From aa9a8f4878a0996e362606eb20f1dfa039c53af2 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Sat, 12 Jul 2014 19:01:58 -0700 Subject: [PATCH] integrated plugin model for remotes --- plugin/remote/bitbucket/init.go | 20 ++++ plugin/remote/github/init.go | 21 ++++ plugin/remote/gitlab/init.go | 17 +++ plugin/remote/remote.go | 19 ++++ server/app/index.html | 2 + server/app/scripts/app.js | 1 + server/app/scripts/controllers/login.js | 13 +++ server/app/scripts/controllers/setup.js | 12 +- server/app/scripts/filters/filters.js | 25 +++++ server/app/scripts/services/remote.js | 13 +++ server/app/styles/drone.css | 66 +++++++++++ server/app/styles/drone.less | 52 +++++++++ server/app/styles/drone.min.css | 2 +- server/app/views/login.html | 28 ++--- server/app/views/setup.html | 142 ++++++++++++++++-------- server/database/config.go | 2 + server/database/remote.go | 18 +++ server/handler/commit.go | 9 +- server/handler/config.go | 35 ------ server/handler/hook.go | 33 ++++-- server/handler/login.go | 45 +++++--- server/handler/remote.go | 64 ++++++++--- server/handler/repo.go | 23 ++-- server/handler/ws.go | 5 +- server/main.go | 20 ++-- server/worker/dispatch.go | 10 +- server/worker/request.go | 12 -- server/worker/worker.go | 37 +++--- shared/model/config.go | 94 ---------------- shared/model/request.go | 9 ++ 30 files changed, 552 insertions(+), 297 deletions(-) create mode 100644 plugin/remote/bitbucket/init.go create mode 100644 plugin/remote/github/init.go create mode 100644 plugin/remote/gitlab/init.go create mode 100644 server/app/scripts/controllers/login.js create mode 100644 server/app/scripts/services/remote.js delete mode 100644 server/handler/config.go delete mode 100644 server/worker/request.go delete mode 100644 shared/model/config.go create mode 100644 shared/model/request.go diff --git a/plugin/remote/bitbucket/init.go b/plugin/remote/bitbucket/init.go new file mode 100644 index 000000000..ec9464e07 --- /dev/null +++ b/plugin/remote/bitbucket/init.go @@ -0,0 +1,20 @@ +package bitbucket + +import ( + "github.com/drone/drone/plugin/remote" + "github.com/drone/drone/shared/model" +) + +func init() { + remote.Register(model.RemoteBitbucket, plugin) +} + +func plugin(remote *model.Remote) remote.Remote { + return &Bitbucket{ + URL: remote.URL, + API: remote.API, + Client: remote.Client, + Secret: remote.Secret, + Enabled: remote.Open, + } +} diff --git a/plugin/remote/github/init.go b/plugin/remote/github/init.go new file mode 100644 index 000000000..35e3a0b77 --- /dev/null +++ b/plugin/remote/github/init.go @@ -0,0 +1,21 @@ +package github + +import ( + "github.com/drone/drone/plugin/remote" + "github.com/drone/drone/shared/model" +) + +func init() { + remote.Register(model.RemoteGithub, plugin) + remote.Register(model.RemoteGithubEnterprise, plugin) +} + +func plugin(remote *model.Remote) remote.Remote { + return &Github{ + URL: remote.URL, + API: remote.API, + Client: remote.Client, + Secret: remote.Secret, + Enabled: remote.Open, + } +} diff --git a/plugin/remote/gitlab/init.go b/plugin/remote/gitlab/init.go new file mode 100644 index 000000000..336991ee8 --- /dev/null +++ b/plugin/remote/gitlab/init.go @@ -0,0 +1,17 @@ +package gitlab + +import ( + "github.com/drone/drone/plugin/remote" + "github.com/drone/drone/shared/model" +) + +func init() { + remote.Register(model.RemoteGitlab, plugin) +} + +func plugin(remote *model.Remote) remote.Remote { + return &Gitlab{ + URL: remote.URL, + Enabled: remote.Open, + } +} diff --git a/plugin/remote/remote.go b/plugin/remote/remote.go index 8acecaa22..041ae1fe1 100644 --- a/plugin/remote/remote.go +++ b/plugin/remote/remote.go @@ -2,8 +2,27 @@ package remote import ( "net/http" + + "github.com/drone/drone/shared/model" ) +// Defines a model for integrating (or pluggin in) remote version +// control systems, such as GitHub and Bitbucket. +type Plugin func(*model.Remote) Remote + +var plugins = map[string]Plugin{} + +// Register registers a new plugin. +func Register(name string, plugin Plugin) { + plugins[name] = plugin +} + +// Lookup retrieves the plugin for the remote. +func Lookup(name string) (Plugin, bool) { + plugin, ok := plugins[name] + return plugin, ok +} + type Remote interface { // GetName returns the name of this remote system. GetName() string diff --git a/server/app/index.html b/server/app/index.html index b53cb0dc8..ca375faf0 100644 --- a/server/app/index.html +++ b/server/app/index.html @@ -42,11 +42,13 @@ + + diff --git a/server/app/scripts/app.js b/server/app/scripts/app.js index a113c5909..eb09477c8 100644 --- a/server/app/scripts/app.js +++ b/server/app/scripts/app.js @@ -23,6 +23,7 @@ app.config(['$routeProvider', '$locationProvider', function($routeProvider, $loc }) .when('/login', { templateUrl: '/views/login.html', + controller: 'LoginController', title: 'Login', }) .when('/setup', { diff --git a/server/app/scripts/controllers/login.js b/server/app/scripts/controllers/login.js new file mode 100644 index 000000000..fcae0f331 --- /dev/null +++ b/server/app/scripts/controllers/login.js @@ -0,0 +1,13 @@ +'use strict'; + +angular.module('app').controller("LoginController", function($scope, $http, remotes) { + $scope.state=0 + $scope.user = remotes.getLogins().success(function (data) { + $scope.remotes = (typeof data==="string")?[]:data; + $scope.state = 1; + }) + .error(function (error) { + $scope.remotes = []; + $scope.state = 1; + }); +}); \ No newline at end of file diff --git a/server/app/scripts/controllers/setup.js b/server/app/scripts/controllers/setup.js index 9b86e077c..fd06ad0f8 100644 --- a/server/app/scripts/controllers/setup.js +++ b/server/app/scripts/controllers/setup.js @@ -1,14 +1,18 @@ 'use strict'; -angular.module('app').controller("SetupController", function($scope, $http, $routeParams) { +angular.module('app').controller("SetupController", function($scope, $http, $routeParams, $window) { // create a remote that will be populated // and persisted to the database. $scope.remote = {}; $scope.remote.type = $routeParams.remote; - $scope.remote.register = true; + $scope.remote.register = false; + $scope.window = $window + + // pre-populate the form if the remote + // type is selected and is a cloud service + // with a known URL and standard configuration. switch($scope.remote.type) { - case undefined: case 'github.com': $scope.remote.type = "github.com" $scope.remote.url = "https://github.com"; @@ -26,7 +30,7 @@ angular.module('app').controller("SetupController", function($scope, $http, $rou success(function(data, status, headers, config) { delete $scope.failure; $scope.remote = data; - console.log('success', $scope.remote); + $window.location.href="/login/"+data.type; }). error(function(data, status, headers, config) { $scope.failure = data; diff --git a/server/app/scripts/filters/filters.js b/server/app/scripts/filters/filters.js index 6f25687d3..c5c3b8d6d 100644 --- a/server/app/scripts/filters/filters.js +++ b/server/app/scripts/filters/filters.js @@ -64,6 +64,31 @@ angular.module('app').filter('badgeMarkup', function() { } }); +angular.module('app').filter('remoteName', function() { + return function(name) { + switch (name) { + case 'gitlab.com' : return 'GitLab'; + case 'github.com' : return 'GitHub'; + case 'enterprise.github.com' : return 'GitHub Enterprise'; + case 'bitbucket.org' : return 'Bitbucket'; + case 'stash.atlassian.com' : return 'Atlassian Stash'; + } + } +}); + +angular.module('app').filter('remoteIcon', function() { + return function(name) { + switch (name) { + case 'gitlab.com' : return 'fa-git-square'; + case 'github.com' : return 'fa-github-square'; + case 'enterprise.github.com' : return 'fa-github-square'; + case 'bitbucket.org' : return 'fa-bitbucket-square'; + case 'stash.atlassian.com' : return 'fa-bitbucket-square'; + } + } +}); + + angular.module('app').filter('unique', function() { return function(input, key) { var unique = {}; diff --git a/server/app/scripts/services/remote.js b/server/app/scripts/services/remote.js new file mode 100644 index 000000000..21f6e08fa --- /dev/null +++ b/server/app/scripts/services/remote.js @@ -0,0 +1,13 @@ +'use strict'; + +// Service facilitates interaction with the remote API. +angular.module('app').service('remotes', ['$http', function($http) { + + this.get = function() { + return $http.get('/v1/remotes'); + }; + + this.getLogins = function() { + return $http.get('/v1/logins'); + }; +}]); \ No newline at end of file diff --git a/server/app/styles/drone.css b/server/app/styles/drone.css index 4ce7e2eb5..48996ea89 100644 --- a/server/app/styles/drone.css +++ b/server/app/styles/drone.css @@ -650,6 +650,7 @@ nav div.options .pure-button i { -o-box-sizing: border-box; box-sizing: border-box; } +#setuppage .pure-g, #loginpage .pure-g { padding: 30px; border: 1px solid #DDD; @@ -662,6 +663,7 @@ nav div.options .pure-button i { -o-box-sizing: border-box; box-sizing: border-box; } +#setuppage .pure-g a, #loginpage .pure-g a { display: block; background: #45494b; @@ -671,9 +673,11 @@ nav div.options .pure-button i { border-radius: 5px; text-decoration: none; } +#setuppage .pure-g a:hover, #loginpage .pure-g a:hover { background: #262626; } +#setuppage .pure-g [class*="fa-"], #loginpage .pure-g [class*="fa-"] { float: left; font-size: 20px; @@ -684,12 +688,74 @@ nav div.options .pure-button i { min-width: 27px; min-height: 20px; } +#setuppage .pure-g .pure-u-1 a, #loginpage .pure-g .pure-u-1 a { margin-bottom: 10px; } +#setuppage .pure-g .pure-u-1:last-child a, #loginpage .pure-g .pure-u-1:last-child a { margin-bottom: 0px; } +#setuppage2 { + margin-bottom: 50px; +} +#setuppage2 section { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} +#setuppage2 section .pure-g { + padding: 30px; + border: 1px solid #DDD; + max-width: 400px; + margin: 0px auto; + margin-top: 50px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} +#setuppage2 section label { + display: inline-block; +} +#setuppage2 section input[type='text'] { + margin-top: 5px; + margin-bottom: 10px; + box-shadow: none; + width: 100%; +} +#setuppage2 section .pure-button-primary { + color: #FFF; + background: #4ab1ce; + padding: 10px 20px; + margin-top: 20px; + width: 100%; +} +#setuppage2 section .tip h2 { + font-size: 16px; + margin-bottom: 20px; +} +#setuppage2 section .tip dd { + font-weight: bold; + color: #666; + margin-top: 15px; + margin-bottom: 5px; +} +#setuppage2 section .tip dt { + padding: .5em .6em; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; + width: 100%; +} #syncpage { width: 100%; } diff --git a/server/app/styles/drone.less b/server/app/styles/drone.less index 2c5ec8ce1..b4f76f1aa 100644 --- a/server/app/styles/drone.less +++ b/server/app/styles/drone.less @@ -579,6 +579,7 @@ nav { } } +#setuppage, #loginpage { .pure-g { padding: 30px; @@ -622,6 +623,57 @@ nav { } } +#setuppage2 { + margin-bottom:50px; + section { + .border_box; + .pure-g { + padding: 30px; + border: 1px solid #DDD; + max-width:400px; + margin:0px auto; + margin-top:50px; + .border_box; + } + label { + display:inline-block; + } + input[type='text'] { + margin-top:5px; + margin-bottom:10px; + box-shadow:none; + width:100%; + } + .pure-button-primary { + color:#FFF; + background: @link2; + padding:10px 20px; + margin-top:20px; + width:100%; + } + .tip { + h2 { + font-size: 16px; + margin-bottom: 20px; + } + dd { + font-weight:bold; + color:#666; + margin-top:15px; + margin-bottom:5px; + } + dt { + padding: .5em .6em; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + .border_box; + width: 100%; + } + } + } +} + #syncpage { width:100%; section { diff --git a/server/app/styles/drone.min.css b/server/app/styles/drone.min.css index dc6bcfb72..80214486a 100644 --- a/server/app/styles/drone.min.css +++ b/server/app/styles/drone.min.css @@ -1 +1 @@ -html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;list-style:none}.hidden{display:none!important;visibility:hidden}.invisible{visibility:hidden}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}.nowrap{white-space:nowrap}.border_box{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.fix3d{-webkit-transform:translate3D(0,0,0)}.border_box{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.invert{-webkit-filter:invert(100%);-moz-filter:invert(100%);-ms-filter:invert(100%);-o-filter:invert(100%);filter:invert(100%)}html{height:100%}body{font-family:'Open Sans';font-weight:400;margin:0;color:#262626;background:#fff;font-size:13px;line-height:1.3;-webkit-font-smoothing:antialiased;height:100%;position:relative}[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none}#container{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding-top:55px;position:relative;min-width:100%;min-height:100%;display:flex;flex-direction:row-reverse;justify-content:space-between}#header{background:#262626;position:fixed;height:55px;top:0;left:0;right:0;z-index:9;color:#fff;font-size:15px;line-height:55px;text-align:center}#header .brand{display:inline-block;font-family:Orbitron;font-size:26px;line-height:55px;text-decoration:none;text-transform:uppercase;color:#CCC}#header .burger{position:absolute;top:0;left:31px;height:55px;font-size:22px;color:#CCC}#header .burger i.fa{line-height:55px}#header .login,#header .user{position:absolute;right:0;top:0;bottom:0;white-space:nowrap;margin-right:20px;display:inline-block}#header .login a,#header .user a{color:#CCC;text-decoration:none;text-transform:uppercase;line-height:55px;font-size:15px}#header .login a img,#header .user a img{border-radius:50%;float:right;width:32px;height:32px;margin-top:10px;margin-left:20px}#body{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;display:flex;min-width:100%;flex-direction:row-reverse;justify-content:space-between}#body article{width:100%}#drawer{visibility:hidden;position:fixed;z-index:10;left:0;top:55px;bottom:0;width:255px;background:#363636;-webkit-transition:all .2s;-moz-transition:all .2s;transition:all .2s;-webkit-transform:translate3d(-100%,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(-100%,0,0)}#drawer ul{margin-top:20px}#drawer ul a{color:#CCC;text-decoration:none;padding:10px 0 10px 30px;display:block;font-size:14px}#drawer ul a i{margin-right:10px;font-size:16px;opacity:.3;min-width:16px;display:inline-block}#drawer ul span.divider{display:block;height:1px;border-top:1px solid rgba(255,255,255,.1);margin-top:15px;margin-bottom:15px}#drawer .signout{position:absolute;bottom:20px;right:30px;color:#CCC;font-size:16px;text-transform:uppercase;text-decoration:none}#drawer .signout i{margin-left:20px}#drawer-checkbox{position:fixed;top:7px;left:10px;width:45px;height:40px;display:block;z-index:9999;opacity:0;background:0 0;border:none;cursor:pointer}#drawer-checkbox:checked~#drawer{visibility:visible;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}#drawer-checkbox:checked~#drawer{visibility:visible;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}#drawer-checkbox:checked~#header .fa-bars:before{content:"\f00d";color:#999}nav{padding-left:30px;background:#FFF;min-height:77px;max-height:77px;line-height:77px;font-family:'Open Sans';font-size:22px;white-space:nowrap;color:rgba(0,0,0,.7);border-bottom:1px solid #EEE;position:relative;z-index:2}nav a{text-decoration:none;color:rgba(0,0,0,.7)}nav a:last-child{color:#000}nav a span.fa{margin-right:20px}nav div.options{float:right;margin-right:20px}nav div.options .pure-button{color:#FFF;text-transform:lowercase;font-size:14px;background:#f2f5f8;padding:10px 30px;color:rgba(0,0,0,.5)}nav div.options .pure-button i{margin-right:10px;margin-left:-10px}@supports (position:sticky){nav{position:sticky;top:55px}}@supports (position:-moz-sticky){nav{position:-moz-sticky;top:55px}}@supports (position:-webkit-sticky){nav{position:-webkit-sticky;top:55px}}.cards .card{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding-right:20px;padding-bottom:20px;text-decoration:none;position:relative;color:#262626;font-family:'Open Sans'}.cards .card[data-status=Success] em{border-top:5px solid #68c598}.cards .card[data-status=Killed] em,.cards .card[data-status=Failure] em,.cards .card[data-status=Error] em{border-top:5px solid #e97041}.cards .card[data-status=Killed] .l-box,.cards .card[data-status=Failure] .l-box,.cards .card[data-status=Error] .l-box{border:1px solid #e97041}.cards .card[data-status=Killed] .l-box:hover,.cards .card[data-status=Failure] .l-box:hover,.cards .card[data-status=Error] .l-box:hover{border:1px solid #262626}.cards .card[data-status=Killed]:after,.cards .card[data-status=Failure]:after,.cards .card[data-status=Error]:after{font-family:FontAwesome;font-size:16px;position:absolute;right:12px;top:-8px;content:"\f111";color:#e97041;min-width:16px;text-align:center}.cards .card .l-box{background:#FFF;border:1px solid #DDD;position:relative;padding:30px 20px;height:200px;-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.cards .card .l-box:hover{border:1px solid #262626}.cards .card .l-box em{position:absolute;bottom:20px;right:20px;left:20px;height:30px;line-height:30px;vertical-align:middle;text-align:right;padding-right:45px;padding-top:20px;font-size:14px;color:#666}.cards .card .l-box img{position:absolute;right:20px;bottom:20px;border-radius:50%;width:30px;height:30px}.cards .card .l-box .timeago{position:absolute;bottom:85px;left:25px;right:25px;text-align:right;font-size:14px;color:#849299;display:none}.cards .card h2{font-size:18px;font-weight:400;min-height:52px;max-height:52px;height:52px;text-align:center;vertical-align:middle;line-height:26px;color:#262626;text-overflow:ellipsis;-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.cards .card h2 .separator{margin:0}.cards .card.card-inactive .l-box{position:relative;box-shadow:none;background:#4ab1ce;color:#FFF;height:180px;border-color:#4ab1ce}.cards .card.card-inactive .l-box:hover{background:#3197b4}.cards .card.card-inactive .l-box:hover:before{background:#3197b4}.cards .card.card-inactive h2{padding-top:10px;color:#FFF}.cards .card.card-inactive em{position:absolute;border-top:1px solid rgba(255,255,255,.5);bottom:15px;font-size:13px;left:25px;right:25px;line-height:1.3;padding:0;padding-top:20px;text-align:center;display:block;height:30px;text-transform:uppercase;color:#FFF}.cards .card.card-browse-inactive,.cards .card.card-browse{text-align:center;color:#4ab1ce;font-size:16px;font-weight:700;text-transform:uppercase}.cards .card.card-browse-inactive .l-box,.cards .card.card-browse .l-box{padding-top:75px;background:#FFF;height:180px}.cards .card.card-browse-inactive .l-box{box-shadow:none}.cards .progressContainer{height:5px;background-color:#e97041;position:absolute;bottom:65px;left:20px;right:20px}.cards .progressContainer .activeProgress,.cards .progressContainer .secondaryProgress{position:absolute;top:0;left:0;bottom:0}.cards .progressContainer .activeProgress{background-color:#68c598}.cards .progressContainer .secondaryProgress{background-color:#68c598}#commitpage{max-width:1180px;margin:0 auto;margin-bottom:50px;margin-top:70px}#commitpage section{margin-top:30px}#commitpage section .commits{border:1px solid #DDD;border-bottom:0 solid #DDD;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#commitpage section .commits a{padding:20px 45px;display:block;border-bottom:1px solid #dadcdd;color:#262626;text-decoration:none;position:relative;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#commitpage section .commits a h2{font-family:'Open Sans';font-weight:700;font-size:16px;margin-bottom:5px}#commitpage section .commits a img{border-radius:50%;margin-right:10px;float:left;display:none}#commitpage section .commits a p{color:#363636;line-height:22px;vertical-align:middle}#commitpage section .commits a[data-status]:before{background:0 0;width:7px;min-width:7px;max-width:7px;position:absolute;left:-1px;top:0;bottom:0;text-align:left;color:#fff;font-size:20px;line-height:50px;font-family:'Open Sans';padding-left:2px;overflow:hidden;content:" "}#commitpage section .commits a[data-result=Killed],#commitpage section .commits a[data-status=Error],#commitpage section .commits a[data-status=Failure]{background:#fff9f5}#commitpage section .commits a[data-result=Killed]:before,#commitpage section .commits a[data-status=Error]:before,#commitpage section .commits a[data-status=Failure]:before{background:#e97041;content:"!"}#commitpage section .commits a[data-status=Success]:before{background:#68c598}#commitpage .date span{display:inline-block;text-align:right;font-size:14px;width:100%;padding-right:30px;margin-top:15px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#loginpage .pure-g{padding:30px;border:1px solid #DDD;max-width:400px;margin:0 auto;margin-top:50px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#loginpage .pure-g a{display:block;background:#45494b;color:#fff;padding:14px 20px;font-size:15px;border-radius:5px;text-decoration:none}#loginpage .pure-g a:hover{background:#262626}#loginpage .pure-g [class*=fa-]{float:left;font-size:20px;position:relative;top:-3px;left:-3px;padding-right:10px;min-width:27px;min-height:20px}#loginpage .pure-g .pure-u-1 a{margin-bottom:10px}#loginpage .pure-g .pure-u-1:last-child a{margin-bottom:0}#syncpage{width:100%}#syncpage section{padding:40px 0 20px}#syncpage section h1{text-align:center;font-size:24px;margin-bottom:20px}#syncpage section h1 i{font-size:32px;margin-top:20px}#homepage{width:100%;background:#f2f5f8}#homepage section{padding:40px 0 20px;border-bottom:1px solid #EEE}#homepage section h1{text-align:center;font-size:24px;margin-bottom:20px}#homepage section h1 i{font-size:32px;margin-top:20px}#homepage section div{max-width:1180px;margin:0 auto}#homepage section:nth-child(2){background:#FFF;padding:40px 0 20px}#homepage section:nth-child(3){border-bottom:0 solid #EEE}#homepage section .card[data-status=Killed] .l-box,#homepage section .card[data-status=Failure] .l-box,#homepage section .card[data-status=Error] .l-box{border:1px solid #e97041}#homepage section .card[data-status=Killed] .l-box:hover,#homepage section .card[data-status=Failure] .l-box:hover,#homepage section .card[data-status=Error] .l-box:hover{border:1px solid #262626}#repospage{width:100%}#repospage section{border-bottom:1px solid #eee;max-width:768px;margin:0 auto;margin-top:30px;margin-bottom:30px}#repospage section .search{margin-bottom:25px}#repospage section .search input[type=text],#repospage section .search input[type=search]{-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear;border:1px solid #ccc;border-radius:0;box-shadow:none;padding:12px}#repospage section .search input[type=text]:focus,#repospage section .search input[type=search]:focus{border-color:#129FEA}#repospage section .repo{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;text-decoration:none}#repospage section .repo:last-child>div{border-bottom:1px solid #fff}#repospage section .repo>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border:1px solid #eee;border-bottom:1px solid #fff;-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear}#repospage section .repo>div>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px 25px}#repospage section .repo>div>div:last-child div{text-align:right}#repospage section .repo>div:hover{border:1px solid #262626}#repospage section .repo h4{font-size:20px;margin-bottom:2px;color:#262626}#repospage section .repo span{color:#666;font-size:14px}#repospage section .repo i{color:#DDD;font-size:22px;margin-left:20px;margin-top:15px}#repospage section .repo i.fa-check-circle-o{color:#68c598}#userspage{width:100%}#userspage section{border-bottom:1px solid #eee;max-width:768px;margin:0 auto;margin-top:30px;margin-bottom:30px}#userspage section .search{margin-bottom:25px}#userspage section .search input[type=text],#userspage section .search input[type=search]{-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear;border:1px solid #ccc;border-radius:0;box-shadow:none;padding:12px}#userspage section .search input[type=text]:focus,#userspage section .search input[type=search]:focus{border-color:#129FEA}#userspage section .user{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;text-decoration:none}#userspage section .user:last-child>div{border-bottom:1px solid #fff}#userspage section .user>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border:1px solid #eee;border-bottom:1px solid #fff;-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear}#userspage section .user>div>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px 25px;padding-right:0}#userspage section .user>div:hover{border:1px solid #262626}#userspage section .user img{border-radius:50%;width:48px;height:48px}#userspage section .user h4{font-size:20px;margin-bottom:2px;color:#262626}#userspage section .user h4 small{font-size:16px;color:#666;margin-left:5px}#userspage section .user span{color:#666;font-size:14px}#repoconfpage{width:100%}#repoconfpage section{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border:1px solid #eee;max-width:768px;margin:0 auto;margin-top:30px;margin-bottom:30px;padding:20px}#repoconfpage section h2{font-size:16px;margin-bottom:15px}#repoconfpage section .markdown,#repoconfpage section .params,#repoconfpage section .key{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;min-height:50px;margin-top:10px;font-family:'Droid Sans Mono';border:1px solid #eee;padding:20px;width:100%;max-width:100%;color:#666}#repoconfpage section .markdown:focus,#repoconfpage section .params:focus,#repoconfpage section .key:focus{border-color:#129FEA;outline:0}#repoconfpage section .pure-button-primary{color:#FFF;background:#4ab1ce;padding:10px 20px;margin-top:20px}#accountpage{width:100%}#accountpage section{position:relative;max-width:768px;margin:0 auto;margin-top:30px;border:1px solid #eee}#accountpage section.profile>div:first-child{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px;text-align:center}#accountpage section.profile>div:last-child{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px}#accountpage section.profile .fullname{font-size:14px;margin-bottom:2px;color:#666;display:block}#accountpage section.profile .email{font-size:14px;color:#666;display:block}#accountpage section.token>div:first-child div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;text-align:center;padding:20px;color:#666;font-size:16px;line-height:22px}#accountpage section.token>div:first-child i{margin-right:7px}#accountpage section.token>div:last-child{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px;color:#666;line-height:22px;font-size:14px}#accountpage section h4{margin:10px 0;font-size:22px}#accountpage section h4 small{opacity:.6;font-size:16px;margin-left:10px}#accountpage section img{width:64px;height:64px;border-radius:50%}#accountpage section .notifications{position:absolute;top:0;right:0;margin:20px}#accountpage section .button-error{color:#FFF;background:#ca3c3c;padding:10px 20px;float:right}#accountpage section .pure-button-primary{color:#FFF;background:#4ab1ce;padding:10px 20px;margin-top:10px}#repopage{width:100%;background:#f2f5f8}#repopage section{padding:40px 0 20px;border-bottom:1px solid #EEE}#repopage section>div{max-width:1180px;margin:0 auto}#repopage section:nth-child(even){background:#FFF}#repopage section:first-child{background:#FFF}#repopage section .card[data-status=Success]:nth-child(2) .l-box{border-color:#68c598}#repopage section .card[data-status=Success]:nth-child(2) .l-box:hover{border:1px solid #262626}#repopage section .card[data-status=Killed]:nth-child(2) .l-box,#repopage section .card[data-status=Failure]:nth-child(2) .l-box,#repopage section .card[data-status=Error]:nth-child(2) .l-box{border-color:#e97041}#repopage section .card[data-status=Killed]:nth-child(2) .l-box:hover,#repopage section .card[data-status=Failure]:nth-child(2) .l-box:hover,#repopage section .card[data-status=Error]:nth-child(2) .l-box:hover{border:1px solid #262626}#repopage section .card[data-status=Started] em:before,#repopage section .card[data-status=Pending] em:before{-webkit-animation:progress 1s linear infinite;-moz-animation:progress 1s linear infinite;animation:progress 1s linear infinite;position:absolute;content:' ';height:5px;top:-5px;left:0;right:0;margin:0;background:#fee172;background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,.55)25%,transparent 25%,transparent 50%,rgba(255,255,255,.55)50%,rgba(255,255,255,.55)75%,transparent 75%,transparent);background-image:linear-gradient(-45deg,rgba(255,255,255,.55)25%,transparent 25%,transparent 50%,rgba(255,255,255,.55)50%,rgba(255,255,255,.55)75%,transparent 75%,transparent);background-repeat:repeat-x;background-size:30px 30px}#repopage section .l-box:after{font-family:FontAwesome;content:"\f104";content:"\f0d9";position:absolute;right:-20px;width:20px;text-align:center;color:rgba(0,0,0,.1);font-size:22px}#repopage section .card:last-child .l-box:after{content:''}#repopage section.nobuilds,#repopage section.inactive{text-align:center;padding-bottom:50px}#repopage section.nobuilds h1,#repopage section.inactive h1{font-size:26px;color:#262626}#repopage section.nobuilds p,#repopage section.inactive p{font-size:16px;color:#666}#repopage section.nobuilds i,#repopage section.inactive i{font-size:32px;margin-top:20px;margin-bottom:20px}#repopage section.nobuilds i.fa-file-code-o,#repopage section.inactive i.fa-file-code-o{font-size:42px;margin-top:30px}#repopage section.nobuilds .pure-button-primary,#repopage section.inactive .pure-button-primary{font-size:14px;text-transform:uppercase;background:#4ab1ce;padding:10px 30px}@-webkit-keyframes progress{to{background-position:30px 0}}@-moz-keyframes progress{to{background-position:30px 0}}@keyframes progress{to{background-position:30px 0}}#sidebar{width:240px;min-width:240px;position:relative;display:block;z-index:5;padding:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#sidebar h1{font-size:28px;font-weight:300}#sidebar h2{font-size:22px;font-weight:300;margin-bottom:20px}#sidebar dl{padding-top:23px;border-top:1px solid #ddd;margin-top:5px}#sidebar dl:first-child{padding-top:0;border-top:none;margin-top:0}#sidebar dl dt{font-size:12px;color:#849299;text-transform:uppercase;padding:3px 0}#sidebar dl dd{font-size:14px;padding:3px 0 20px}#sidebar dl a{text-transform:none}#sidebar dl small{font-size:12px}#sidebar dl .large{font-size:18px;padding-bottom:5px}#sidebar dl .time{float:right;margin-left:8px}#sidebar dl .photo{margin-right:4px}#sidebar dl .negative{color:#e97041}#sidebar dl .photoline{display:inline-block;position:relative;top:-10px;font-weight:700}#sidebar dl .small{padding-bottom:5px;font-weight:700;font-size:12px}#sidebar .status{border:1px solid transparent;display:block;text-align:center;padding:5px 20px;border-radius:50px;text-transform:uppercase;margin:0 -5px 10px;font-weight:700}#sidebar .status:before{float:left;margin-left:-5px}#sidebar .status.status_ok{color:#68c598;border-color:#68c598}#sidebar .status.status_ok:before{content:"\f00c";font-family:FontAwesome}#sidebar .status.status_error{color:#e97041;border-color:#e97041}#sidebar .status.status_error:before{content:"!"}#sidebar .result{background:#4ab1ce;background:#68c598;color:#fff;margin:-30px -30px -6px;padding:30px;position:relative}#sidebar .result .status{color:#fff;background:rgba(255,255,255,.2)}#sidebar .result .status:before{content:"\f00c";font-family:FontAwesome}#sidebar .result dl dd{padding:7px 0}#sidebar .result dl dd strong{font-size:16px}#sidebar .result[data-result=Killed],#sidebar .result[data-result=Failure],#sidebar .result[data-result=Error]{background:#e97041}#sidebar .result[data-result=Killed] .status:before,#sidebar .result[data-result=Failure] .status:before,#sidebar .result[data-result=Error] .status:before{content:"!"}#sidebar .result[data-result=Pending],#sidebar .result[data-result=Started]{background:#4ab1ce}#main{flex-grow:2}#main.output{background:#525252}#main.output pre{margin:0 auto;padding:30px;color:#FFF;font-family:'Droid Sans Mono';font-size:13px;white-space:pre-wrap;overflow:hidden}.pure-form.pure-form-stacked label{font-size:14px;color:#666;margin-top:20px;margin-bottom:5px}.pure-form.pure-form-stacked label:first-child{margin-top:0}.pure-form.pure-form-stacked input[type=text],.pure-form.pure-form-stacked select{min-width:300px;box-shadow:none;padding:10px;border:1px solid #ccc;border-radius:0}.toggle{margin-bottom:10px}.toggle:nth-child(2){margin-top:40px}.toggle span{line-height:32px;vertical-align:middle;display:inline-block;margin-left:10px}.toggle input[type=checkbox]{max-height:0;max-width:0;opacity:0}.toggle input[type=checkbox]+label{display:inline-block;vertical-align:middle;position:relative;box-shadow:inset 0 0 0 1px #d5d5d5;text-indent:-5000px;height:30px;width:50px;border-radius:15px}.toggle input[type=checkbox]+label:before{content:"";position:absolute;display:block;height:30px;width:30px;top:0;left:0;border-radius:15px;background:rgba(19,191,17,0);-webkit-transition:.25s ease-in-out;-moz-transition:.25s ease-in-out;-ms-transition:.25s ease-in-out;-o-transition:.25s ease-in-out;transition:.25s ease-in-out}.toggle input[type=checkbox]+label:after{content:"";position:absolute;display:block;height:30px;width:30px;top:0;left:0;border-radius:15px;background:#fff;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2),0 2px 4px rgba(0,0,0,.2);-webkit-transition:.25s ease-in-out;-moz-transition:.25s ease-in-out;-ms-transition:.25s ease-in-out;-o-transition:.25s ease-in-out;transition:.25s ease-in-out}.toggle input[type=checkbox]:checked+label:before{width:50px;background:#4ab1ce}.toggle input[type=checkbox]:checked+label:after{left:20px;box-shadow:inset 0 0 0 1px #4ab1ce,0 2px 4px rgba(0,0,0,.2)}.toast{position:fixed;bottom:50px;left:20%;right:20%;background:#363636;border-radius:3px;z-index:999;color:#FFF;padding:15px 20px;font-size:14px;box-shadow:2px 2px 2px rgba(0,0,0,.2)}.toast a,.toast a:visited,.toast a:hover,.toast a:active{color:#FFF}.toast button{float:right;background:0 0;border:none;color:#EEFF41;text-transform:uppercase;margin-left:10px} \ No newline at end of file +html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;list-style:none}.hidden{display:none!important;visibility:hidden}.invisible{visibility:hidden}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}.nowrap{white-space:nowrap}.border_box{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.fix3d{-webkit-transform:translate3D(0,0,0)}.border_box{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.invert{-webkit-filter:invert(100%);-moz-filter:invert(100%);-ms-filter:invert(100%);-o-filter:invert(100%);filter:invert(100%)}html{height:100%}body{font-family:'Open Sans';font-weight:400;margin:0;color:#262626;background:#fff;font-size:13px;line-height:1.3;-webkit-font-smoothing:antialiased;height:100%;position:relative}[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none}#container{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding-top:55px;position:relative;min-width:100%;min-height:100%;display:flex;flex-direction:row-reverse;justify-content:space-between}#header{background:#262626;position:fixed;height:55px;top:0;left:0;right:0;z-index:9;color:#fff;font-size:15px;line-height:55px;text-align:center}#header .brand{display:inline-block;font-family:Orbitron;font-size:26px;line-height:55px;text-decoration:none;text-transform:uppercase;color:#CCC}#header .burger{position:absolute;top:0;left:31px;height:55px;font-size:22px;color:#CCC}#header .burger i.fa{line-height:55px}#header .login,#header .user{position:absolute;right:0;top:0;bottom:0;white-space:nowrap;margin-right:20px;display:inline-block}#header .login a,#header .user a{color:#CCC;text-decoration:none;text-transform:uppercase;line-height:55px;font-size:15px}#header .login a img,#header .user a img{border-radius:50%;float:right;width:32px;height:32px;margin-top:10px;margin-left:20px}#body{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;display:flex;min-width:100%;flex-direction:row-reverse;justify-content:space-between}#body article{width:100%}#drawer{visibility:hidden;position:fixed;z-index:10;left:0;top:55px;bottom:0;width:255px;background:#363636;-webkit-transition:all .2s;-moz-transition:all .2s;transition:all .2s;-webkit-transform:translate3d(-100%,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(-100%,0,0)}#drawer ul{margin-top:20px}#drawer ul a{color:#CCC;text-decoration:none;padding:10px 0 10px 30px;display:block;font-size:14px}#drawer ul a i{margin-right:10px;font-size:16px;opacity:.3;min-width:16px;display:inline-block}#drawer ul span.divider{display:block;height:1px;border-top:1px solid rgba(255,255,255,.1);margin-top:15px;margin-bottom:15px}#drawer .signout{position:absolute;bottom:20px;right:30px;color:#CCC;font-size:16px;text-transform:uppercase;text-decoration:none}#drawer .signout i{margin-left:20px}#drawer-checkbox{position:fixed;top:7px;left:10px;width:45px;height:40px;display:block;z-index:9999;opacity:0;background:0 0;border:none;cursor:pointer}#drawer-checkbox:checked~#drawer{visibility:visible;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}#drawer-checkbox:checked~#drawer{visibility:visible;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}#drawer-checkbox:checked~#header .fa-bars:before{content:"\f00d";color:#999}nav{padding-left:30px;background:#FFF;min-height:77px;max-height:77px;line-height:77px;font-family:'Open Sans';font-size:22px;white-space:nowrap;color:rgba(0,0,0,.7);border-bottom:1px solid #EEE;position:relative;z-index:2}nav a{text-decoration:none;color:rgba(0,0,0,.7)}nav a:last-child{color:#000}nav a span.fa{margin-right:20px}nav div.options{float:right;margin-right:20px}nav div.options .pure-button{color:#FFF;text-transform:lowercase;font-size:14px;background:#f2f5f8;padding:10px 30px;color:rgba(0,0,0,.5)}nav div.options .pure-button i{margin-right:10px;margin-left:-10px}@supports (position:sticky){nav{position:sticky;top:55px}}@supports (position:-moz-sticky){nav{position:-moz-sticky;top:55px}}@supports (position:-webkit-sticky){nav{position:-webkit-sticky;top:55px}}.cards .card{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding-right:20px;padding-bottom:20px;text-decoration:none;position:relative;color:#262626;font-family:'Open Sans'}.cards .card[data-status=Success] em{border-top:5px solid #68c598}.cards .card[data-status=Killed] em,.cards .card[data-status=Failure] em,.cards .card[data-status=Error] em{border-top:5px solid #e97041}.cards .card[data-status=Killed] .l-box,.cards .card[data-status=Failure] .l-box,.cards .card[data-status=Error] .l-box{border:1px solid #e97041}.cards .card[data-status=Killed] .l-box:hover,.cards .card[data-status=Failure] .l-box:hover,.cards .card[data-status=Error] .l-box:hover{border:1px solid #262626}.cards .card[data-status=Killed]:after,.cards .card[data-status=Failure]:after,.cards .card[data-status=Error]:after{font-family:FontAwesome;font-size:16px;position:absolute;right:12px;top:-8px;content:"\f111";color:#e97041;min-width:16px;text-align:center}.cards .card .l-box{background:#FFF;border:1px solid #DDD;position:relative;padding:30px 20px;height:200px;-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}.cards .card .l-box:hover{border:1px solid #262626}.cards .card .l-box em{position:absolute;bottom:20px;right:20px;left:20px;height:30px;line-height:30px;vertical-align:middle;text-align:right;padding-right:45px;padding-top:20px;font-size:14px;color:#666}.cards .card .l-box img{position:absolute;right:20px;bottom:20px;border-radius:50%;width:30px;height:30px}.cards .card .l-box .timeago{position:absolute;bottom:85px;left:25px;right:25px;text-align:right;font-size:14px;color:#849299;display:none}.cards .card h2{font-size:18px;font-weight:400;min-height:52px;max-height:52px;height:52px;text-align:center;vertical-align:middle;line-height:26px;color:#262626;text-overflow:ellipsis;-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.cards .card h2 .separator{margin:0}.cards .card.card-inactive .l-box{position:relative;box-shadow:none;background:#4ab1ce;color:#FFF;height:180px;border-color:#4ab1ce}.cards .card.card-inactive .l-box:hover{background:#3197b4}.cards .card.card-inactive .l-box:hover:before{background:#3197b4}.cards .card.card-inactive h2{padding-top:10px;color:#FFF}.cards .card.card-inactive em{position:absolute;border-top:1px solid rgba(255,255,255,.5);bottom:15px;font-size:13px;left:25px;right:25px;line-height:1.3;padding:0;padding-top:20px;text-align:center;display:block;height:30px;text-transform:uppercase;color:#FFF}.cards .card.card-browse-inactive,.cards .card.card-browse{text-align:center;color:#4ab1ce;font-size:16px;font-weight:700;text-transform:uppercase}.cards .card.card-browse-inactive .l-box,.cards .card.card-browse .l-box{padding-top:75px;background:#FFF;height:180px}.cards .card.card-browse-inactive .l-box{box-shadow:none}.cards .progressContainer{height:5px;background-color:#e97041;position:absolute;bottom:65px;left:20px;right:20px}.cards .progressContainer .activeProgress,.cards .progressContainer .secondaryProgress{position:absolute;top:0;left:0;bottom:0}.cards .progressContainer .activeProgress{background-color:#68c598}.cards .progressContainer .secondaryProgress{background-color:#68c598}#commitpage{max-width:1180px;margin:0 auto;margin-bottom:50px;margin-top:70px}#commitpage section{margin-top:30px}#commitpage section .commits{border:1px solid #DDD;border-bottom:0 solid #DDD;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#commitpage section .commits a{padding:20px 45px;display:block;border-bottom:1px solid #dadcdd;color:#262626;text-decoration:none;position:relative;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#commitpage section .commits a h2{font-family:'Open Sans';font-weight:700;font-size:16px;margin-bottom:5px}#commitpage section .commits a img{border-radius:50%;margin-right:10px;float:left;display:none}#commitpage section .commits a p{color:#363636;line-height:22px;vertical-align:middle}#commitpage section .commits a[data-status]:before{background:0 0;width:7px;min-width:7px;max-width:7px;position:absolute;left:-1px;top:0;bottom:0;text-align:left;color:#fff;font-size:20px;line-height:50px;font-family:'Open Sans';padding-left:2px;overflow:hidden;content:" "}#commitpage section .commits a[data-result=Killed],#commitpage section .commits a[data-status=Error],#commitpage section .commits a[data-status=Failure]{background:#fff9f5}#commitpage section .commits a[data-result=Killed]:before,#commitpage section .commits a[data-status=Error]:before,#commitpage section .commits a[data-status=Failure]:before{background:#e97041;content:"!"}#commitpage section .commits a[data-status=Success]:before{background:#68c598}#commitpage .date span{display:inline-block;text-align:right;font-size:14px;width:100%;padding-right:30px;margin-top:15px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#setuppage .pure-g,#loginpage .pure-g{padding:30px;border:1px solid #DDD;max-width:400px;margin:0 auto;margin-top:50px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#setuppage .pure-g a,#loginpage .pure-g a{display:block;background:#45494b;color:#fff;padding:14px 20px;font-size:15px;border-radius:5px;text-decoration:none}#setuppage .pure-g a:hover,#loginpage .pure-g a:hover{background:#262626}#setuppage .pure-g [class*=fa-],#loginpage .pure-g [class*=fa-]{float:left;font-size:20px;position:relative;top:-3px;left:-3px;padding-right:10px;min-width:27px;min-height:20px}#setuppage .pure-g .pure-u-1 a,#loginpage .pure-g .pure-u-1 a{margin-bottom:10px}#setuppage .pure-g .pure-u-1:last-child a,#loginpage .pure-g .pure-u-1:last-child a{margin-bottom:0}#setuppage2{margin-bottom:50px}#setuppage2 section{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#setuppage2 section .pure-g{padding:30px;border:1px solid #DDD;max-width:400px;margin:0 auto;margin-top:50px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#setuppage2 section label{display:inline-block}#setuppage2 section input[type=text]{margin-top:5px;margin-bottom:10px;box-shadow:none;width:100%}#setuppage2 section .pure-button-primary{color:#FFF;background:#4ab1ce;padding:10px 20px;margin-top:20px;width:100%}#setuppage2 section .tip h2{font-size:16px;margin-bottom:20px}#setuppage2 section .tip dd{font-weight:700;color:#666;margin-top:15px;margin-bottom:5px}#setuppage2 section .tip dt{padding:.5em .6em;display:inline-block;border:1px solid #ccc;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;width:100%}#syncpage{width:100%}#syncpage section{padding:40px 0 20px}#syncpage section h1{text-align:center;font-size:24px;margin-bottom:20px}#syncpage section h1 i{font-size:32px;margin-top:20px}#homepage{width:100%;background:#f2f5f8}#homepage section{padding:40px 0 20px;border-bottom:1px solid #EEE}#homepage section h1{text-align:center;font-size:24px;margin-bottom:20px}#homepage section h1 i{font-size:32px;margin-top:20px}#homepage section div{max-width:1180px;margin:0 auto}#homepage section:nth-child(2){background:#FFF;padding:40px 0 20px}#homepage section:nth-child(3){border-bottom:0 solid #EEE}#homepage section .card[data-status=Killed] .l-box,#homepage section .card[data-status=Failure] .l-box,#homepage section .card[data-status=Error] .l-box{border:1px solid #e97041}#homepage section .card[data-status=Killed] .l-box:hover,#homepage section .card[data-status=Failure] .l-box:hover,#homepage section .card[data-status=Error] .l-box:hover{border:1px solid #262626}#repospage{width:100%}#repospage section{border-bottom:1px solid #eee;max-width:768px;margin:0 auto;margin-top:30px;margin-bottom:30px}#repospage section .search{margin-bottom:25px}#repospage section .search input[type=text],#repospage section .search input[type=search]{-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear;border:1px solid #ccc;border-radius:0;box-shadow:none;padding:12px}#repospage section .search input[type=text]:focus,#repospage section .search input[type=search]:focus{border-color:#129FEA}#repospage section .repo{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;text-decoration:none}#repospage section .repo:last-child>div{border-bottom:1px solid #fff}#repospage section .repo>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border:1px solid #eee;border-bottom:1px solid #fff;-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear}#repospage section .repo>div>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px 25px}#repospage section .repo>div>div:last-child div{text-align:right}#repospage section .repo>div:hover{border:1px solid #262626}#repospage section .repo h4{font-size:20px;margin-bottom:2px;color:#262626}#repospage section .repo span{color:#666;font-size:14px}#repospage section .repo i{color:#DDD;font-size:22px;margin-left:20px;margin-top:15px}#repospage section .repo i.fa-check-circle-o{color:#68c598}#userspage{width:100%}#userspage section{border-bottom:1px solid #eee;max-width:768px;margin:0 auto;margin-top:30px;margin-bottom:30px}#userspage section .search{margin-bottom:25px}#userspage section .search input[type=text],#userspage section .search input[type=search]{-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear;border:1px solid #ccc;border-radius:0;box-shadow:none;padding:12px}#userspage section .search input[type=text]:focus,#userspage section .search input[type=search]:focus{border-color:#129FEA}#userspage section .user{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;text-decoration:none}#userspage section .user:last-child>div{border-bottom:1px solid #fff}#userspage section .user>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border:1px solid #eee;border-bottom:1px solid #fff;-webkit-transition:.4s border linear;-moz-transition:.4s border linear;-ms-transition:.4s border linear;-o-transition:.4s border linear;transition:.4s border linear}#userspage section .user>div>div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px 25px;padding-right:0}#userspage section .user>div:hover{border:1px solid #262626}#userspage section .user img{border-radius:50%;width:48px;height:48px}#userspage section .user h4{font-size:20px;margin-bottom:2px;color:#262626}#userspage section .user h4 small{font-size:16px;color:#666;margin-left:5px}#userspage section .user span{color:#666;font-size:14px}#repoconfpage{width:100%}#repoconfpage section{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;border:1px solid #eee;max-width:768px;margin:0 auto;margin-top:30px;margin-bottom:30px;padding:20px}#repoconfpage section h2{font-size:16px;margin-bottom:15px}#repoconfpage section .markdown,#repoconfpage section .params,#repoconfpage section .key{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;min-height:50px;margin-top:10px;font-family:'Droid Sans Mono';border:1px solid #eee;padding:20px;width:100%;max-width:100%;color:#666}#repoconfpage section .markdown:focus,#repoconfpage section .params:focus,#repoconfpage section .key:focus{border-color:#129FEA;outline:0}#repoconfpage section .pure-button-primary{color:#FFF;background:#4ab1ce;padding:10px 20px;margin-top:20px}#accountpage{width:100%}#accountpage section{position:relative;max-width:768px;margin:0 auto;margin-top:30px;border:1px solid #eee}#accountpage section.profile>div:first-child{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px;text-align:center}#accountpage section.profile>div:last-child{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px}#accountpage section.profile .fullname{font-size:14px;margin-bottom:2px;color:#666;display:block}#accountpage section.profile .email{font-size:14px;color:#666;display:block}#accountpage section.token>div:first-child div{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;text-align:center;padding:20px;color:#666;font-size:16px;line-height:22px}#accountpage section.token>div:first-child i{margin-right:7px}#accountpage section.token>div:last-child{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;padding:20px;color:#666;line-height:22px;font-size:14px}#accountpage section h4{margin:10px 0;font-size:22px}#accountpage section h4 small{opacity:.6;font-size:16px;margin-left:10px}#accountpage section img{width:64px;height:64px;border-radius:50%}#accountpage section .notifications{position:absolute;top:0;right:0;margin:20px}#accountpage section .button-error{color:#FFF;background:#ca3c3c;padding:10px 20px;float:right}#accountpage section .pure-button-primary{color:#FFF;background:#4ab1ce;padding:10px 20px;margin-top:10px}#repopage{width:100%;background:#f2f5f8}#repopage section{padding:40px 0 20px;border-bottom:1px solid #EEE}#repopage section>div{max-width:1180px;margin:0 auto}#repopage section:nth-child(even){background:#FFF}#repopage section:first-child{background:#FFF}#repopage section .card[data-status=Success]:nth-child(2) .l-box{border-color:#68c598}#repopage section .card[data-status=Success]:nth-child(2) .l-box:hover{border:1px solid #262626}#repopage section .card[data-status=Killed]:nth-child(2) .l-box,#repopage section .card[data-status=Failure]:nth-child(2) .l-box,#repopage section .card[data-status=Error]:nth-child(2) .l-box{border-color:#e97041}#repopage section .card[data-status=Killed]:nth-child(2) .l-box:hover,#repopage section .card[data-status=Failure]:nth-child(2) .l-box:hover,#repopage section .card[data-status=Error]:nth-child(2) .l-box:hover{border:1px solid #262626}#repopage section .card[data-status=Started] em:before,#repopage section .card[data-status=Pending] em:before{-webkit-animation:progress 1s linear infinite;-moz-animation:progress 1s linear infinite;animation:progress 1s linear infinite;position:absolute;content:' ';height:5px;top:-5px;left:0;right:0;margin:0;background:#fee172;background-image:-webkit-linear-gradient(-45deg,rgba(255,255,255,.55)25%,transparent 25%,transparent 50%,rgba(255,255,255,.55)50%,rgba(255,255,255,.55)75%,transparent 75%,transparent);background-image:linear-gradient(-45deg,rgba(255,255,255,.55)25%,transparent 25%,transparent 50%,rgba(255,255,255,.55)50%,rgba(255,255,255,.55)75%,transparent 75%,transparent);background-repeat:repeat-x;background-size:30px 30px}#repopage section .l-box:after{font-family:FontAwesome;content:"\f104";content:"\f0d9";position:absolute;right:-20px;width:20px;text-align:center;color:rgba(0,0,0,.1);font-size:22px}#repopage section .card:last-child .l-box:after{content:''}#repopage section.nobuilds,#repopage section.inactive{text-align:center;padding-bottom:50px}#repopage section.nobuilds h1,#repopage section.inactive h1{font-size:26px;color:#262626}#repopage section.nobuilds p,#repopage section.inactive p{font-size:16px;color:#666}#repopage section.nobuilds i,#repopage section.inactive i{font-size:32px;margin-top:20px;margin-bottom:20px}#repopage section.nobuilds i.fa-file-code-o,#repopage section.inactive i.fa-file-code-o{font-size:42px;margin-top:30px}#repopage section.nobuilds .pure-button-primary,#repopage section.inactive .pure-button-primary{font-size:14px;text-transform:uppercase;background:#4ab1ce;padding:10px 30px}@-webkit-keyframes progress{to{background-position:30px 0}}@-moz-keyframes progress{to{background-position:30px 0}}@keyframes progress{to{background-position:30px 0}}#sidebar{width:240px;min-width:240px;position:relative;display:block;z-index:5;padding:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box}#sidebar h1{font-size:28px;font-weight:300}#sidebar h2{font-size:22px;font-weight:300;margin-bottom:20px}#sidebar dl{padding-top:23px;border-top:1px solid #ddd;margin-top:5px}#sidebar dl:first-child{padding-top:0;border-top:none;margin-top:0}#sidebar dl dt{font-size:12px;color:#849299;text-transform:uppercase;padding:3px 0}#sidebar dl dd{font-size:14px;padding:3px 0 20px}#sidebar dl a{text-transform:none}#sidebar dl small{font-size:12px}#sidebar dl .large{font-size:18px;padding-bottom:5px}#sidebar dl .time{float:right;margin-left:8px}#sidebar dl .photo{margin-right:4px}#sidebar dl .negative{color:#e97041}#sidebar dl .photoline{display:inline-block;position:relative;top:-10px;font-weight:700}#sidebar dl .small{padding-bottom:5px;font-weight:700;font-size:12px}#sidebar .status{border:1px solid transparent;display:block;text-align:center;padding:5px 20px;border-radius:50px;text-transform:uppercase;margin:0 -5px 10px;font-weight:700}#sidebar .status:before{float:left;margin-left:-5px}#sidebar .status.status_ok{color:#68c598;border-color:#68c598}#sidebar .status.status_ok:before{content:"\f00c";font-family:FontAwesome}#sidebar .status.status_error{color:#e97041;border-color:#e97041}#sidebar .status.status_error:before{content:"!"}#sidebar .result{background:#4ab1ce;background:#68c598;color:#fff;margin:-30px -30px -6px;padding:30px;position:relative}#sidebar .result .status{color:#fff;background:rgba(255,255,255,.2)}#sidebar .result .status:before{content:"\f00c";font-family:FontAwesome}#sidebar .result dl dd{padding:7px 0}#sidebar .result dl dd strong{font-size:16px}#sidebar .result[data-result=Killed],#sidebar .result[data-result=Failure],#sidebar .result[data-result=Error]{background:#e97041}#sidebar .result[data-result=Killed] .status:before,#sidebar .result[data-result=Failure] .status:before,#sidebar .result[data-result=Error] .status:before{content:"!"}#sidebar .result[data-result=Pending],#sidebar .result[data-result=Started]{background:#4ab1ce}#main{flex-grow:2}#main.output{background:#525252}#main.output pre{margin:0 auto;padding:30px;color:#FFF;font-family:'Droid Sans Mono';font-size:13px;white-space:pre-wrap;overflow:hidden}.pure-form.pure-form-stacked label{font-size:14px;color:#666;margin-top:20px;margin-bottom:5px}.pure-form.pure-form-stacked label:first-child{margin-top:0}.pure-form.pure-form-stacked input[type=text],.pure-form.pure-form-stacked select{min-width:300px;box-shadow:none;padding:10px;border:1px solid #ccc;border-radius:0}.toggle{margin-bottom:10px}.toggle:nth-child(2){margin-top:40px}.toggle span{line-height:32px;vertical-align:middle;display:inline-block;margin-left:10px}.toggle input[type=checkbox]{max-height:0;max-width:0;opacity:0}.toggle input[type=checkbox]+label{display:inline-block;vertical-align:middle;position:relative;box-shadow:inset 0 0 0 1px #d5d5d5;text-indent:-5000px;height:30px;width:50px;border-radius:15px}.toggle input[type=checkbox]+label:before{content:"";position:absolute;display:block;height:30px;width:30px;top:0;left:0;border-radius:15px;background:rgba(19,191,17,0);-webkit-transition:.25s ease-in-out;-moz-transition:.25s ease-in-out;-ms-transition:.25s ease-in-out;-o-transition:.25s ease-in-out;transition:.25s ease-in-out}.toggle input[type=checkbox]+label:after{content:"";position:absolute;display:block;height:30px;width:30px;top:0;left:0;border-radius:15px;background:#fff;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2),0 2px 4px rgba(0,0,0,.2);-webkit-transition:.25s ease-in-out;-moz-transition:.25s ease-in-out;-ms-transition:.25s ease-in-out;-o-transition:.25s ease-in-out;transition:.25s ease-in-out}.toggle input[type=checkbox]:checked+label:before{width:50px;background:#4ab1ce}.toggle input[type=checkbox]:checked+label:after{left:20px;box-shadow:inset 0 0 0 1px #4ab1ce,0 2px 4px rgba(0,0,0,.2)}.toast{position:fixed;bottom:50px;left:20%;right:20%;background:#363636;border-radius:3px;z-index:999;color:#FFF;padding:15px 20px;font-size:14px;box-shadow:2px 2px 2px rgba(0,0,0,.2)}.toast a,.toast a:visited,.toast a:hover,.toast a:active{color:#FFF}.toast button{float:right;background:0 0;border:none;color:#EEFF41;text-transform:uppercase;margin-left:10px} \ No newline at end of file diff --git a/server/app/views/login.html b/server/app/views/login.html index f181f3fe2..8ed6996e2 100644 --- a/server/app/views/login.html +++ b/server/app/views/login.html @@ -1,28 +1,14 @@
-
- - GitHub + - - - - diff --git a/server/app/views/setup.html b/server/app/views/setup.html index 2a7ef2ad8..c2229bbb7 100644 --- a/server/app/views/setup.html +++ b/server/app/views/setup.html @@ -1,57 +1,105 @@ -

First Time Setup

+ -
- -
- - - +
+ + +
+
+
+

Register with {{ remote.type | remoteName }}

+
+
Homepage URL
+
{{ window.location.protocol }}//{{ window.location.host }}
+
+
+
Authorization callback URL
+
{{ window.location.protocol }}//{{ window.location.host }}/login/{{ remote.type }}
+
+
-
- - -
+
+
+
+
+
+ +
+ +
+
-
- - -
+
+ +
+ +
+
-
- +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + + Enable Self-Registration +
+ + +
+
-
-
\ No newline at end of file + +
diff --git a/server/database/config.go b/server/database/config.go index a04c5d520..7479dc8f2 100644 --- a/server/database/config.go +++ b/server/database/config.go @@ -1,5 +1,6 @@ package database +/* import ( "github.com/BurntSushi/toml" "github.com/drone/drone/shared/model" @@ -33,3 +34,4 @@ func NewConfigManager(filename string) ConfigManager { func (c *configManager) Find() *model.Config { return c.conf } +*/ diff --git a/server/database/remote.go b/server/database/remote.go index 371b2a0c8..713cf3c52 100644 --- a/server/database/remote.go +++ b/server/database/remote.go @@ -14,6 +14,9 @@ type RemoteManager interface { // FindHost finds the Remote by hostname. FindHost(name string) (*model.Remote, error) + // FindHost finds the Remote by type. + FindType(t string) (*model.Remote, error) + // List finds all registered Remotes of the system. List() ([]*model.Remote, error) @@ -40,10 +43,19 @@ WHERE remote_host=? LIMIT 1 ` +// SQL query to retrieve a Remote by remote login. +const findRemoteTypeQuery = ` +SELECT * +FROM remotes +WHERE remote_type=? +LIMIT 1 +` + // SQL query to retrieve a list of all Remotes. const listRemoteQuery = ` SELECT * FROM remotes +ORDER BY remote_type ` // SQL statement to delete a Remote by ID. @@ -69,6 +81,12 @@ func (db *remoteManager) FindHost(host string) (*model.Remote, error) { return &dst, err } +func (db *remoteManager) FindType(t string) (*model.Remote, error) { + dst := model.Remote{} + err := meddler.QueryRow(db, &dst, findRemoteTypeQuery, t) + return &dst, err +} + func (db *remoteManager) List() ([]*model.Remote, error) { var dst []*model.Remote err := meddler.QueryAll(db, &dst, listRemoteQuery) diff --git a/server/handler/commit.go b/server/handler/commit.go index d5fb2f617..e6159fd00 100644 --- a/server/handler/commit.go +++ b/server/handler/commit.go @@ -6,7 +6,7 @@ import ( "github.com/drone/drone/server/database" "github.com/drone/drone/server/session" - "github.com/drone/drone/server/worker" + "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/model" "github.com/gorilla/pat" ) @@ -16,10 +16,10 @@ type CommitHandler struct { repos database.RepoManager commits database.CommitManager sess session.Session - queue chan *worker.Request + queue chan *model.Request } -func NewCommitHandler(repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, queue chan *worker.Request) *CommitHandler { +func NewCommitHandler(repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, queue chan *model.Request) *CommitHandler { return &CommitHandler{perms, repos, commits, sess, queue} } @@ -160,7 +160,8 @@ func (h *CommitHandler) PostCommit(w http.ResponseWriter, r *http.Request) error // drop the items on the queue // drop the items on the queue go func() { - h.queue <- &worker.Request{ + h.queue <- &model.Request{ + Host: httputil.GetURL(r), Repo: repo, Commit: c, } diff --git a/server/handler/config.go b/server/handler/config.go deleted file mode 100644 index 7e904bd50..000000000 --- a/server/handler/config.go +++ /dev/null @@ -1,35 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - - "github.com/drone/drone/server/database" - "github.com/drone/drone/server/session" - "github.com/gorilla/pat" -) - -type ConfigHandler struct { - conf database.ConfigManager - sess session.Session -} - -func NewConfigHandler(conf database.ConfigManager, sess session.Session) *ConfigHandler { - return &ConfigHandler{conf, sess} -} - -// GetConfig gets the system configuration details. -// GET /api/config -func (h *ConfigHandler) GetConfig(w http.ResponseWriter, r *http.Request) error { - // get the user form the session - user := h.sess.User(r) - if user == nil || !user.Admin { - return notAuthorized{} - } - - return json.NewEncoder(w).Encode(h.conf.Find()) -} - -func (h *ConfigHandler) Register(r *pat.Router) { - r.Get("/v1/config", errorHandler(h.GetConfig)) -} diff --git a/server/handler/hook.go b/server/handler/hook.go index c88575dbd..7bc165b38 100644 --- a/server/handler/hook.go +++ b/server/handler/hook.go @@ -3,8 +3,9 @@ package handler import ( "net/http" + "github.com/drone/drone/plugin/remote" "github.com/drone/drone/server/database" - "github.com/drone/drone/server/worker" + "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/model" "github.com/gorilla/pat" ) @@ -13,12 +14,12 @@ type HookHandler struct { users database.UserManager repos database.RepoManager commits database.CommitManager - conf database.ConfigManager - queue chan *worker.Request + remotes database.RemoteManager + queue chan *model.Request } -func NewHookHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, conf database.ConfigManager, queue chan *worker.Request) *HookHandler { - return &HookHandler{users, repos, commits, conf, queue} +func NewHookHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, remotes database.RemoteManager, queue chan *model.Request) *HookHandler { + return &HookHandler{users, repos, commits, remotes, queue} } // PostHook receives a post-commit hook from GitHub, Bitbucket, etc @@ -26,14 +27,21 @@ func NewHookHandler(users database.UserManager, repos database.RepoManager, comm func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { host := r.FormValue(":host") - // get the remote system's client. - remote := h.conf.Find().GetRemote(host) - if remote == nil { + remoteServer, err := h.remotes.FindType(host) + if err != nil { + return notFound{err} + } + + remotePlugin, ok := remote.Lookup(remoteServer.Type) + if !ok { return notFound{} } + // get the remote system's client. + plugin := remotePlugin(remoteServer) + // parse the hook payload - hook, err := remote.GetHook(r) + hook, err := plugin.GetHook(r) if err != nil { return badRequest{err} } @@ -47,7 +55,7 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { } // fetch the repository from the database - repo, err := h.repos.FindName(remote.GetHost(), hook.Owner, hook.Repo) + repo, err := h.repos.FindName(plugin.GetHost(), hook.Owner, hook.Repo) if err != nil { return notFound{} } @@ -66,7 +74,7 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { } // featch the .drone.yml file from the database - client := remote.GetClient(user.Access, user.Secret) + client := plugin.GetClient(user.Access, user.Secret) yml, err := client.GetScript(hook) if err != nil { return badRequest{err} @@ -91,7 +99,8 @@ func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error { // drop the items on the queue go func() { - h.queue <- &worker.Request{ + h.queue <- &model.Request{ + Host: httputil.GetURL(r), Repo: repo, Commit: &c, } diff --git a/server/handler/login.go b/server/handler/login.go index 92c45aa5c..12ddfc11f 100644 --- a/server/handler/login.go +++ b/server/handler/login.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/drone/drone/plugin/remote" "github.com/drone/drone/server/database" "github.com/drone/drone/server/session" "github.com/drone/drone/shared/model" @@ -15,12 +16,13 @@ type LoginHandler struct { users database.UserManager repos database.RepoManager perms database.PermManager - conf database.ConfigManager - sess session.Session + //conf database.ConfigManager + sess session.Session + remotes database.RemoteManager } -func NewLoginHandler(users database.UserManager, repos database.RepoManager, perms database.PermManager, sess session.Session, conf database.ConfigManager) *LoginHandler { - return &LoginHandler{users, repos, perms, conf, sess} +func NewLoginHandler(users database.UserManager, repos database.RepoManager, perms database.PermManager, sess session.Session /*conf database.ConfigManager,*/, remotes database.RemoteManager) *LoginHandler { + return &LoginHandler{users, repos, perms /*conf,*/, sess, remotes} } // GetLogin gets the login to the 3rd party remote system. @@ -29,14 +31,21 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { host := r.FormValue(":host") redirect := "/" - // get the remote system's client. - remote := h.conf.Find().GetRemote(host) - if remote == nil { + remoteServer, err := h.remotes.FindType(host) + if err != nil { + return notFound{err} + } + + remotePlugin, ok := remote.Lookup(remoteServer.Type) + if !ok { return notFound{} } + // get the remote system's client. + plugin := remotePlugin(remoteServer) + // authenticate the user - login, err := remote.GetLogin(w, r) + login, err := plugin.GetLogin(w, r) if err != nil { return badRequest{err} } else if login == nil { @@ -51,12 +60,12 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { // if self-registration is disabled we should // return a notAuthorized error. the only exception // is if no users exist yet in the system we'll proceed. - if h.conf.Find().Registration == false && h.users.Exist() { + if remoteServer.Open == false && h.users.Exist() { return notAuthorized{} } // create the user account - u = model.NewUser(remote.GetName(), login.Login, login.Email) + u = model.NewUser(plugin.GetName(), login.Login, login.Email) u.Name = login.Name u.SetEmail(login.Email) @@ -102,7 +111,7 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { // its own package / sync utility. go func() { // list all repositories - client := remote.GetClient(u.Access, u.Secret) + client := plugin.GetClient(u.Access, u.Secret) repos, err := client.GetRepos("") if err != nil { log.Println("Error syncing user account, listing repositories", u.Login, err) @@ -111,7 +120,7 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { // insert all repositories for _, remoteRepo := range repos { - repo, _ := model.NewRepo(remote.GetName(), remoteRepo.Owner, remoteRepo.Name) + repo, _ := model.NewRepo(plugin.GetName(), remoteRepo.Owner, remoteRepo.Name) repo.Private = remoteRepo.Private repo.Host = remoteRepo.Host repo.CloneURL = remoteRepo.Clone @@ -131,13 +140,13 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { } log.Println("Successfully syced repo.", u.Login+"/"+remoteRepo.Name) + } - u.Synced = time.Now().Unix() - u.Syncing = false - if err := h.users.Update(u); err != nil { - log.Println("Error syncing user account, updating sync date", u.Login, err) - return - } + u.Synced = time.Now().UTC().Unix() + u.Syncing = false + if err := h.users.Update(u); err != nil { + log.Println("Error syncing user account, updating sync date", u.Login, err) + return } }() } diff --git a/server/handler/remote.go b/server/handler/remote.go index bc4b664d8..30cea09a4 100644 --- a/server/handler/remote.go +++ b/server/handler/remote.go @@ -38,6 +38,23 @@ func (h *RemoteHandler) GetRemotes(w http.ResponseWriter, r *http.Request) error return json.NewEncoder(w).Encode(remotes) } +// GetRemoteLogins gets all remote logins. +// GET /api/remotes/logins +func (h *RemoteHandler) GetRemoteLogins(w http.ResponseWriter, r *http.Request) error { + remotes, err := h.remotes.List() + if err != nil { + return internalServerError{err} + } + var logins []interface{} + for _, remote := range remotes { + logins = append(logins, struct { + Type string `json:"type"` + Host string `json:"host"` + }{remote.Type, remote.Host}) + } + return json.NewEncoder(w).Encode(&logins) +} + // PostRemote creates a new remote. // POST /api/remotes func (h *RemoteHandler) PostRemote(w http.ResponseWriter, r *http.Request) error { @@ -57,7 +74,6 @@ func (h *RemoteHandler) PostRemote(w http.ResponseWriter, r *http.Request) error if err := json.NewDecoder(r.Body).Decode(&in); err != nil { return badRequest{err} } - uri, err := url.Parse(in.URL) if err != nil { return badRequest{err} @@ -65,7 +81,8 @@ func (h *RemoteHandler) PostRemote(w http.ResponseWriter, r *http.Request) error in.Host = uri.Host // there is an edge case where, during installation, a user could attempt - // to add the same result multiple times. + // to add the same result multiple times. In this case we will delete + // the old remote prior to adding the new one. if remote, err := h.remotes.FindHost(in.Host); err == nil && h.users.Exist() { h.remotes.Delete(remote) } @@ -78,31 +95,50 @@ func (h *RemoteHandler) PostRemote(w http.ResponseWriter, r *http.Request) error return json.NewEncoder(w).Encode(&in) } -// DeleteRemote delete the remote. -// GET /api/remotes/:name -func (h *RemoteHandler) DeleteRemote(w http.ResponseWriter, r *http.Request) error { - host := r.FormValue(":host") - +// PutRemote updates an existing remote. +// PUT /api/remotes +func (h *RemoteHandler) PutRemote(w http.ResponseWriter, r *http.Request) error { // get the user form the session user := h.sess.User(r) if user == nil || !user.Admin { return notAuthorized{} } - // get the remote - remote, err := h.remotes.FindHost(host) + // unmarshal the remote from the payload + defer r.Body.Close() + in := model.Remote{} + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + return badRequest{err} + } + uri, err := url.Parse(in.URL) + if err != nil { + return badRequest{err} + } + in.Host = uri.Host + + // retrieve the remote and return an error if not exists + remote, err := h.remotes.FindHost(in.Host) if err != nil { return notFound{err} } - if err := h.remotes.Delete(remote); err != nil { + + // update the remote details + remote.API = in.API + remote.URL = in.URL + remote.Host = in.Host + remote.Client = in.Client + remote.Secret = in.Secret + + // insert the remote in the database + if err := h.remotes.Update(remote); err != nil { return internalServerError{err} } - w.WriteHeader(http.StatusNoContent) - return nil + return json.NewEncoder(w).Encode(remote) } func (h *RemoteHandler) Register(r *pat.Router) { - r.Delete("/v1/remotes/:name", errorHandler(h.DeleteRemote)) - r.Post("/v1/remotes", errorHandler(h.PostRemote)) + r.Get("/v1/logins", errorHandler(h.GetRemoteLogins)) r.Get("/v1/remotes", errorHandler(h.GetRemotes)) + r.Post("/v1/remotes", errorHandler(h.PostRemote)) + r.Put("/v1/remotes", errorHandler(h.PutRemote)) } diff --git a/server/handler/repo.go b/server/handler/repo.go index 33f625add..c53ad6862 100644 --- a/server/handler/repo.go +++ b/server/handler/repo.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/drone/drone/plugin/remote" "github.com/drone/drone/server/database" "github.com/drone/drone/server/session" "github.com/drone/drone/shared/httputil" @@ -14,7 +15,7 @@ import ( ) type RepoHandler struct { - conf database.ConfigManager + remotes database.RemoteManager commits database.CommitManager perms database.PermManager repos database.RepoManager @@ -22,8 +23,8 @@ type RepoHandler struct { } func NewRepoHandler(repos database.RepoManager, commits database.CommitManager, - perms database.PermManager, sess session.Session, conf database.ConfigManager) *RepoHandler { - return &RepoHandler{conf, commits, perms, repos, sess} + perms database.PermManager, sess session.Session, remotes database.RemoteManager) *RepoHandler { + return &RepoHandler{remotes, commits, perms, repos, sess} } // GetRepo gets the named repository. @@ -105,16 +106,24 @@ func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error { repo.PrivateKey = sshutil.MarshalPrivateKey(key) // get the remote and client - remote := h.conf.Find().GetRemote(host) - if remote == nil { + remoteServer, err := h.remotes.FindType(repo.Remote) + if err != nil { + return notFound{err} + } + + remotePlugin, ok := remote.Lookup(remoteServer.Type) + if !ok { return notFound{} } + // get the remote system's client. + plugin := remotePlugin(remoteServer) + // post commit hook url - hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), remote.GetName()) + hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), plugin.GetName()) // activate the repository in the remote system - client := remote.GetClient(user.Access, user.Secret) + client := plugin.GetClient(user.Access, user.Secret) if err := client.SetActive(owner, name, hook, repo.PublicKey); err != nil { return badRequest{err} } diff --git a/server/handler/ws.go b/server/handler/ws.go index f6c7e3687..8086d3ae1 100644 --- a/server/handler/ws.go +++ b/server/handler/ws.go @@ -9,7 +9,6 @@ import ( "github.com/drone/drone/server/database" "github.com/drone/drone/server/pubsub" "github.com/drone/drone/server/session" - "github.com/drone/drone/server/worker" "github.com/drone/drone/shared/model" "github.com/gorilla/pat" @@ -72,7 +71,7 @@ func (h *WsHandler) WsUser(w http.ResponseWriter, r *http.Request) error { for { select { case msg := <-sub.Read(): - work, ok := msg.(*worker.Request) + work, ok := msg.(*model.Request) if !ok { break } @@ -203,7 +202,7 @@ func readWebsocket(ws *websocket.Conn) { // will be removed prior to release func (h *WsHandler) Ping(w http.ResponseWriter, r *http.Request) error { channel := h.pubsub.Register("_global") - msg := worker.Request{ + msg := model.Request{ Repo: &model.Repo{ID: 1, Private: false, Host: "github.com", Owner: "drone", Name: "drone"}, Commit: &model.Commit{ID: 1, Status: "Started", Branch: "master", Sha: "113f4917ff9174945388d86395f902cd154074cb", Message: "Remove branches by SCM hook", Author: "bradrydzewski", Gravatar: "8c58a0be77ee441bb8f8595b7f1b4e87"}, } diff --git a/server/main.go b/server/main.go index 76811aae0..a8ff57616 100644 --- a/server/main.go +++ b/server/main.go @@ -25,6 +25,10 @@ import ( "github.com/GeertJohan/go.rice" _ "github.com/mattn/go-sqlite3" "github.com/russross/meddler" + + _ "github.com/drone/drone/plugin/remote/bitbucket" + _ "github.com/drone/drone/plugin/remote/github" + _ "github.com/drone/drone/plugin/remote/gitlab" ) var ( @@ -79,7 +83,7 @@ func main() { commits := database.NewCommitManager(db) servers := database.NewServerManager(db) remotes := database.NewRemoteManager(db) - configs := database.NewConfigManager(filepath.Join(home, "config.toml")) + //configs := database.NewConfigManager(filepath.Join(home, "config.toml")) // message broker pubsub := pubsub.NewPubSub() @@ -87,10 +91,10 @@ func main() { // cancel all previously running builds go commits.CancelAll() - queue := make(chan *worker.Request) - workers := make(chan chan *worker.Request) + queue := make(chan *model.Request) + workers := make(chan chan *model.Request) worker.NewDispatch(queue, workers).Start() - worker.NewWorker(workers, users, repos, commits, configs, pubsub, &model.Server{}).Start() + worker.NewWorker(workers, users, repos, commits, pubsub, &model.Server{}).Start() // setup the session managers sess := session.NewSession(users) @@ -99,13 +103,13 @@ func main() { router := pat.New() handler.NewUsersHandler(users, sess).Register(router) handler.NewUserHandler(users, repos, commits, sess).Register(router) - handler.NewHookHandler(users, repos, commits, configs, queue).Register(router) - handler.NewLoginHandler(users, repos, perms, sess, configs).Register(router) + handler.NewHookHandler(users, repos, commits, remotes, queue).Register(router) + handler.NewLoginHandler(users, repos, perms, sess, remotes).Register(router) handler.NewCommitHandler(repos, commits, perms, sess, queue).Register(router) handler.NewBranchHandler(repos, commits, perms, sess).Register(router) - handler.NewRepoHandler(repos, commits, perms, sess, configs).Register(router) + handler.NewRepoHandler(repos, commits, perms, sess, remotes).Register(router) handler.NewBadgeHandler(repos, commits).Register(router) - handler.NewConfigHandler(configs, sess).Register(router) + //handler.NewConfigHandler(configs, sess).Register(router) handler.NewServerHandler(servers, sess).Register(router) handler.NewRemoteHandler(users, remotes, sess).Register(router) handler.NewWsHandler(repos, commits, perms, sess, pubsub).Register(router) diff --git a/server/worker/dispatch.go b/server/worker/dispatch.go index 76a1aeac8..614dcb6e7 100644 --- a/server/worker/dispatch.go +++ b/server/worker/dispatch.go @@ -1,14 +1,18 @@ package worker +import ( + "github.com/drone/drone/shared/model" +) + // http://nesv.github.io/golang/2014/02/25/worker-queues-in-go.html type Dispatch struct { - requests chan *Request - workers chan chan *Request + requests chan *model.Request + workers chan chan *model.Request quit chan bool } -func NewDispatch(requests chan *Request, workers chan chan *Request) *Dispatch { +func NewDispatch(requests chan *model.Request, workers chan chan *model.Request) *Dispatch { return &Dispatch{ requests: requests, workers: workers, diff --git a/server/worker/request.go b/server/worker/request.go deleted file mode 100644 index b9eb44305..000000000 --- a/server/worker/request.go +++ /dev/null @@ -1,12 +0,0 @@ -package worker - -import ( - "github.com/drone/drone/shared/model" -) - -type Request struct { - User *model.User `json:"-"` - Repo *model.Repo `json:"repo"` - Commit *model.Commit `json:"commit"` - server *model.Server -} diff --git a/server/worker/worker.go b/server/worker/worker.go index 50f7ea401..5b1aafd34 100644 --- a/server/worker/worker.go +++ b/server/worker/worker.go @@ -24,25 +24,25 @@ type worker struct { users database.UserManager repos database.RepoManager commits database.CommitManager - config database.ConfigManager - pubsub *pubsub.PubSub - server *model.Server + //config database.ConfigManager + pubsub *pubsub.PubSub + server *model.Server - request chan *Request - dispatch chan chan *Request + request chan *model.Request + dispatch chan chan *model.Request quit chan bool } -func NewWorker(dispatch chan chan *Request, users database.UserManager, repos database.RepoManager, commits database.CommitManager, config database.ConfigManager, pubsub *pubsub.PubSub, server *model.Server) Worker { +func NewWorker(dispatch chan chan *model.Request, users database.UserManager, repos database.RepoManager, commits database.CommitManager /*config database.ConfigManager,*/, pubsub *pubsub.PubSub, server *model.Server) Worker { return &worker{ - users: users, - repos: repos, - commits: commits, - config: config, + users: users, + repos: repos, + commits: commits, + //config: config, pubsub: pubsub, server: server, dispatch: dispatch, - request: make(chan *Request), + request: make(chan *model.Request), quit: make(chan bool), } } @@ -59,7 +59,7 @@ func (w *worker) Start() { select { case r := <-w.request: // handle the request - r.server = w.server + r.Server = w.server w.Execute(r) case <-w.quit: @@ -78,7 +78,7 @@ func (w *worker) Stop() { // Execute executes the work Request, persists the // results to the database, and sends event messages // to the pubsub (for websocket updates on the website). -func (w *worker) Execute(r *Request) { +func (w *worker) Execute(r *model.Request) { // mark the build as Started and update the database r.Commit.Status = model.StatusStarted r.Commit.Started = time.Now().UTC().Unix() @@ -123,6 +123,11 @@ func (w *worker) Execute(r *Request) { dockerClient = docker.NewHost(w.server.Host) } + // send all "started" notifications + if script.Notifications != nil { + script.Notifications.Send(r) + } + // create an instance of the Docker builder builder := build.New(dockerClient) builder.Build = script @@ -162,5 +167,9 @@ func (w *worker) Execute(r *Request) { // todo(bradrydzewski) update github status API // todo(bradrydzewski) send email notifications - // todo(bradrydzewski) send other notifications + + // send all "finished" notifications + if script.Notifications != nil { + script.Notifications.Send(r) + } } diff --git a/shared/model/config.go b/shared/model/config.go deleted file mode 100644 index f9c21fb09..000000000 --- a/shared/model/config.go +++ /dev/null @@ -1,94 +0,0 @@ -package model - -import ( - "github.com/drone/drone/plugin/remote" - "github.com/drone/drone/plugin/remote/bitbucket" - "github.com/drone/drone/plugin/remote/github" - "github.com/drone/drone/plugin/remote/gitlab" - "github.com/drone/drone/plugin/remote/stash" - "github.com/drone/drone/plugin/smtp" -) - -type Config struct { - // Hostname of the server, eg drone.io - //Host string `json:"host"` - - // Scheme of the server, eg https - //Scheme string `json:"scheme"` - - // Registration with a value of True allows developers - // to register themselves. If false, must be approved - // or invited by the system administrator. - Registration bool `json:"registration"` - - // SMTP stores configuration details for connecting with - // and smtp server to send email notifications. - SMTP *smtp.SMTP `json:"smtp"` - - // Bitbucket stores configuration details for communicating - // with the bitbucket.org public cloud service. - Bitbucket *bitbucket.Bitbucket `json:"bitbucket"` - - // Github stores configuration details for communicating - // with the github.com public cloud service. - Github *github.Github `json:"github"` - - // GithubEnterprise stores configuration details for - // communicating with a private Github installation. - GithubEnterprise *github.Github `json:"githubEnterprise"` - - // Gitlab stores configuration details for communicating - // with a private gitlab installation. - Gitlab *gitlab.Gitlab `json:"gitlab"` - - // Stash stores configuration details for communicating - // with a private Atlassian Stash installation. - Stash *stash.Stash `json:"stash"` -} - -// GetRemote is a helper function that will return the -// remote plugin name based on the specified hostname. -func (c *Config) GetRemote(name string) remote.Remote { - // first attempt to get the remote instance - // by the unique plugin name (ie enterprise.github.com) - switch name { - case c.Github.GetName(): - return c.Github - case c.Bitbucket.GetName(): - return c.Bitbucket - case c.GithubEnterprise.GetName(): - return c.GithubEnterprise - case c.Gitlab.GetName(): - return c.Gitlab - case c.Stash.GetName(): - return c.Stash - } - - // else attempt to get the remote instance - // by the hostname (ie github.drone.io) - switch { - case c.Github.IsMatch(name): - return c.Github - case c.Bitbucket.IsMatch(name): - return c.Bitbucket - case c.GithubEnterprise.IsMatch(name): - return c.GithubEnterprise - case c.Gitlab.IsMatch(name): - return c.Gitlab - case c.Stash.IsMatch(name): - return c.Stash - } - - // else none found - return nil -} - -// GetClient is a helper function taht will return the named -// remote plugin client, used to interact with the remote system. -func (c *Config) GetClient(name, access, secret string) remote.Client { - remote := c.GetRemote(name) - if remote == nil { - return nil - } - return remote.GetClient(access, secret) -} diff --git a/shared/model/request.go b/shared/model/request.go new file mode 100644 index 000000000..ca3ec3fbc --- /dev/null +++ b/shared/model/request.go @@ -0,0 +1,9 @@ +package model + +type Request struct { + Host string `json:"-"` + User *User `json:"-"` + Repo *Repo `json:"repo"` + Commit *Commit `json:"commit"` + Server *Server `json:"-"` +}