summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/ci/arch.jpgbin0 -> 25222 bytes
-rw-r--r--app/assets/images/ci/favicon.icobin0 -> 5430 bytes
-rw-r--r--app/assets/images/ci/loader.gifbin0 -> 4405 bytes
-rw-r--r--app/assets/images/ci/no_avatar.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/ci/rails.pngbin0 -> 6646 bytes
-rw-r--r--app/assets/images/ci/service_sample.pngbin0 -> 76024 bytes
-rw-r--r--app/assets/javascripts/ci/Chart.min.js39
-rw-r--r--app/assets/javascripts/ci/application.js.coffee50
-rw-r--r--app/assets/javascripts/ci/build.coffee41
-rw-r--r--app/assets/javascripts/ci/pager.js.coffee42
-rw-r--r--app/assets/javascripts/ci/projects.js.coffee6
-rw-r--r--app/assets/stylesheets/ci/application.scss46
-rw-r--r--app/assets/stylesheets/ci/generic/avatar.scss29
-rw-r--r--app/assets/stylesheets/ci/generic/buttons.scss7
-rw-r--r--app/assets/stylesheets/ci/generic/callout.scss45
-rw-r--r--app/assets/stylesheets/ci/generic/common.scss189
-rw-r--r--app/assets/stylesheets/ci/generic/forms.scss28
-rw-r--r--app/assets/stylesheets/ci/generic/tables.scss20
-rw-r--r--app/assets/stylesheets/ci/generic/typography.scss63
-rw-r--r--app/assets/stylesheets/ci/generic/xterm.scss904
-rw-r--r--app/assets/stylesheets/ci/main/fonts.scss2
-rw-r--r--app/assets/stylesheets/ci/main/layout.scss18
-rw-r--r--app/assets/stylesheets/ci/main/mixins.scss31
-rw-r--r--app/assets/stylesheets/ci/main/variables.scss44
-rw-r--r--app/assets/stylesheets/ci/sections/builds.scss54
-rw-r--r--app/assets/stylesheets/ci/sections/lint.scss8
-rw-r--r--app/assets/stylesheets/ci/sections/login.scss13
-rw-r--r--app/assets/stylesheets/ci/sections/navbar.scss54
-rw-r--r--app/assets/stylesheets/ci/sections/projects.scss61
-rw-r--r--app/assets/stylesheets/ci/sections/runners.scss34
-rw-r--r--app/assets/stylesheets/ci/sections/setup.scss11
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/ci/admin/application_controller.rb10
-rw-r--r--app/controllers/ci/admin/application_settings_controller.rb31
-rw-r--r--app/controllers/ci/admin/builds_controller.rb12
-rw-r--r--app/controllers/ci/admin/events_controller.rb9
-rw-r--r--app/controllers/ci/admin/projects_controller.rb19
-rw-r--r--app/controllers/ci/admin/runner_projects_controller.rb34
-rw-r--r--app/controllers/ci/admin/runners_controller.rb69
-rw-r--r--app/controllers/ci/application_controller.rb133
-rw-r--r--app/controllers/ci/builds_controller.rb77
-rw-r--r--app/controllers/ci/charts_controller.rb24
-rw-r--r--app/controllers/ci/commits_controller.rb37
-rw-r--r--app/controllers/ci/events_controller.rb21
-rw-r--r--app/controllers/ci/helps_controller.rb16
-rw-r--r--app/controllers/ci/lints_controller.rb26
-rw-r--r--app/controllers/ci/projects_controller.rb136
-rw-r--r--app/controllers/ci/runner_projects_controller.rb34
-rw-r--r--app/controllers/ci/runners_controller.rb71
-rw-r--r--app/controllers/ci/services_controller.rb59
-rw-r--r--app/controllers/ci/triggers_controller.rb43
-rw-r--r--app/controllers/ci/user_sessions_controller.rb65
-rw-r--r--app/controllers/ci/variables_controller.rb33
-rw-r--r--app/controllers/ci/web_hooks_controller.rb53
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb2
-rw-r--r--app/controllers/projects/network_controller.rb2
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/helpers/appearances_helper.rb21
-rw-r--r--app/helpers/application_helper.rb315
-rw-r--r--app/helpers/application_settings_helper.rb59
-rw-r--r--app/helpers/auth_helper.rb50
-rw-r--r--app/helpers/blob_helper.rb74
-rw-r--r--app/helpers/branches_helper.rb17
-rw-r--r--app/helpers/broadcast_messages_helper.rb16
-rw-r--r--app/helpers/ci/application_helper.rb140
-rw-r--r--app/helpers/ci/builds_helper.rb41
-rw-r--r--app/helpers/ci/commits_helper.rb26
-rw-r--r--app/helpers/ci/gitlab_helper.rb36
-rw-r--r--app/helpers/ci/icons_helper.rb11
-rw-r--r--app/helpers/ci/projects_helper.rb36
-rw-r--r--app/helpers/ci/routes_helper.rb29
-rw-r--r--app/helpers/ci/runners_helper.rb22
-rw-r--r--app/helpers/ci/triggers_helper.rb7
-rw-r--r--app/helpers/ci/user_helper.rb15
-rw-r--r--app/helpers/ci/user_sessions_helper.rb32
-rw-r--r--app/helpers/commits_helper.rb183
-rw-r--r--app/helpers/compare_helper.rb21
-rw-r--r--app/helpers/dashboard_helper.rb9
-rw-r--r--app/helpers/diff_helper.rb170
-rw-r--r--app/helpers/emails_helper.rb57
-rw-r--r--app/helpers/events_helper.rb203
-rw-r--r--app/helpers/explore_helper.rb17
-rw-r--r--app/helpers/external_wiki_helper.rb11
-rw-r--r--app/helpers/git_helper.rb5
-rw-r--r--app/helpers/gitlab/appearances_helper.rb23
-rw-r--r--app/helpers/gitlab/application_helper.rb317
-rw-r--r--app/helpers/gitlab/application_settings_helper.rb61
-rw-r--r--app/helpers/gitlab/auth_helper.rb52
-rw-r--r--app/helpers/gitlab/blob_helper.rb76
-rw-r--r--app/helpers/gitlab/branches_helper.rb19
-rw-r--r--app/helpers/gitlab/broadcast_messages_helper.rb18
-rw-r--r--app/helpers/gitlab/commits_helper.rb185
-rw-r--r--app/helpers/gitlab/compare_helper.rb23
-rw-r--r--app/helpers/gitlab/dashboard_helper.rb11
-rw-r--r--app/helpers/gitlab/diff_helper.rb172
-rw-r--r--app/helpers/gitlab/emails_helper.rb59
-rw-r--r--app/helpers/gitlab/events_helper.rb205
-rw-r--r--app/helpers/gitlab/explore_helper.rb19
-rw-r--r--app/helpers/gitlab/external_wiki_helper.rb13
-rw-r--r--app/helpers/gitlab/git_helper.rb7
-rw-r--r--app/helpers/gitlab/gitlab_markdown_helper.rb195
-rw-r--r--app/helpers/gitlab/gitlab_routing_helper.rb69
-rw-r--r--app/helpers/gitlab/graph_helper.rb18
-rw-r--r--app/helpers/gitlab/groups_helper.rb35
-rw-r--r--app/helpers/gitlab/icons_helper.rb87
-rw-r--r--app/helpers/gitlab/issues_helper.rb90
-rw-r--r--app/helpers/gitlab/labels_helper.rb103
-rw-r--r--app/helpers/gitlab/merge_requests_helper.rb76
-rw-r--r--app/helpers/gitlab/milestones_helper.rb38
-rw-r--r--app/helpers/gitlab/namespaces_helper.rb38
-rw-r--r--app/helpers/gitlab/nav_helper.rb23
-rw-r--r--app/helpers/gitlab/notes_helper.rb78
-rw-r--r--app/helpers/gitlab/notifications_helper.rb17
-rw-r--r--app/helpers/gitlab/page_layout_helper.rb28
-rw-r--r--app/helpers/gitlab/preferences_helper.rb67
-rw-r--r--app/helpers/gitlab/projects_helper.rb332
-rw-r--r--app/helpers/gitlab/search_helper.rb114
-rw-r--r--app/helpers/gitlab/selects_helper.rb47
-rw-r--r--app/helpers/gitlab/snippets_helper.rb22
-rw-r--r--app/helpers/gitlab/sorting_helper.rb98
-rw-r--r--app/helpers/gitlab/submodule_helper.rb76
-rw-r--r--app/helpers/gitlab/tab_helper.rb133
-rw-r--r--app/helpers/gitlab/tags_helper.rb16
-rw-r--r--app/helpers/gitlab/tree_helper.rb89
-rw-r--r--app/helpers/gitlab/version_check_helper.rb9
-rw-r--r--app/helpers/gitlab/visibility_level_helper.rb97
-rw-r--r--app/helpers/gitlab/wiki_helper.rb26
-rw-r--r--app/helpers/gitlab_markdown_helper.rb193
-rw-r--r--app/helpers/gitlab_routing_helper.rb67
-rw-r--r--app/helpers/graph_helper.rb16
-rw-r--r--app/helpers/groups_helper.rb33
-rw-r--r--app/helpers/icons_helper.rb85
-rw-r--r--app/helpers/issues_helper.rb88
-rw-r--r--app/helpers/labels_helper.rb101
-rw-r--r--app/helpers/merge_requests_helper.rb74
-rw-r--r--app/helpers/milestones_helper.rb36
-rw-r--r--app/helpers/namespaces_helper.rb36
-rw-r--r--app/helpers/nav_helper.rb21
-rw-r--r--app/helpers/notes_helper.rb76
-rw-r--r--app/helpers/notifications_helper.rb15
-rw-r--r--app/helpers/page_layout_helper.rb26
-rw-r--r--app/helpers/preferences_helper.rb65
-rw-r--r--app/helpers/projects_helper.rb330
-rw-r--r--app/helpers/search_helper.rb112
-rw-r--r--app/helpers/selects_helper.rb45
-rw-r--r--app/helpers/snippets_helper.rb20
-rw-r--r--app/helpers/sorting_helper.rb96
-rw-r--r--app/helpers/submodule_helper.rb74
-rw-r--r--app/helpers/tab_helper.rb131
-rw-r--r--app/helpers/tags_helper.rb14
-rw-r--r--app/helpers/tree_helper.rb88
-rw-r--r--app/helpers/version_check_helper.rb7
-rw-r--r--app/helpers/visibility_level_helper.rb95
-rw-r--r--app/helpers/wiki_helper.rb24
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/mailers/ci/emails/builds.rb17
-rw-r--r--app/mailers/ci/notify.rb47
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/ci/application_setting.rb27
-rw-r--r--app/models/ci/build.rb285
-rw-r--r--app/models/ci/commit.rb267
-rw-r--r--app/models/ci/event.rb27
-rw-r--r--app/models/ci/network.rb122
-rw-r--r--app/models/ci/project.rb221
-rw-r--r--app/models/ci/project_status.rb47
-rw-r--r--app/models/ci/runner.rb80
-rw-r--r--app/models/ci/runner_project.rb21
-rw-r--r--app/models/ci/service.rb105
-rw-r--r--app/models/ci/trigger.rb39
-rw-r--r--app/models/ci/trigger_request.rb23
-rw-r--r--app/models/ci/user.rb97
-rw-r--r--app/models/ci/user_session.rb23
-rw-r--r--app/models/ci/variable.rb25
-rw-r--r--app/models/ci/web_hook.rb44
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_services/ci/hip_chat_message.rb78
-rw-r--r--app/models/project_services/ci/hip_chat_service.rb93
-rw-r--r--app/models/project_services/ci/mail_service.rb84
-rw-r--r--app/models/project_services/ci/slack_message.rb97
-rw-r--r--app/models/project_services/ci/slack_service.rb81
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/services/ci/create_commit_service.rb50
-rw-r--r--app/services/ci/create_project_service.rb35
-rw-r--r--app/services/ci/create_trigger_request_service.rb17
-rw-r--r--app/services/ci/event_service.rb31
-rw-r--r--app/services/ci/image_for_build_service.rb31
-rw-r--r--app/services/ci/register_build_service.rb40
-rw-r--r--app/services/ci/test_hook_service.rb7
-rw-r--r--app/services/ci/web_hook_service.rb36
-rw-r--r--app/views/ci/admin/application_settings/_form.html.haml24
-rw-r--r--app/views/ci/admin/application_settings/show.html.haml3
-rw-r--r--app/views/ci/admin/builds/_build.html.haml32
-rw-r--r--app/views/ci/admin/builds/index.html.haml27
-rw-r--r--app/views/ci/admin/events/index.html.haml17
-rw-r--r--app/views/ci/admin/projects/_project.html.haml28
-rw-r--r--app/views/ci/admin/projects/index.html.haml14
-rw-r--r--app/views/ci/admin/runner_projects/index.html.haml57
-rw-r--r--app/views/ci/admin/runners/_runner.html.haml48
-rw-r--r--app/views/ci/admin/runners/index.html.haml51
-rw-r--r--app/views/ci/admin/runners/show.html.haml118
-rw-r--r--app/views/ci/admin/runners/update.js.haml2
-rw-r--r--app/views/ci/builds/_build.html.haml45
-rw-r--r--app/views/ci/builds/show.html.haml176
-rw-r--r--app/views/ci/charts/_build_times.haml21
-rw-r--r--app/views/ci/charts/_builds.haml41
-rw-r--r--app/views/ci/charts/_overall.haml21
-rw-r--r--app/views/ci/charts/show.html.haml4
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/ci/commits/show.html.haml96
-rw-r--r--app/views/ci/errors/show.haml2
-rw-r--r--app/views/ci/events/index.html.haml19
-rw-r--r--app/views/ci/helps/oauth2.html.haml20
-rw-r--r--app/views/ci/helps/show.html.haml40
-rw-r--r--app/views/ci/kaminari/_first_page.html.haml2
-rw-r--r--app/views/ci/kaminari/_gap.html.haml2
-rw-r--r--app/views/ci/kaminari/_last_page.html.haml2
-rw-r--r--app/views/ci/kaminari/_next_page.html.haml2
-rw-r--r--app/views/ci/kaminari/_page.html.haml2
-rw-r--r--app/views/ci/kaminari/_paginator.html.haml11
-rw-r--r--app/views/ci/kaminari/_prev_page.html.haml2
-rw-r--r--app/views/ci/lints/_create.html.haml39
-rw-r--r--app/views/ci/lints/create.js.haml2
-rw-r--r--app/views/ci/lints/show.html.haml25
-rw-r--r--app/views/ci/notify/build_fail_email.html.haml19
-rw-r--r--app/views/ci/notify/build_fail_email.text.erb9
-rw-r--r--app/views/ci/notify/build_success_email.html.haml20
-rw-r--r--app/views/ci/notify/build_success_email.text.erb9
-rw-r--r--app/views/ci/projects/_form.html.haml101
-rw-r--r--app/views/ci/projects/_gl_projects.html.haml15
-rw-r--r--app/views/ci/projects/_info.html.haml2
-rw-r--r--app/views/ci/projects/_no_runners.html.haml8
-rw-r--r--app/views/ci/projects/_project.html.haml22
-rw-r--r--app/views/ci/projects/_public.html.haml21
-rw-r--r--app/views/ci/projects/_search.html.haml18
-rw-r--r--app/views/ci/projects/edit.html.haml21
-rw-r--r--app/views/ci/projects/gitlab.html.haml35
-rw-r--r--app/views/ci/projects/index.html.haml22
-rw-r--r--app/views/ci/projects/show.html.haml59
-rw-r--r--app/views/ci/runners/_runner.html.haml35
-rw-r--r--app/views/ci/runners/_shared_runners.html.haml23
-rw-r--r--app/views/ci/runners/_specific_runners.html.haml29
-rw-r--r--app/views/ci/runners/edit.html.haml27
-rw-r--r--app/views/ci/runners/index.html.haml25
-rw-r--r--app/views/ci/runners/show.html.haml64
-rw-r--r--app/views/ci/services/_form.html.haml57
-rw-r--r--app/views/ci/services/edit.html.haml1
-rw-r--r--app/views/ci/services/index.html.haml22
-rw-r--r--app/views/ci/shared/_guide.html.haml15
-rw-r--r--app/views/ci/shared/_no_runners.html.haml7
-rw-r--r--app/views/ci/triggers/_trigger.html.haml14
-rw-r--r--app/views/ci/triggers/index.html.haml67
-rw-r--r--app/views/ci/user_sessions/new.html.haml8
-rw-r--r--app/views/ci/user_sessions/show.html.haml15
-rw-r--r--app/views/ci/variables/show.html.haml37
-rw-r--r--app/views/ci/web_hooks/index.html.haml92
-rw-r--r--app/views/layouts/ci/_head.html.haml11
-rw-r--r--app/views/layouts/ci/_info.html.haml9
-rw-r--r--app/views/layouts/ci/_nav.html.haml32
-rw-r--r--app/views/layouts/ci/_nav_admin.html.haml28
-rw-r--r--app/views/layouts/ci/_nav_project.html.haml40
-rw-r--r--app/views/layouts/ci/admin.html.haml17
-rw-r--r--app/views/layouts/ci/application.html.haml13
-rw-r--r--app/views/layouts/ci/empty.html.haml13
-rw-r--r--app/views/layouts/ci/notify.html.haml19
-rw-r--r--app/views/layouts/ci/project.html.haml26
-rw-r--r--app/workers/ci/hip_chat_notifier_worker.rb19
-rw-r--r--app/workers/ci/slack_notifier_worker.rb10
-rw-r--r--app/workers/ci/web_hook_worker.rb9
272 files changed, 10936 insertions, 3215 deletions
diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg
new file mode 100644
index 00000000000..0e05674e840
--- /dev/null
+++ b/app/assets/images/ci/arch.jpg
Binary files differ
diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico
new file mode 100644
index 00000000000..9663d4d00b9
--- /dev/null
+++ b/app/assets/images/ci/favicon.ico
Binary files differ
diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif
new file mode 100644
index 00000000000..2fcb8f2da0d
--- /dev/null
+++ b/app/assets/images/ci/loader.gif
Binary files differ
diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png
new file mode 100644
index 00000000000..752d26adba7
--- /dev/null
+++ b/app/assets/images/ci/no_avatar.png
Binary files differ
diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png
new file mode 100644
index 00000000000..d5edc04e65f
--- /dev/null
+++ b/app/assets/images/ci/rails.png
Binary files differ
diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png
new file mode 100644
index 00000000000..65d29e3fd89
--- /dev/null
+++ b/app/assets/images/ci/service_sample.png
Binary files differ
diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js
new file mode 100644
index 00000000000..ab635881087
--- /dev/null
+++ b/app/assets/javascripts/ci/Chart.min.js
@@ -0,0 +1,39 @@
+var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a=
+Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
+isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c?
+b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)?
+0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1==
+a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
+Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10*
+(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)*
+a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0,
+scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",
+animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",
+scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a,
+c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,
+onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,
+pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",
+scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]);
+d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
+m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
+c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE;
+h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
+a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
+(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
+1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath();
+b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
+v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
+c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
+(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
+0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;
+for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
+0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
+b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
+e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
+c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<
+h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=
+Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
+d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
+k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
+b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}}; \ No newline at end of file
diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee
new file mode 100644
index 00000000000..8a8aed1385c
--- /dev/null
+++ b/app/assets/javascripts/ci/application.js.coffee
@@ -0,0 +1,50 @@
+# This is a manifest file that'll be compiled into application.js, which will include all the files
+# listed below.
+#
+# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
+#
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
+# GO AFTER THE REQUIRES BELOW.
+#
+#= require jquery
+#= require bootstrap
+#= require jquery_ujs
+#= require turbolinks
+#= require jquery.turbolinks
+#= require jquery.endless-scroll
+#= require pager
+#= require nprogress
+#= require nprogress-turbolinks
+#= require jquery_nested_form
+#= require_tree .
+#
+#
+
+$(document).on 'click', '.edit-runner-link', (event) ->
+ event.preventDefault()
+
+ descr = $(this).closest('.runner-description').first()
+ descr.addClass('hide')
+ form = descr.next('.runner-description-form')
+ descrInput = form.find('input.description')
+ originalValue = descrInput.val()
+ form.removeClass('hide')
+ form.find('.cancel').on 'click', (event) ->
+ event.preventDefault()
+
+ form.addClass('hide')
+ descrInput.val(originalValue)
+ descr.removeClass('hide')
+
+$(document).on 'click', '.assign-all-runner', ->
+ $(this).replaceWith('<i class="fa-refresh fa-spin"></i> Assign in progress..')
+
+window.unbindEvents = ->
+ $(document).unbind('scroll')
+ $(document).off('scroll')
+
+document.addEventListener("page:fetch", unbindEvents)
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
new file mode 100644
index 00000000000..be4a3aa757a
--- /dev/null
+++ b/app/assets/javascripts/ci/build.coffee
@@ -0,0 +1,41 @@
+class CiBuild
+ @interval: null
+
+ constructor: (build_url, build_status) ->
+ clearInterval(CiBuild.interval)
+
+ if build_status == "running" || build_status == "pending"
+ #
+ # Bind autoscroll button to follow build output
+ #
+ $("#autoscroll-button").bind "click", ->
+ state = $(this).data("state")
+ if "enabled" is state
+ $(this).data "state", "disabled"
+ $(this).text "enable autoscroll"
+ else
+ $(this).data "state", "enabled"
+ $(this).text "disable autoscroll"
+
+ #
+ # Check for new build output if user still watching build page
+ # Only valid for runnig build when output changes during time
+ #
+ CiBuild.interval = setInterval =>
+ if window.location.href is build_url
+ $.ajax
+ url: build_url
+ dataType: "json"
+ success: (build) =>
+ if build.status == "running"
+ $('#build-trace code').html build.trace_html
+ $('#build-trace code').append '<i class="fa-refresh fa-spin"/>'
+ @checkAutoscroll()
+ else
+ Turbolinks.visit build_url
+ , 4000
+
+ checkAutoscroll: ->
+ $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
+
+@CiBuild = CiBuild
diff --git a/app/assets/javascripts/ci/pager.js.coffee b/app/assets/javascripts/ci/pager.js.coffee
new file mode 100644
index 00000000000..b57e7c736e9
--- /dev/null
+++ b/app/assets/javascripts/ci/pager.js.coffee
@@ -0,0 +1,42 @@
+@CiPager =
+ init: (@url, @limit = 0, preload, @disable = false) ->
+ if preload
+ @offset = 0
+ @getItems()
+ else
+ @offset = @limit
+ @initLoadMore()
+
+ getItems: ->
+ $(".loading").show()
+ $.ajax
+ type: "GET"
+ url: @url
+ data: "limit=" + @limit + "&offset=" + @offset
+ complete: =>
+ $(".loading").hide()
+ success: (data) =>
+ Pager.append(data.count, data.html)
+ dataType: "json"
+
+ append: (count, html) ->
+ if count > 1
+ $(".content-list").append html
+ if count == @limit
+ @offset += count
+ else
+ @disable = true
+
+ initLoadMore: ->
+ $(document).unbind('scroll')
+ $(document).endlessScroll
+ bottomPixels: 400
+ fireDelay: 1000
+ fireOnce: true
+ ceaseFire: ->
+ Pager.disable
+
+ callback: (i) =>
+ unless $(".loading").is(':visible')
+ $(".loading").show()
+ Pager.getItems()
diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee
new file mode 100644
index 00000000000..7e028b4e115
--- /dev/null
+++ b/app/assets/javascripts/ci/projects.js.coffee
@@ -0,0 +1,6 @@
+$(document).on 'click', '.badge-codes-toggle', ->
+ $('.badge-codes-block').toggleClass("hide")
+ return false
+
+$(document).on 'click', '.sync-now', ->
+ $(this).find('i').addClass('fa-spin')
diff --git a/app/assets/stylesheets/ci/application.scss b/app/assets/stylesheets/ci/application.scss
new file mode 100644
index 00000000000..ce080c7cf8a
--- /dev/null
+++ b/app/assets/stylesheets/ci/application.scss
@@ -0,0 +1,46 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
+ * compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require_self
+ */
+
+@import "main/variables.scss";
+@import "main/mixins.scss";
+@import "main/fonts.scss";
+@import "main/layout.scss";
+
+/**
+ * Twitter bootstrap
+ */
+@import 'bootstrap';
+
+/**
+ * Font icons
+ *
+ */
+@import "font-awesome";
+
+/**
+ * Generic css (forms, nav etc):
+ */
+@import "generic/*";
+
+/**
+ * Page specific styles (issues, projects etc):
+ */
+
+@import "sections/*";
+
+/*
+ * NProgress
+ */
+$nprogress-color: #9BC;
+@import 'nprogress';
+@import 'nprogress-bootstrap';
diff --git a/app/assets/stylesheets/ci/generic/avatar.scss b/app/assets/stylesheets/ci/generic/avatar.scss
new file mode 100644
index 00000000000..fc0914cddea
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/avatar.scss
@@ -0,0 +1,29 @@
+.avatar {
+ float: left;
+ margin-right: 12px;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ @include border-radius($avatar_radius);
+
+ &.avatar-inline {
+ float: none;
+ margin-left: 4px;
+ margin-bottom: 2px;
+
+ &.s16 { margin-right: 4px; }
+ &.s24 { margin-right: 4px; }
+ }
+
+ &.avatar-tile {
+ @include border-radius(0px);
+ }
+
+ &.s16 { width: 16px; height: 16px; margin-right: 6px; }
+ &.s24 { width: 24px; height: 24px; margin-right: 8px; }
+ &.s26 { width: 26px; height: 26px; margin-right: 8px; }
+ &.s32 { width: 32px; height: 32px; margin-right: 10px; }
+ &.s60 { width: 60px; height: 60px; margin-right: 12px; }
+ &.s90 { width: 90px; height: 90px; margin-right: 15px; }
+ &.s160 { width: 160px; height: 160px; margin-right: 20px; }
+}
diff --git a/app/assets/stylesheets/ci/generic/buttons.scss b/app/assets/stylesheets/ci/generic/buttons.scss
new file mode 100644
index 00000000000..5605c097c03
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/buttons.scss
@@ -0,0 +1,7 @@
+.btn {
+ @extend .btn-default;
+
+ &.btn-save {
+ @extend .btn-primary;
+ }
+}
diff --git a/app/assets/stylesheets/ci/generic/callout.scss b/app/assets/stylesheets/ci/generic/callout.scss
new file mode 100644
index 00000000000..f1699d21c9b
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/callout.scss
@@ -0,0 +1,45 @@
+/*
+ * Callouts from Bootstrap3 docs
+ *
+ * Not quite alerts, but custom and helpful notes for folks reading the docs.
+ * Requires a base and modifier class.
+ */
+
+/* Common styles for all types */
+.bs-callout {
+ margin: 20px 0;
+ padding: 20px;
+ border-left: 3px solid #eee;
+ color: #666;
+ background: #f9f9f9;
+}
+.bs-callout h4 {
+ margin-top: 0;
+ margin-bottom: 5px;
+}
+.bs-callout p:last-child {
+ margin-bottom: 0;
+}
+
+/* Variations */
+.bs-callout-danger {
+ background-color: #fdf7f7;
+ border-color: #eed3d7;
+ color: #b94a48;
+}
+.bs-callout-warning {
+ background-color: #faf8f0;
+ border-color: #faebcc;
+ color: #8a6d3b;
+}
+.bs-callout-info {
+ background-color: #f4f8fa;
+ border-color: #bce8f1;
+ color: #34789a;
+}
+.bs-callout-success {
+ background-color: #dff0d8;
+ border-color: #5cA64d;
+ color: #3c763d;
+}
+
diff --git a/app/assets/stylesheets/ci/generic/common.scss b/app/assets/stylesheets/ci/generic/common.scss
new file mode 100644
index 00000000000..58b7a93b0ad
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/common.scss
@@ -0,0 +1,189 @@
+/** COLORS **/
+.cgray { color: gray }
+.clgray { color: #BBB }
+.cred { color: #D12F19 }
+.cgreen { color: #4a2 }
+.cblue { color: #29A }
+.cblack { color: #111 }
+.cdark { color: #444 }
+.camber { color: #ffc000 }
+.cwhite { color: #fff!important }
+.bgred { background: #F2DEDE!important }
+
+/** COMMON CLASSES **/
+.prepend-top-10 { margin-top:10px }
+.prepend-top-20 { margin-top:20px }
+.prepend-left-10 { margin-left:10px }
+.prepend-left-20 { margin-left:20px }
+.append-right-10 { margin-right:10px }
+.append-right-20 { margin-right:20px }
+.append-bottom-10 { margin-bottom:10px }
+.append-bottom-15 { margin-bottom:15px }
+.append-bottom-20 { margin-bottom:20px }
+.inline { display: inline-block }
+.padded { padding:20px }
+.ipadded { padding:20px!important }
+.lborder { border-left:1px solid #eee }
+.underlined_link { text-decoration: underline; }
+.hint { font-style: italic; color: #999; }
+.light { color: #888 }
+.tiny { font-weight: normal }
+.vtop { vertical-align: top !important; }
+
+
+.dropdown-menu > li > a {
+ text-shadow: none;
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+ background: #29b;
+}
+
+.breadcrumb > li + li:before {
+ content: "/";
+ padding: 0;
+ color: #666;
+}
+
+.str-truncated {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+ white-space: nowrap;
+ max-width: 82%;
+}
+
+.page-title {
+ color: #444;
+ line-height: 1.5;
+ margin-top: 0px;
+ margin-bottom: 15px;
+}
+
+.slead {
+ margin-bottom: 18px;
+ font-size: 16px;
+ font-weight: normal;
+ line-height: 1.4;
+}
+
+.help-callout {
+ li {
+ font-size: 15px;
+ line-height: 1.6;
+ }
+}
+
+/** light list with border-bottom between li **/
+ul.bordered-list {
+ margin: 5px 0px;
+ padding: 0px;
+ li {
+ padding: 5px 0;
+ border-bottom: 1px solid #EEE;
+ overflow: hidden;
+ display: block;
+ margin: 0px;
+ &:last-child { border:none }
+ &.active {
+ background: #f9f9f9;
+ a { font-weight: bold; }
+ }
+ }
+
+ &.top-list {
+ li:first-child {
+ padding-top: 0;
+ h4, h5 {
+ margin-top: 0;
+ }
+ }
+ }
+}
+
+.underlined-title {
+ border-bottom: 1px solid #ccc;
+ padding: 0 0 3px 3px;
+}
+
+// Nav tabs
+.nav.nav-tabs {
+ li {
+ > a {
+ padding: 8px 20px;
+ margin-right: 7px;
+ line-height: 20px;
+ border-color: #EEE;
+ color: #888;
+ border-bottom: 1px solid #ddd;
+ .badge {
+ background-color: #eee;
+ color: #888;
+ text-shadow: 0 1px 1px #fff;
+ }
+ i[class^="fa-"] {
+ line-height: 14px;
+ }
+ }
+ &.active {
+ > a {
+ border-color: #CCC;
+ border-bottom: 1px solid #fff;
+ color: #333;
+ font-weight: bold;
+ }
+ }
+ }
+
+ &.nav-small-tabs > li > a {
+ padding: 6px 9px;
+ }
+}
+
+.nav-tabs > li > a,
+.nav-pills > li > a {
+ color: #666;
+}
+
+.nav-small > li > a {
+ padding: 3px 5px;
+ font-size: 12px;
+}
+
+
+
+// Breadcrumb
+ul.breadcrumb {
+ background: white;
+ border: none;
+ li {
+ display: inline;
+ text-shadow: 0 1px 0 white
+ }
+
+ a {
+ font-size: 16px;
+ }
+}
+
+/**
+ * fix to keep tooltips position in top navigation bar
+ *
+ */
+.navbar .nav > li {
+ position: relative;
+ white-space: nowrap;
+}
+
+// alerts
+.alert-disabled {
+ background-color: #e6e6e6;
+ border-color: #ebccd1;
+ color: #b0b0b0;
+}
+
+.label {
+ margin-right: 5px;
+ font-weight: normal;
+}
diff --git a/app/assets/stylesheets/ci/generic/forms.scss b/app/assets/stylesheets/ci/generic/forms.scss
new file mode 100644
index 00000000000..c8e4e8d6602
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/forms.scss
@@ -0,0 +1,28 @@
+input[type='text'].danger {
+ background: #F2DEDE!important;
+ border-color: #D66;
+ text-shadow: 0 1px 1px #fff
+}
+
+fieldset {
+ margin-bottom: 25px;
+}
+
+.form-actions {
+ padding: 17px 20px 18px;
+ margin-top: 18px;
+ margin-bottom: 18px;
+ background-color: whitesmoke;
+ border-top: 1px solid #e5e5e5;
+ padding-left: 17%;
+}
+
+label {
+ &.control-label {
+ @extend .col-sm-2;
+ }
+
+ &.inline-label {
+ margin: 0;
+ }
+}
diff --git a/app/assets/stylesheets/ci/generic/tables.scss b/app/assets/stylesheets/ci/generic/tables.scss
new file mode 100644
index 00000000000..71a7d4abaee
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/tables.scss
@@ -0,0 +1,20 @@
+table {
+ &.table {
+ tr {
+ td, th {
+ padding: 8px 10px;
+ line-height: 20px;
+ vertical-align: middle;
+ }
+ th {
+ font-weight: normal;
+ font-size: 15px;
+ border-bottom: 1px solid #CCC !important;
+ }
+ td {
+ border-color: #F1F1F1 !important;
+ border-bottom: 1px solid;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/ci/generic/typography.scss b/app/assets/stylesheets/ci/generic/typography.scss
new file mode 100644
index 00000000000..b9ed23b9d3a
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/typography.scss
@@ -0,0 +1,63 @@
+h6 {
+ color: #888;
+ text-transform: uppercase;
+}
+
+pre {
+ font-family: $monospace_font;
+
+ &.dark {
+ background: #333;
+ color: #f5f5f5;
+ }
+}
+
+/**
+ * Links
+ *
+ */
+a {
+ outline: none;
+ color: $link_color;
+ &:hover {
+ text-decoration: none;
+ color: $primary_color;
+ }
+
+ &:focus {
+ text-decoration: underline;
+ }
+
+ &.dark {
+ color: $style_color;
+ }
+
+ &.lined {
+ text-decoration: underline;
+ &:hover { text-decoration: underline; }
+ }
+
+ &.gray {
+ color: gray;
+ }
+
+ &.supp_diff_link {
+ text-align: center;
+ padding: 20px 0;
+ background: #f1f1f1;
+ width: 100%;
+ float: left;
+ }
+
+ &.neib {
+ margin-right: 15px;
+ }
+}
+
+a:focus {
+ outline: none;
+}
+
+.monospace {
+ font-family: $monospace_font;
+}
diff --git a/app/assets/stylesheets/ci/generic/xterm.scss b/app/assets/stylesheets/ci/generic/xterm.scss
new file mode 100644
index 00000000000..460a6bb2024
--- /dev/null
+++ b/app/assets/stylesheets/ci/generic/xterm.scss
@@ -0,0 +1,904 @@
+// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
+// see also: https://gist.github.com/jasonm23/2868981
+
+$black: #000000;
+$red: #cd0000;
+$green: #00cd00;
+$yellow: #cdcd00;
+$blue: #0000ee; // according to wikipedia, this is the xterm standard
+//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
+$magenta: #cd00cd;
+$cyan: #00cdcd;
+$white: #e5e5e5;
+$l-black: #7f7f7f;
+$l-red: #ff0000;
+$l-green: #00ff00;
+$l-yellow: #ffff00;
+$l-blue: #5c5cff;
+$l-magenta: #ff00ff;
+$l-cyan: #00ffff;
+$l-white: #ffffff;
+
+.term-bold {
+ font-weight: bold;
+}
+.term-italic {
+ font-style: italic;
+}
+.term-conceal {
+ visibility: hidden;
+}
+.term-underline {
+ text-decoration: underline;
+}
+.term-cross {
+ text-decoration: line-through;
+}
+
+.term-fg-black {
+ color: $black;
+}
+.term-fg-red {
+ color: $red;
+}
+.term-fg-green {
+ color: $green;
+}
+.term-fg-yellow {
+ color: $yellow;
+}
+.term-fg-blue {
+ color: $blue;
+}
+.term-fg-magenta {
+ color: $magenta;
+}
+.term-fg-cyan {
+ color: $cyan;
+}
+.term-fg-white {
+ color: $white;
+}
+.term-fg-l-black {
+ color: $l-black;
+}
+.term-fg-l-red {
+ color: $l-red;
+}
+.term-fg-l-green {
+ color: $l-green;
+}
+.term-fg-l-yellow {
+ color: $l-yellow;
+}
+.term-fg-l-blue {
+ color: $l-blue;
+}
+.term-fg-l-magenta {
+ color: $l-magenta;
+}
+.term-fg-l-cyan {
+ color: $l-cyan;
+}
+.term-fg-l-white {
+ color: $l-white;
+}
+
+.term-bg-black {
+ background-color: $black;
+}
+.term-bg-red {
+ background-color: $red;
+}
+.term-bg-green {
+ background-color: $green;
+}
+.term-bg-yellow {
+ background-color: $yellow;
+}
+.term-bg-blue {
+ background-color: $blue;
+}
+.term-bg-magenta {
+ background-color: $magenta;
+}
+.term-bg-cyan {
+ background-color: $cyan;
+}
+.term-bg-white {
+ background-color: $white;
+}
+.term-bg-l-black {
+ background-color: $l-black;
+}
+.term-bg-l-red {
+ background-color: $l-red;
+}
+.term-bg-l-green {
+ background-color: $l-green;
+}
+.term-bg-l-yellow {
+ background-color: $l-yellow;
+}
+.term-bg-l-blue {
+ background-color: $l-blue;
+}
+.term-bg-l-magenta {
+ background-color: $l-magenta;
+}
+.term-bg-l-cyan {
+ background-color: $l-cyan;
+}
+.term-bg-l-white {
+ background-color: $l-white;
+}
+
+
+.xterm-fg-0 {
+ color: #000000;
+}
+.xterm-fg-1 {
+ color: #800000;
+}
+.xterm-fg-2 {
+ color: #008000;
+}
+.xterm-fg-3 {
+ color: #808000;
+}
+.xterm-fg-4 {
+ color: #000080;
+}
+.xterm-fg-5 {
+ color: #800080;
+}
+.xterm-fg-6 {
+ color: #008080;
+}
+.xterm-fg-7 {
+ color: #c0c0c0;
+}
+.xterm-fg-8 {
+ color: #808080;
+}
+.xterm-fg-9 {
+ color: #ff0000;
+}
+.xterm-fg-10 {
+ color: #00ff00;
+}
+.xterm-fg-11 {
+ color: #ffff00;
+}
+.xterm-fg-12 {
+ color: #0000ff;
+}
+.xterm-fg-13 {
+ color: #ff00ff;
+}
+.xterm-fg-14 {
+ color: #00ffff;
+}
+.xterm-fg-15 {
+ color: #ffffff;
+}
+.xterm-fg-16 {
+ color: #000000;
+}
+.xterm-fg-17 {
+ color: #00005f;
+}
+.xterm-fg-18 {
+ color: #000087;
+}
+.xterm-fg-19 {
+ color: #0000af;
+}
+.xterm-fg-20 {
+ color: #0000d7;
+}
+.xterm-fg-21 {
+ color: #0000ff;
+}
+.xterm-fg-22 {
+ color: #005f00;
+}
+.xterm-fg-23 {
+ color: #005f5f;
+}
+.xterm-fg-24 {
+ color: #005f87;
+}
+.xterm-fg-25 {
+ color: #005faf;
+}
+.xterm-fg-26 {
+ color: #005fd7;
+}
+.xterm-fg-27 {
+ color: #005fff;
+}
+.xterm-fg-28 {
+ color: #008700;
+}
+.xterm-fg-29 {
+ color: #00875f;
+}
+.xterm-fg-30 {
+ color: #008787;
+}
+.xterm-fg-31 {
+ color: #0087af;
+}
+.xterm-fg-32 {
+ color: #0087d7;
+}
+.xterm-fg-33 {
+ color: #0087ff;
+}
+.xterm-fg-34 {
+ color: #00af00;
+}
+.xterm-fg-35 {
+ color: #00af5f;
+}
+.xterm-fg-36 {
+ color: #00af87;
+}
+.xterm-fg-37 {
+ color: #00afaf;
+}
+.xterm-fg-38 {
+ color: #00afd7;
+}
+.xterm-fg-39 {
+ color: #00afff;
+}
+.xterm-fg-40 {
+ color: #00d700;
+}
+.xterm-fg-41 {
+ color: #00d75f;
+}
+.xterm-fg-42 {
+ color: #00d787;
+}
+.xterm-fg-43 {
+ color: #00d7af;
+}
+.xterm-fg-44 {
+ color: #00d7d7;
+}
+.xterm-fg-45 {
+ color: #00d7ff;
+}
+.xterm-fg-46 {
+ color: #00ff00;
+}
+.xterm-fg-47 {
+ color: #00ff5f;
+}
+.xterm-fg-48 {
+ color: #00ff87;
+}
+.xterm-fg-49 {
+ color: #00ffaf;
+}
+.xterm-fg-50 {
+ color: #00ffd7;
+}
+.xterm-fg-51 {
+ color: #00ffff;
+}
+.xterm-fg-52 {
+ color: #5f0000;
+}
+.xterm-fg-53 {
+ color: #5f005f;
+}
+.xterm-fg-54 {
+ color: #5f0087;
+}
+.xterm-fg-55 {
+ color: #5f00af;
+}
+.xterm-fg-56 {
+ color: #5f00d7;
+}
+.xterm-fg-57 {
+ color: #5f00ff;
+}
+.xterm-fg-58 {
+ color: #5f5f00;
+}
+.xterm-fg-59 {
+ color: #5f5f5f;
+}
+.xterm-fg-60 {
+ color: #5f5f87;
+}
+.xterm-fg-61 {
+ color: #5f5faf;
+}
+.xterm-fg-62 {
+ color: #5f5fd7;
+}
+.xterm-fg-63 {
+ color: #5f5fff;
+}
+.xterm-fg-64 {
+ color: #5f8700;
+}
+.xterm-fg-65 {
+ color: #5f875f;
+}
+.xterm-fg-66 {
+ color: #5f8787;
+}
+.xterm-fg-67 {
+ color: #5f87af;
+}
+.xterm-fg-68 {
+ color: #5f87d7;
+}
+.xterm-fg-69 {
+ color: #5f87ff;
+}
+.xterm-fg-70 {
+ color: #5faf00;
+}
+.xterm-fg-71 {
+ color: #5faf5f;
+}
+.xterm-fg-72 {
+ color: #5faf87;
+}
+.xterm-fg-73 {
+ color: #5fafaf;
+}
+.xterm-fg-74 {
+ color: #5fafd7;
+}
+.xterm-fg-75 {
+ color: #5fafff;
+}
+.xterm-fg-76 {
+ color: #5fd700;
+}
+.xterm-fg-77 {
+ color: #5fd75f;
+}
+.xterm-fg-78 {
+ color: #5fd787;
+}
+.xterm-fg-79 {
+ color: #5fd7af;
+}
+.xterm-fg-80 {
+ color: #5fd7d7;
+}
+.xterm-fg-81 {
+ color: #5fd7ff;
+}
+.xterm-fg-82 {
+ color: #5fff00;
+}
+.xterm-fg-83 {
+ color: #5fff5f;
+}
+.xterm-fg-84 {
+ color: #5fff87;
+}
+.xterm-fg-85 {
+ color: #5fffaf;
+}
+.xterm-fg-86 {
+ color: #5fffd7;
+}
+.xterm-fg-87 {
+ color: #5fffff;
+}
+.xterm-fg-88 {
+ color: #870000;
+}
+.xterm-fg-89 {
+ color: #87005f;
+}
+.xterm-fg-90 {
+ color: #870087;
+}
+.xterm-fg-91 {
+ color: #8700af;
+}
+.xterm-fg-92 {
+ color: #8700d7;
+}
+.xterm-fg-93 {
+ color: #8700ff;
+}
+.xterm-fg-94 {
+ color: #875f00;
+}
+.xterm-fg-95 {
+ color: #875f5f;
+}
+.xterm-fg-96 {
+ color: #875f87;
+}
+.xterm-fg-97 {
+ color: #875faf;
+}
+.xterm-fg-98 {
+ color: #875fd7;
+}
+.xterm-fg-99 {
+ color: #875fff;
+}
+.xterm-fg-100 {
+ color: #878700;
+}
+.xterm-fg-101 {
+ color: #87875f;
+}
+.xterm-fg-102 {
+ color: #878787;
+}
+.xterm-fg-103 {
+ color: #8787af;
+}
+.xterm-fg-104 {
+ color: #8787d7;
+}
+.xterm-fg-105 {
+ color: #8787ff;
+}
+.xterm-fg-106 {
+ color: #87af00;
+}
+.xterm-fg-107 {
+ color: #87af5f;
+}
+.xterm-fg-108 {
+ color: #87af87;
+}
+.xterm-fg-109 {
+ color: #87afaf;
+}
+.xterm-fg-110 {
+ color: #87afd7;
+}
+.xterm-fg-111 {
+ color: #87afff;
+}
+.xterm-fg-112 {
+ color: #87d700;
+}
+.xterm-fg-113 {
+ color: #87d75f;
+}
+.xterm-fg-114 {
+ color: #87d787;
+}
+.xterm-fg-115 {
+ color: #87d7af;
+}
+.xterm-fg-116 {
+ color: #87d7d7;
+}
+.xterm-fg-117 {
+ color: #87d7ff;
+}
+.xterm-fg-118 {
+ color: #87ff00;
+}
+.xterm-fg-119 {
+ color: #87ff5f;
+}
+.xterm-fg-120 {
+ color: #87ff87;
+}
+.xterm-fg-121 {
+ color: #87ffaf;
+}
+.xterm-fg-122 {
+ color: #87ffd7;
+}
+.xterm-fg-123 {
+ color: #87ffff;
+}
+.xterm-fg-124 {
+ color: #af0000;
+}
+.xterm-fg-125 {
+ color: #af005f;
+}
+.xterm-fg-126 {
+ color: #af0087;
+}
+.xterm-fg-127 {
+ color: #af00af;
+}
+.xterm-fg-128 {
+ color: #af00d7;
+}
+.xterm-fg-129 {
+ color: #af00ff;
+}
+.xterm-fg-130 {
+ color: #af5f00;
+}
+.xterm-fg-131 {
+ color: #af5f5f;
+}
+.xterm-fg-132 {
+ color: #af5f87;
+}
+.xterm-fg-133 {
+ color: #af5faf;
+}
+.xterm-fg-134 {
+ color: #af5fd7;
+}
+.xterm-fg-135 {
+ color: #af5fff;
+}
+.xterm-fg-136 {
+ color: #af8700;
+}
+.xterm-fg-137 {
+ color: #af875f;
+}
+.xterm-fg-138 {
+ color: #af8787;
+}
+.xterm-fg-139 {
+ color: #af87af;
+}
+.xterm-fg-140 {
+ color: #af87d7;
+}
+.xterm-fg-141 {
+ color: #af87ff;
+}
+.xterm-fg-142 {
+ color: #afaf00;
+}
+.xterm-fg-143 {
+ color: #afaf5f;
+}
+.xterm-fg-144 {
+ color: #afaf87;
+}
+.xterm-fg-145 {
+ color: #afafaf;
+}
+.xterm-fg-146 {
+ color: #afafd7;
+}
+.xterm-fg-147 {
+ color: #afafff;
+}
+.xterm-fg-148 {
+ color: #afd700;
+}
+.xterm-fg-149 {
+ color: #afd75f;
+}
+.xterm-fg-150 {
+ color: #afd787;
+}
+.xterm-fg-151 {
+ color: #afd7af;
+}
+.xterm-fg-152 {
+ color: #afd7d7;
+}
+.xterm-fg-153 {
+ color: #afd7ff;
+}
+.xterm-fg-154 {
+ color: #afff00;
+}
+.xterm-fg-155 {
+ color: #afff5f;
+}
+.xterm-fg-156 {
+ color: #afff87;
+}
+.xterm-fg-157 {
+ color: #afffaf;
+}
+.xterm-fg-158 {
+ color: #afffd7;
+}
+.xterm-fg-159 {
+ color: #afffff;
+}
+.xterm-fg-160 {
+ color: #d70000;
+}
+.xterm-fg-161 {
+ color: #d7005f;
+}
+.xterm-fg-162 {
+ color: #d70087;
+}
+.xterm-fg-163 {
+ color: #d700af;
+}
+.xterm-fg-164 {
+ color: #d700d7;
+}
+.xterm-fg-165 {
+ color: #d700ff;
+}
+.xterm-fg-166 {
+ color: #d75f00;
+}
+.xterm-fg-167 {
+ color: #d75f5f;
+}
+.xterm-fg-168 {
+ color: #d75f87;
+}
+.xterm-fg-169 {
+ color: #d75faf;
+}
+.xterm-fg-170 {
+ color: #d75fd7;
+}
+.xterm-fg-171 {
+ color: #d75fff;
+}
+.xterm-fg-172 {
+ color: #d78700;
+}
+.xterm-fg-173 {
+ color: #d7875f;
+}
+.xterm-fg-174 {
+ color: #d78787;
+}
+.xterm-fg-175 {
+ color: #d787af;
+}
+.xterm-fg-176 {
+ color: #d787d7;
+}
+.xterm-fg-177 {
+ color: #d787ff;
+}
+.xterm-fg-178 {
+ color: #d7af00;
+}
+.xterm-fg-179 {
+ color: #d7af5f;
+}
+.xterm-fg-180 {
+ color: #d7af87;
+}
+.xterm-fg-181 {
+ color: #d7afaf;
+}
+.xterm-fg-182 {
+ color: #d7afd7;
+}
+.xterm-fg-183 {
+ color: #d7afff;
+}
+.xterm-fg-184 {
+ color: #d7d700;
+}
+.xterm-fg-185 {
+ color: #d7d75f;
+}
+.xterm-fg-186 {
+ color: #d7d787;
+}
+.xterm-fg-187 {
+ color: #d7d7af;
+}
+.xterm-fg-188 {
+ color: #d7d7d7;
+}
+.xterm-fg-189 {
+ color: #d7d7ff;
+}
+.xterm-fg-190 {
+ color: #d7ff00;
+}
+.xterm-fg-191 {
+ color: #d7ff5f;
+}
+.xterm-fg-192 {
+ color: #d7ff87;
+}
+.xterm-fg-193 {
+ color: #d7ffaf;
+}
+.xterm-fg-194 {
+ color: #d7ffd7;
+}
+.xterm-fg-195 {
+ color: #d7ffff;
+}
+.xterm-fg-196 {
+ color: #ff0000;
+}
+.xterm-fg-197 {
+ color: #ff005f;
+}
+.xterm-fg-198 {
+ color: #ff0087;
+}
+.xterm-fg-199 {
+ color: #ff00af;
+}
+.xterm-fg-200 {
+ color: #ff00d7;
+}
+.xterm-fg-201 {
+ color: #ff00ff;
+}
+.xterm-fg-202 {
+ color: #ff5f00;
+}
+.xterm-fg-203 {
+ color: #ff5f5f;
+}
+.xterm-fg-204 {
+ color: #ff5f87;
+}
+.xterm-fg-205 {
+ color: #ff5faf;
+}
+.xterm-fg-206 {
+ color: #ff5fd7;
+}
+.xterm-fg-207 {
+ color: #ff5fff;
+}
+.xterm-fg-208 {
+ color: #ff8700;
+}
+.xterm-fg-209 {
+ color: #ff875f;
+}
+.xterm-fg-210 {
+ color: #ff8787;
+}
+.xterm-fg-211 {
+ color: #ff87af;
+}
+.xterm-fg-212 {
+ color: #ff87d7;
+}
+.xterm-fg-213 {
+ color: #ff87ff;
+}
+.xterm-fg-214 {
+ color: #ffaf00;
+}
+.xterm-fg-215 {
+ color: #ffaf5f;
+}
+.xterm-fg-216 {
+ color: #ffaf87;
+}
+.xterm-fg-217 {
+ color: #ffafaf;
+}
+.xterm-fg-218 {
+ color: #ffafd7;
+}
+.xterm-fg-219 {
+ color: #ffafff;
+}
+.xterm-fg-220 {
+ color: #ffd700;
+}
+.xterm-fg-221 {
+ color: #ffd75f;
+}
+.xterm-fg-222 {
+ color: #ffd787;
+}
+.xterm-fg-223 {
+ color: #ffd7af;
+}
+.xterm-fg-224 {
+ color: #ffd7d7;
+}
+.xterm-fg-225 {
+ color: #ffd7ff;
+}
+.xterm-fg-226 {
+ color: #ffff00;
+}
+.xterm-fg-227 {
+ color: #ffff5f;
+}
+.xterm-fg-228 {
+ color: #ffff87;
+}
+.xterm-fg-229 {
+ color: #ffffaf;
+}
+.xterm-fg-230 {
+ color: #ffffd7;
+}
+.xterm-fg-231 {
+ color: #ffffff;
+}
+.xterm-fg-232 {
+ color: #080808;
+}
+.xterm-fg-233 {
+ color: #121212;
+}
+.xterm-fg-234 {
+ color: #1c1c1c;
+}
+.xterm-fg-235 {
+ color: #262626;
+}
+.xterm-fg-236 {
+ color: #303030;
+}
+.xterm-fg-237 {
+ color: #3a3a3a;
+}
+.xterm-fg-238 {
+ color: #444444;
+}
+.xterm-fg-239 {
+ color: #4e4e4e;
+}
+.xterm-fg-240 {
+ color: #585858;
+}
+.xterm-fg-241 {
+ color: #626262;
+}
+.xterm-fg-242 {
+ color: #6c6c6c;
+}
+.xterm-fg-243 {
+ color: #767676;
+}
+.xterm-fg-244 {
+ color: #808080;
+}
+.xterm-fg-245 {
+ color: #8a8a8a;
+}
+.xterm-fg-246 {
+ color: #949494;
+}
+.xterm-fg-247 {
+ color: #9e9e9e;
+}
+.xterm-fg-248 {
+ color: #a8a8a8;
+}
+.xterm-fg-249 {
+ color: #b2b2b2;
+}
+.xterm-fg-250 {
+ color: #bcbcbc;
+}
+.xterm-fg-251 {
+ color: #c6c6c6;
+}
+.xterm-fg-252 {
+ color: #d0d0d0;
+}
+.xterm-fg-253 {
+ color: #dadada;
+}
+.xterm-fg-254 {
+ color: #e4e4e4;
+}
+.xterm-fg-255 {
+ color: #eeeeee;
+}
diff --git a/app/assets/stylesheets/ci/main/fonts.scss b/app/assets/stylesheets/ci/main/fonts.scss
new file mode 100644
index 00000000000..8cc9986415c
--- /dev/null
+++ b/app/assets/stylesheets/ci/main/fonts.scss
@@ -0,0 +1,2 @@
+/** Typo **/
+$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace;
diff --git a/app/assets/stylesheets/ci/main/layout.scss b/app/assets/stylesheets/ci/main/layout.scss
new file mode 100644
index 00000000000..fa54481fa05
--- /dev/null
+++ b/app/assets/stylesheets/ci/main/layout.scss
@@ -0,0 +1,18 @@
+html {
+ overflow-y: scroll;
+
+ &.touch .tooltip { display: none !important; }
+}
+
+body {
+ margin-bottom: 20px;
+}
+
+.container {
+ padding-top: 0;
+ z-index: 5;
+}
+
+.container .content {
+ margin: 0 0;
+}
diff --git a/app/assets/stylesheets/ci/main/mixins.scss b/app/assets/stylesheets/ci/main/mixins.scss
new file mode 100644
index 00000000000..40040822331
--- /dev/null
+++ b/app/assets/stylesheets/ci/main/mixins.scss
@@ -0,0 +1,31 @@
+@mixin box-shadow($shadow) {
+ -webkit-box-shadow: $shadow;
+ -moz-box-shadow: $shadow;
+ -ms-box-shadow: $shadow;
+ -o-box-shadow: $shadow;
+ box-shadow: $shadow;
+}
+
+@mixin border-radius($radius) {
+ -webkit-border-radius: $radius;
+ -moz-border-radius: $radius;
+ -ms-border-radius: $radius;
+ -o-border-radius: $radius;
+ border-radius: $radius;
+}
+
+@mixin linear-gradient($from, $to) {
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
+ background-image: -webkit-linear-gradient($from, $to);
+ background-image: -moz-linear-gradient($from, $to);
+ background-image: -ms-linear-gradient($from, $to);
+ background-image: -o-linear-gradient($from, $to);
+}
+
+@mixin transition($transition) {
+ -webkit-transition: $transition;
+ -moz-transition: $transition;
+ -ms-transition: $transition;
+ -o-transition: $transition;
+ transition: $transition;
+}
diff --git a/app/assets/stylesheets/ci/main/variables.scss b/app/assets/stylesheets/ci/main/variables.scss
new file mode 100644
index 00000000000..a8c672a8057
--- /dev/null
+++ b/app/assets/stylesheets/ci/main/variables.scss
@@ -0,0 +1,44 @@
+/**
+ * General Colors
+ */
+$primary_color: #2FA0BB;
+$link_color: #3A89A3;
+$style_color: #246;
+$bg_style_color: #246;
+$hover: #D9EDF7;
+
+/*
+ * Success colors (green)
+ */
+$border_success: #019875;
+$bg_success: #019875;
+
+/*
+ * Danger colors (red)
+ */
+$border_danger: #d43f3a;
+$bg_danger: #d9534f;
+
+/*
+ * Primary colors (blue)
+ */
+$border_primary: #246;
+$bg_primary: #246;
+
+/*
+ * Warning colors (yellow)
+ */
+$bg_warning: #EB9532;
+$border_warning: #EB9532;
+
+/**
+ * Twitter bootstrap variables
+ */
+$font-size-base: 13px !default;
+$nav-pills-active-link-hover-bg: $bg_style_color;
+$pagination-active-bg: $bg_style_color;
+
+/**
+ * Avatar variables
+ */
+$avatar_radius: 50%;
diff --git a/app/assets/stylesheets/ci/sections/builds.scss b/app/assets/stylesheets/ci/sections/builds.scss
new file mode 100644
index 00000000000..a9d39bb0cbd
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/builds.scss
@@ -0,0 +1,54 @@
+pre.trace {
+ background: #111111;
+ color: #fff;
+ font-family: $monospace_font;
+ white-space: pre;
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+ overflow: auto;
+ overflow-y: hidden;
+ font-size: 12px;
+
+ .fa-refresh {
+ font-size: 24px;
+ margin-left: 20px;
+ }
+}
+
+.autoscroll-container {
+ position: fixed;
+ bottom: 10px;
+ right: 20px;
+ z-index: 100;
+}
+
+.scroll-controls {
+ position: fixed;
+ bottom: 10px;
+ left: 20px;
+ z-index: 100;
+
+ a {
+ display: block;
+ margin-bottom: 5px;
+ }
+}
+
+.build-widget {
+ padding: 10px;
+ background: #f4f4f4;
+ margin-bottom: 20px;
+ border-radius: 4px;
+
+ .title {
+ margin-top: 0;
+ color: #666;
+ line-height: 1.5;
+ }
+ .attr-name {
+ color: #777;
+ }
+}
diff --git a/app/assets/stylesheets/ci/sections/lint.scss b/app/assets/stylesheets/ci/sections/lint.scss
new file mode 100644
index 00000000000..7191b5d47aa
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/lint.scss
@@ -0,0 +1,8 @@
+.incorrect-syntax{
+ font-size: 19px;
+ color: red;
+}
+.correct-syntax{
+ font-size: 19px;
+ color: #47a447;
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/ci/sections/login.scss b/app/assets/stylesheets/ci/sections/login.scss
new file mode 100644
index 00000000000..47e453ec8d2
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/login.scss
@@ -0,0 +1,13 @@
+.login-block {
+ padding: 15px;
+ margin: 0 auto;
+ text-align: center;
+
+ p {
+ font-size: 15px;
+ }
+
+ .btn-login {
+ padding: 18px 32px;
+ }
+}
diff --git a/app/assets/stylesheets/ci/sections/navbar.scss b/app/assets/stylesheets/ci/sections/navbar.scss
new file mode 100644
index 00000000000..efa70eb2956
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/navbar.scss
@@ -0,0 +1,54 @@
+.navbar-static-top {
+ margin-bottom: 20px;
+}
+
+.navbar-ci {
+ background: $style_color;
+
+ .navbar-brand {
+ color: #fff;
+
+ &:hover {
+ color: #fff;
+ }
+ }
+ .brand,
+ .nav > li > a {
+ color: #fff;
+
+ &:hover, &:focus, &:active {
+ background: none;
+ }
+ }
+
+ .profile-holder {
+ position: relative;
+
+ img {
+ position: absolute;
+ top: -8px;
+ width: 32px;
+ @include border-radius(32px);
+ }
+
+ span {
+ margin-left: 42px;
+ }
+ }
+
+ .btn-login {
+ padding: 7px 22px;
+ margin-top: 7px;
+ &:hover, &:active, &:focus {
+ background: #018865 !important;
+ }
+ }
+}
+
+.turbolink-spinner {
+ position: absolute;
+ top: 11px;
+ left: 50%;
+ color: #FFF;
+ font-size: 20px;
+}
diff --git a/app/assets/stylesheets/ci/sections/projects.scss b/app/assets/stylesheets/ci/sections/projects.scss
new file mode 100644
index 00000000000..84ee1399bff
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/projects.scss
@@ -0,0 +1,61 @@
+.project-title {
+ margin: 0;
+ color: #444;
+ font-size: 20px;
+ line-height: 1.5;
+}
+
+.builds {
+ @extend .table;
+
+ .build {
+ &.alert{
+ margin-bottom: 6px;
+ }
+ }
+}
+
+.projects-table {
+ td {
+ vertical-align: middle !important;
+ }
+}
+
+.commit-info {
+ font-size: 14px;
+
+ .attr-name {
+ font-weight: 300;
+ color: #666;
+ margin-right: 5px;
+ }
+
+ pre.commit-message {
+ font-size: 14px;
+ background: none;
+ padding: 0;
+ margin: 0;
+ border: none;
+ margin: 20px 0;
+ border-bottom: 1px solid #EEE;
+ padding-bottom: 20px;
+ border-radius: 0;
+ }
+}
+
+.search{
+ width: 300px;
+
+ .search-input{
+ height: 35px;
+ }
+
+ form{
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+}
+
+.loading{
+ font-size: 20px;
+}
diff --git a/app/assets/stylesheets/ci/sections/runners.scss b/app/assets/stylesheets/ci/sections/runners.scss
new file mode 100644
index 00000000000..a9111a7388f
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/runners.scss
@@ -0,0 +1,34 @@
+.runner-state {
+ padding: 6px 12px;
+ margin-right: 10px;
+ color: #FFF;
+
+ &.runner-state-shared {
+ background: #32b186;
+ }
+ &.runner-state-specific {
+ background: #3498db;
+ }
+}
+
+.runner-status-online {
+ color: green;
+}
+
+.runner-status-offline {
+ color: gray;
+}
+
+.runner-status-paused {
+ color: red;
+}
+
+.runner {
+ .btn {
+ padding: 1px 6px;
+ }
+
+ h4 {
+ font-weight: normal;
+ }
+}
diff --git a/app/assets/stylesheets/ci/sections/setup.scss b/app/assets/stylesheets/ci/sections/setup.scss
new file mode 100644
index 00000000000..242614616d1
--- /dev/null
+++ b/app/assets/stylesheets/ci/sections/setup.scss
@@ -0,0 +1,11 @@
+.welcome-block {
+ margin-top: 50px;
+ color: #555;
+ font-size: 16px;
+ line-height: 1.5;
+
+ h1, h2, h3 {
+ font-weight: bold;
+ margin-bottom: 20px;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 12d439b0b31..ac9484a4cd2 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,9 +1,13 @@
require 'gon'
class ApplicationController < ActionController::Base
+ def self.railtie_helpers_paths
+ "app/helpers/gitlab"
+ end
+
include Gitlab::CurrentSettings
- include GitlabRoutingHelper
- include PageLayoutHelper
+ include Gitlab::GitlabRoutingHelper
+ include Gitlab::PageLayoutHelper
PER_PAGE = 20
@@ -131,9 +135,6 @@ class ApplicationController < ActionController::Base
def repository
@repository ||= project.repository
- rescue Grit::NoSuchPathError => e
- log_exception(e)
- nil
end
def authorize_project!(action)
diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb
new file mode 100644
index 00000000000..430fae14c7d
--- /dev/null
+++ b/app/controllers/ci/admin/application_controller.rb
@@ -0,0 +1,10 @@
+module Ci
+ module Admin
+ class ApplicationController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :authenticate_admin!
+
+ layout "ci/admin"
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb
new file mode 100644
index 00000000000..71e253fac67
--- /dev/null
+++ b/app/controllers/ci/admin/application_settings_controller.rb
@@ -0,0 +1,31 @@
+module Ci
+ class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController
+ before_action :set_application_setting
+
+ def show
+ end
+
+ def update
+ if @application_setting.update_attributes(application_setting_params)
+ redirect_to ci_admin_application_settings_path,
+ notice: 'Application settings saved successfully'
+ else
+ render :show
+ end
+ end
+
+ private
+
+ def set_application_setting
+ @application_setting = Ci::ApplicationSetting.current
+ @application_setting ||= Ci::ApplicationSetting.create_from_defaults
+ end
+
+ def application_setting_params
+ params.require(:application_setting).permit(
+ :all_broken_builds,
+ :add_pusher,
+ )
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb
new file mode 100644
index 00000000000..8fc776dd98e
--- /dev/null
+++ b/app/controllers/ci/admin/builds_controller.rb
@@ -0,0 +1,12 @@
+module Ci
+ class Admin::BuildsController < Ci::Admin::ApplicationController
+ def index
+ @scope = params[:scope]
+ @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30)
+
+ if ["pending", "running"].include? @scope
+ @builds = @builds.send(@scope)
+ end
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb
new file mode 100644
index 00000000000..5939efff980
--- /dev/null
+++ b/app/controllers/ci/admin/events_controller.rb
@@ -0,0 +1,9 @@
+module Ci
+ class Admin::EventsController < Ci::Admin::ApplicationController
+ EVENTS_PER_PAGE = 50
+
+ def index
+ @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE)
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb
new file mode 100644
index 00000000000..5bbd0ce7396
--- /dev/null
+++ b/app/controllers/ci/admin/projects_controller.rb
@@ -0,0 +1,19 @@
+module Ci
+ class Admin::ProjectsController < Ci::Admin::ApplicationController
+ def index
+ @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30)
+ end
+
+ def destroy
+ project.destroy
+
+ redirect_to ci_projects_url
+ end
+
+ protected
+
+ def project
+ @project ||= Ci::Project.find(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb
new file mode 100644
index 00000000000..e7de6eb12ca
--- /dev/null
+++ b/app/controllers/ci/admin/runner_projects_controller.rb
@@ -0,0 +1,34 @@
+module Ci
+ class Admin::RunnerProjectsController < Ci::Admin::ApplicationController
+ layout 'ci/project'
+
+ def index
+ @runner_projects = project.runner_projects.all
+ @runner_project = project.runner_projects.new
+ end
+
+ def create
+ @runner = Ci::Runner.find(params[:runner_project][:runner_id])
+
+ if @runner.assign_to(project, current_user)
+ redirect_to ci_admin_runner_path(@runner)
+ else
+ redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ rp = Ci::RunnerProject.find(params[:id])
+ runner = rp.runner
+ rp.destroy
+
+ redirect_to ci_admin_runner_path(runner)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb
new file mode 100644
index 00000000000..4f5f3776ddc
--- /dev/null
+++ b/app/controllers/ci/admin/runners_controller.rb
@@ -0,0 +1,69 @@
+module Ci
+ class Admin::RunnersController < Ci::Admin::ApplicationController
+ before_filter :runner, except: :index
+
+ def index
+ @runners = Ci::Runner.order('id DESC')
+ @runners = @runners.search(params[:search]) if params[:search].present?
+ @runners = @runners.page(params[:page]).per(30)
+ @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count
+ end
+
+ def show
+ @builds = @runner.builds.order('id DESC').first(30)
+ @projects = Ci::Project.all
+ @projects = @projects.search(params[:search]) if params[:search].present?
+ @projects = @projects.where("projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any?
+ @projects = @projects.page(params[:page]).per(30)
+ end
+
+ def update
+ @runner.update_attributes(runner_params)
+
+ respond_to do |format|
+ format.js
+ format.html { redirect_to ci_admin_runner_path(@runner) }
+ end
+ end
+
+ def destroy
+ @runner.destroy
+
+ redirect_to ci_admin_runners_path
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
+ end
+ end
+
+ def assign_all
+ Ci::Project.unassigned(@runner).all.each do |project|
+ @runner.assign_to(project, current_user)
+ end
+
+ redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects"
+ end
+
+ private
+
+ def runner
+ @runner ||= Ci::Runner.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active)
+ end
+ end
+end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
new file mode 100644
index 00000000000..726781cb30b
--- /dev/null
+++ b/app/controllers/ci/application_controller.rb
@@ -0,0 +1,133 @@
+module Ci
+ class ApplicationController < ActionController::Base
+ def self.railtie_helpers_paths
+ "app/helpers/ci"
+ end
+
+ include Ci::UserSessionsHelper
+
+ rescue_from Ci::Network::UnauthorizedError, with: :invalid_token
+ before_filter :default_headers
+ before_filter :check_config
+
+ protect_from_forgery
+
+ helper_method :current_user
+ before_filter :reset_cache
+
+ private
+
+ def current_user
+ @current_user ||= session[:ci_current_user]
+ end
+
+ def sign_in(user)
+ session[:ci_current_user] = user
+ end
+
+ def sign_out
+ reset_session
+ end
+
+ def authenticate_user!
+ unless current_user
+ redirect_to new_ci_user_sessions_path
+ return
+ end
+ end
+
+ def authenticate_admin!
+ unless current_user && current_user.is_admin
+ redirect_to new_ci_user_sessions_path
+ return
+ end
+ end
+
+ def authenticate_public_page!
+ unless project.public
+ unless current_user
+ redirect_to(new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath))) and return
+ end
+
+ unless current_user.can_access_project?(project.gitlab_id)
+ page_404 and return
+ end
+ end
+ end
+
+ def authenticate_token!
+ unless project.valid_token?(params[:token])
+ return head(403)
+ end
+ end
+
+ def authorize_access_project!
+ unless current_user.can_access_project?(@project.gitlab_id)
+ return page_404
+ end
+ end
+
+ def authorize_project_developer!
+ unless current_user.has_developer_access?(@project.gitlab_id)
+ return page_404
+ end
+ end
+
+ def authorize_manage_project!
+ unless current_user.can_manage_project?(@project.gitlab_id)
+ return page_404
+ end
+ end
+
+ def page_404
+ render file: "#{Rails.root}/public/404.html", status: 404, layout: false
+ end
+
+ # Reset user cache every day for security purposes
+ def reset_cache
+ if current_user && current_user.sync_at < (Time.zone.now - 24.hours)
+ current_user.reset_cache
+ end
+ end
+
+ def default_headers
+ headers['X-Frame-Options'] = 'DENY'
+ headers['X-XSS-Protection'] = '1; mode=block'
+ end
+
+ # JSON for infinite scroll via Pager object
+ def pager_json(partial, count)
+ html = render_to_string(
+ partial,
+ layout: false,
+ formats: [:html]
+ )
+
+ render json: {
+ html: html,
+ count: count
+ }
+ end
+
+ def check_config
+ redirect_to oauth2_ci_help_path unless valid_config?
+ end
+
+ def valid_config?
+ server = GitlabCi.config.gitlab_server
+
+ if server.blank? || server.url.blank? || server.app_id.blank? || server.app_secret.blank?
+ false
+ else
+ true
+ end
+ rescue Settingslogic::MissingSetting, NoMethodError
+ false
+ end
+
+ def invalid_token
+ reset_session
+ redirect_to ci_root_path
+ end
+ end
+end
diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb
new file mode 100644
index 00000000000..eeff3f1e0a0
--- /dev/null
+++ b/app/controllers/ci/builds_controller.rb
@@ -0,0 +1,77 @@
+module Ci
+ class BuildsController < Ci::ApplicationController
+ before_filter :authenticate_user!, except: [:status, :show]
+ before_filter :authenticate_public_page!, only: :show
+ before_filter :project
+ before_filter :authorize_access_project!, except: [:status, :show]
+ before_filter :authorize_manage_project!, except: [:status, :show, :retry, :cancel]
+ before_filter :authorize_project_developer!, only: [:retry, :cancel]
+ before_filter :build, except: [:show]
+
+ def show
+ if params[:id] =~ /\A\d+\Z/
+ @build = build
+ else
+ # try to find commit by sha
+ commit = commit_by_sha
+
+ if commit
+ # Redirect to commit page
+ redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha)
+ return
+ end
+ end
+
+ raise ActiveRecord::RecordNotFound unless @build
+
+ @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC')
+ @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20)
+ @commit = @build.commit
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @build.to_json(methods: :trace_html)
+ end
+ end
+ end
+
+ def retry
+ if @build.commands.blank?
+ return page_404
+ end
+
+ build = Ci::Build.retry(@build)
+
+ if params[:return_to]
+ redirect_to URI.parse(params[:return_to]).path
+ else
+ redirect_to ci_project_build_path(project, build)
+ end
+ end
+
+ def status
+ render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
+ end
+
+ def cancel
+ @build.cancel
+
+ redirect_to ci_project_build_path(@project, @build)
+ end
+
+ protected
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def build
+ @build ||= project.builds.unscoped.find_by(id: params[:id])
+ end
+
+ def commit_by_sha
+ @project.commits.find_by(sha: params[:id])
+ end
+ end
+end
diff --git a/app/controllers/ci/charts_controller.rb b/app/controllers/ci/charts_controller.rb
new file mode 100644
index 00000000000..63326ef36cc
--- /dev/null
+++ b/app/controllers/ci/charts_controller.rb
@@ -0,0 +1,24 @@
+module Ci
+ class ChartsController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def show
+ @charts = {}
+ @charts[:week] = Ci::Charts::WeekChart.new(@project)
+ @charts[:month] = Ci::Charts::MonthChart.new(@project)
+ @charts[:year] = Ci::Charts::YearChart.new(@project)
+ @charts[:build_times] = Ci::Charts::BuildTime.new(@project)
+ end
+
+ protected
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb
new file mode 100644
index 00000000000..9f74a2fd807
--- /dev/null
+++ b/app/controllers/ci/commits_controller.rb
@@ -0,0 +1,37 @@
+module Ci
+ class CommitsController < Ci::ApplicationController
+ before_filter :authenticate_user!, except: [:status, :show]
+ before_filter :authenticate_public_page!, only: :show
+ before_filter :project
+ before_filter :authorize_access_project!, except: [:status, :show, :cancel]
+ before_filter :authorize_project_developer!, only: [:cancel]
+ before_filter :commit, only: :show
+
+ def show
+ @builds = @commit.builds
+ end
+
+ def status
+ commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
+ render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage])
+ rescue ActiveRecord::RecordNotFound
+ render json: { status: "not_found" }
+ end
+
+ def cancel
+ commit.builds.running_or_pending.each(&:cancel)
+
+ redirect_to ci_project_ref_commit_path(project, commit.ref, commit.sha)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+
+ def commit
+ @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb
new file mode 100644
index 00000000000..c515caabe63
--- /dev/null
+++ b/app/controllers/ci/events_controller.rb
@@ -0,0 +1,21 @@
+module Ci
+ class EventsController < Ci::ApplicationController
+ EVENTS_PER_PAGE = 50
+
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/helps_controller.rb b/app/controllers/ci/helps_controller.rb
new file mode 100644
index 00000000000..a1ee4111614
--- /dev/null
+++ b/app/controllers/ci/helps_controller.rb
@@ -0,0 +1,16 @@
+module Ci
+ class HelpsController < Ci::ApplicationController
+ skip_filter :check_config
+
+ def show
+ end
+
+ def oauth2
+ if valid_config?
+ redirect_to ci_root_path
+ else
+ render layout: 'ci/empty'
+ end
+ end
+ end
+end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
new file mode 100644
index 00000000000..62c2ba86e86
--- /dev/null
+++ b/app/controllers/ci/lints_controller.rb
@@ -0,0 +1,26 @@
+module Ci
+ class LintsController < Ci::ApplicationController
+ before_filter :authenticate_user!
+
+ def show
+ end
+
+ def create
+ if params[:content].blank?
+ @status = false
+ @error = "Please provide content of .gitlab-ci.yml"
+ else
+ @config_processor = Ci::GitlabCiYamlProcessor.new params[:content]
+ @stages = @config_processor.stages
+ @builds = @config_processor.builds
+ @status = true
+ end
+ rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ @error = e.message
+ @status = false
+ rescue Exception => e
+ @error = "Undefined error"
+ @status = false
+ end
+ end
+end
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
new file mode 100644
index 00000000000..6ff7fc9f77a
--- /dev/null
+++ b/app/controllers/ci/projects_controller.rb
@@ -0,0 +1,136 @@
+module Ci
+ class ProjectsController < Ci::ApplicationController
+ PROJECTS_BATCH = 100
+
+ before_filter :authenticate_user!, except: [:build, :badge, :index, :show]
+ before_filter :authenticate_public_page!, only: :show
+ before_filter :project, only: [:build, :integration, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
+ before_filter :authorize_access_project!, except: [:build, :gitlab, :badge, :index, :show, :new, :create]
+ before_filter :authorize_manage_project!, only: [:edit, :integration, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
+ before_filter :authenticate_token!, only: [:build]
+ before_filter :no_cache, only: [:badge]
+ protect_from_forgery except: :build
+
+ layout 'ci/project', except: [:index, :gitlab]
+
+ def index
+ @projects = Ci::Project.ordered_by_last_commit_date.public_only.page(params[:page]) unless current_user
+ end
+
+ def gitlab
+ @limit, @offset = (params[:limit] || PROJECTS_BATCH).to_i, (params[:offset] || 0).to_i
+ @page = @offset == 0 ? 1 : (@offset / @limit + 1)
+
+ current_user.reset_cache if params[:reset_cache]
+
+ @gl_projects = current_user.gitlab_projects(params[:search], @page, @limit)
+ @projects = Ci::Project.where(gitlab_id: @gl_projects.map(&:id)).ordered_by_last_commit_date
+ @total_count = @gl_projects.size
+ @gl_projects.reject! { |gl_project| @projects.map(&:gitlab_id).include?(gl_project.id) }
+ respond_to do |format|
+ format.json do
+ pager_json("ci/projects/gitlab", @total_count)
+ end
+ end
+ rescue Ci::Network::UnauthorizedError
+ raise
+ rescue
+ @error = 'Failed to fetch GitLab projects'
+ end
+
+ def show
+ @ref = params[:ref]
+
+ @commits = @project.commits.reverse_order
+ @commits = @commits.where(ref: @ref) if @ref
+ @commits = @commits.page(params[:page]).per(20)
+ end
+
+ def integration
+ end
+
+ def create
+ project_data = OpenStruct.new(JSON.parse(params["project"]))
+
+ unless current_user.can_manage_project?(project_data.id)
+ return redirect_to ci_root_path, alert: 'You have to have at least master role to enable CI for this project'
+ end
+
+ @project = Ci::CreateProjectService.new.execute(current_user, project_data, ci_project_url(":project_id"))
+
+ if @project.persisted?
+ redirect_to ci_project_path(@project, show_guide: true), notice: 'Project was successfully created.'
+ else
+ redirect_to :back, alert: 'Cannot save project'
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ if project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, project)
+
+ redirect_to :back, notice: 'Project was successfully updated.'
+ else
+ render action: "edit"
+ end
+ end
+
+ def destroy
+ project.destroy
+ Ci::Network.new.disable_ci(project.gitlab_id, current_user.authenticate_options)
+
+ Ci::EventService.new.remove_project(current_user, project)
+
+ redirect_to ci_projects_url
+ end
+
+ def build
+ @commit = Ci::CreateCommitService.new.execute(@project, params.dup)
+
+ if @commit && @commit.valid?
+ head 201
+ else
+ head 400
+ end
+ end
+
+ # Project status badge
+ # Image with build status for sha or ref
+ def badge
+ image = Ci::ImageForBuildService.new.execute(@project, params)
+
+ send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
+ end
+
+ def toggle_shared_runners
+ project.toggle!(:shared_runners_enabled)
+ redirect_to :back
+ end
+
+ def dumped_yaml
+ send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml'
+ end
+
+ protected
+
+ def project
+ @project ||= Ci::Project.find(params[:id])
+ end
+
+ def no_cache
+ response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
+ end
+
+ def project_params
+ params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build,
+ :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients,
+ :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token,
+ { variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+ end
+end
diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb
new file mode 100644
index 00000000000..3a52087cc6b
--- /dev/null
+++ b/app/controllers/ci/runner_projects_controller.rb
@@ -0,0 +1,34 @@
+module Ci
+ class RunnerProjectsController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def create
+ @runner = Ci::Runner.find(params[:runner_project][:runner_id])
+
+ return head(403) unless current_user.authorized_runners.include?(@runner)
+
+ if @runner.assign_to(project, current_user)
+ redirect_to ci_project_runners_path(project)
+ else
+ redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ runner_project = project.runner_projects.find(params[:id])
+ runner_project.destroy
+
+ redirect_to ci_project_runners_path(project)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/runners_controller.rb b/app/controllers/ci/runners_controller.rb
new file mode 100644
index 00000000000..01eebf7e6a7
--- /dev/null
+++ b/app/controllers/ci/runners_controller.rb
@@ -0,0 +1,71 @@
+module Ci
+ class RunnersController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @runners = @project.runners.order('id DESC')
+ @specific_runners = current_user.authorized_runners.
+ where.not(id: @runners).order('runners.id DESC').page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def edit
+ end
+
+ def update
+ if @runner.update_attributes(runner_params)
+ redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def destroy
+ if @runner.only_for?(@project)
+ @runner.destroy
+ end
+
+ redirect_to ci_project_runners_path(@project)
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def show
+ end
+
+ protected
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def set_runner
+ @runner ||= @project.runners.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:description, :tag_list, :contacted_at, :active)
+ end
+ end
+end
diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb
new file mode 100644
index 00000000000..e99f40f3a0a
--- /dev/null
+++ b/app/controllers/ci/services_controller.rb
@@ -0,0 +1,59 @@
+module Ci
+ class ServicesController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+ before_filter :service, only: [:edit, :update, :test]
+
+ respond_to :html
+
+ layout 'ci/project'
+
+ def index
+ @project.build_missing_services
+ @services = @project.services.reload
+ end
+
+ def edit
+ end
+
+ def update
+ if @service.update_attributes(service_params)
+ redirect_to edit_ci_project_service_path(@project, @service.to_param), notice: 'Service was successfully updated.'
+ else
+ render 'edit'
+ end
+ end
+
+ def test
+ last_build = @project.builds.last
+
+ if @service.execute(last_build)
+ message = { notice: 'We successfully tested the service' }
+ else
+ message = { alert: 'We tried to test the service but error occurred' }
+ end
+
+ redirect_to :back, message
+ end
+
+ private
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def service
+ @service ||= @project.services.find { |service| service.to_param == params[:id] }
+ end
+
+ def service_params
+ params.require(:service).permit(
+ :type, :active, :webhook, :notify_only_broken_builds,
+ :email_recipients, :email_only_broken_builds, :email_add_pusher,
+ :hipchat_token, :hipchat_room, :hipchat_server
+ )
+ end
+ end
+end
diff --git a/app/controllers/ci/triggers_controller.rb b/app/controllers/ci/triggers_controller.rb
new file mode 100644
index 00000000000..6ba37cd843e
--- /dev/null
+++ b/app/controllers/ci/triggers_controller.rb
@@ -0,0 +1,43 @@
+module Ci
+ class TriggersController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @triggers = @project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def create
+ @trigger = @project.triggers.new
+ @trigger.save
+
+ if @trigger.valid?
+ redirect_to ci_project_triggers_path(@project)
+ else
+ @triggers = @project.triggers.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ trigger.destroy
+
+ redirect_to ci_project_triggers_path(@project)
+ end
+
+ private
+
+ def trigger
+ @trigger ||= @project.triggers.find(params[:id])
+ end
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/user_sessions_controller.rb b/app/controllers/ci/user_sessions_controller.rb
new file mode 100644
index 00000000000..82134c1f7ba
--- /dev/null
+++ b/app/controllers/ci/user_sessions_controller.rb
@@ -0,0 +1,65 @@
+module Ci
+ class UserSessionsController < Ci::ApplicationController
+ before_filter :authenticate_user!, except: [:new, :callback, :auth]
+
+ def show
+ @user = current_user
+ end
+
+ def new
+ end
+
+ def auth
+ unless is_oauth_state_valid?(params[:state])
+ redirect_to new_ci_user_sessions_path
+ return
+ end
+
+ redirect_to client.auth_code.authorize_url({
+ redirect_uri: callback_ci_user_sessions_url,
+ state: params[:state]
+ })
+ end
+
+ def callback
+ unless is_oauth_state_valid?(params[:state])
+ redirect_to new_ci_user_sessions_path
+ return
+ end
+
+ token = client.auth_code.get_token(params[:code], redirect_uri: callback_ci_user_sessions_url).token
+
+ @user_session = Ci::UserSession.new
+ user = @user_session.authenticate(access_token: token)
+
+ if user && sign_in(user)
+ return_to = get_ouath_state_return_to(params[:state])
+ redirect_to(return_to || ci_root_path)
+ else
+ @error = 'Invalid credentials'
+ render :new
+ end
+
+ end
+
+ def destroy
+ sign_out
+
+ redirect_to new_ci_user_sessions_path
+ end
+
+ protected
+
+ def client
+ @client ||= ::OAuth2::Client.new(
+ GitlabCi.config.gitlab_server.app_id,
+ GitlabCi.config.gitlab_server.app_secret,
+ {
+ site: GitlabCi.config.gitlab_server.url,
+ authorize_url: 'oauth/authorize',
+ token_url: 'oauth/token'
+ }
+ )
+ end
+ end
+end
diff --git a/app/controllers/ci/variables_controller.rb b/app/controllers/ci/variables_controller.rb
new file mode 100644
index 00000000000..6908e0877f0
--- /dev/null
+++ b/app/controllers/ci/variables_controller.rb
@@ -0,0 +1,33 @@
+module Ci
+ class VariablesController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def show
+ end
+
+ def update
+ if project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, project)
+
+ redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+
+ def project_params
+ params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+ end
+end
diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb
new file mode 100644
index 00000000000..eea4842c91c
--- /dev/null
+++ b/app/controllers/ci/web_hooks_controller.rb
@@ -0,0 +1,53 @@
+module Ci
+ class WebHooksController < Ci::ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @web_hooks = @project.web_hooks
+ @web_hook = Ci::WebHook.new
+ end
+
+ def create
+ @web_hook = @project.web_hooks.new(web_hook_params)
+ @web_hook.save
+
+ if @web_hook.valid?
+ redirect_to ci_project_web_hooks_path(@project)
+ else
+ @web_hooks = @project.web_hooks.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def test
+ Ci::TestHookService.new.execute(hook, current_user)
+
+ redirect_to :back
+ end
+
+ def destroy
+ hook.destroy
+
+ redirect_to ci_project_web_hooks_path(@project)
+ end
+
+ private
+
+ def hook
+ @web_hook ||= @project.web_hooks.find(params[:id])
+ end
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def web_hook_params
+ params.require(:web_hook).permit(:url)
+ end
+ end
+end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index fc31118124b..4e007d2a4d0 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,6 +1,6 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
- include PageLayoutHelper
+ include Gitlab::PageLayoutHelper
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 4193ac11399..08d94408fc8 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -1,5 +1,5 @@
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
- include PageLayoutHelper
+ include Gitlab::PageLayoutHelper
layout 'profile'
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index b181c47baec..b70e12365da 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -1,6 +1,6 @@
class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath
- include ApplicationHelper
+ include Gitlab::ApplicationHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 6080c849c8d..a9081a5ae16 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -1,6 +1,6 @@
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
- include TreeHelper
+ include Gitlab::TreeHelper
before_action :require_non_empty_project
before_action :assign_ref_vars
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 50512cb6dc3..870ff035b03 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -5,7 +5,7 @@ class Projects::WikisController < Projects::ApplicationController
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
before_action :load_project_wiki
- include WikiHelper
+ include Gitlab::WikiHelper
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index eb0408a95e5..63d336b2bd5 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,5 +1,5 @@
class SearchController < ApplicationController
- include SearchHelper
+ include Gitlab::SearchHelper
layout 'search'
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
deleted file mode 100644
index 14df8d4cbd7..00000000000
--- a/app/helpers/appearances_helper.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module AppearancesHelper
- def brand_item
- nil
- end
-
- def brand_title
- 'GitLab Community Edition'
- end
-
- def brand_image
- nil
- end
-
- def brand_text
- nil
- end
-
- def brand_header_logo
- image_tag 'logo.svg'
- end
-end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
deleted file mode 100644
index a803b66c502..00000000000
--- a/app/helpers/application_helper.rb
+++ /dev/null
@@ -1,315 +0,0 @@
-require 'digest/md5'
-require 'uri'
-
-module ApplicationHelper
- # Check if a particular controller is the current one
- #
- # args - One or more controller names to check
- #
- # Examples
- #
- # # On TreeController
- # current_controller?(:tree) # => true
- # current_controller?(:commits) # => false
- # current_controller?(:commits, :tree) # => true
- def current_controller?(*args)
- args.any? { |v| v.to_s.downcase == controller.controller_name }
- end
-
- # Check if a particular action is the current one
- #
- # args - One or more action names to check
- #
- # Examples
- #
- # # On Projects#new
- # current_action?(:new) # => true
- # current_action?(:create) # => false
- # current_action?(:new, :create) # => true
- def current_action?(*args)
- args.any? { |v| v.to_s.downcase == action_name }
- end
-
- def project_icon(project_id, options = {})
- project =
- if project_id.is_a?(Project)
- project = project_id
- else
- Project.find_with_namespace(project_id)
- end
-
- if project.avatar_url
- image_tag project.avatar_url, options
- else # generated icon
- project_identicon(project, options)
- end
- end
-
- def project_identicon(project, options = {})
- allowed_colors = {
- red: 'FFEBEE',
- purple: 'F3E5F5',
- indigo: 'E8EAF6',
- blue: 'E3F2FD',
- teal: 'E0F2F1',
- orange: 'FBE9E7',
- gray: 'EEEEEE'
- }
-
- options[:class] ||= ''
- options[:class] << ' identicon'
- bg_key = project.id % 7
- style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555"
-
- content_tag(:div, class: options[:class], style: style) do
- project.name[0, 1].upcase
- end
- end
-
- def avatar_icon(user_email = '', size = nil)
- user = User.find_by(email: user_email)
-
- if user
- user.avatar_url(size) || default_avatar
- else
- gravatar_icon(user_email, size)
- end
- end
-
- def gravatar_icon(user_email = '', size = nil)
- GravatarService.new.execute(user_email, size) ||
- default_avatar
- end
-
- def default_avatar
- image_path('no_avatar.png')
- end
-
- def last_commit(project)
- if project.repo_exists?
- time_ago_with_tooltip(project.repository.commit.committed_date)
- else
- 'Never'
- end
- rescue
- 'Never'
- end
-
- def grouped_options_refs
- repository = @project.repository
-
- options = [
- ['Branches', repository.branch_names],
- ['Tags', VersionSorter.rsort(repository.tag_names)]
- ]
-
- # If reference is commit id - we should add it to branch/tag selectbox
- if(@ref && !options.flatten.include?(@ref) &&
- @ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
- options << ['Commit', [@ref]]
- end
-
- grouped_options_for_select(options, @ref || @project.default_branch)
- end
-
- def emoji_autocomplete_source
- # should be an array of strings
- # so to_s can be called, because it is sufficient and to_json is too slow
- Emoji.names.to_s
- end
-
- # Define whenever show last push event
- # with suggestion to create MR
- def show_last_push_widget?(event)
- # Skip if event is not about added or modified non-master branch
- return false unless event && event.last_push_to_non_root? && !event.rm_ref?
-
- project = event.project
-
- # Skip if project repo is empty or MR disabled
- return false unless project && !project.empty_repo? && project.merge_requests_enabled
-
- # Skip if user already created appropriate MR
- return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
-
- # Skip if user removed branch right after that
- return false unless project.repository.branch_names.include?(event.branch_name)
-
- true
- end
-
- def hexdigest(string)
- Digest::SHA1.hexdigest string
- end
-
- def simple_sanitize(str)
- sanitize(str, tags: %w(a span))
- end
-
- def body_data_page
- path = controller.controller_path.split('/')
- namespace = path.first if path.second
-
- [namespace, controller.controller_name, controller.action_name].compact.join(':')
- end
-
- # shortcut for gitlab config
- def gitlab_config
- Gitlab.config.gitlab
- end
-
- # shortcut for gitlab extra config
- def extra_config
- Gitlab.config.extra
- end
-
- def search_placeholder
- if @project && @project.persisted?
- 'Search in this project'
- elsif @snippet || @snippets || @show_snippets
- 'Search snippets'
- elsif @group && @group.persisted?
- 'Search in this group'
- else
- 'Search'
- end
- end
-
- def broadcast_message
- BroadcastMessage.current
- end
-
- # Render a `time` element with Javascript-based relative date and tooltip
- #
- # time - Time object
- # placement - Tooltip placement String (default: "top")
- # html_class - Custom class for `time` element (default: "time_ago")
- # skip_js - When true, exclude the `script` tag (default: false)
- #
- # By default also includes a `script` element with Javascript necessary to
- # initialize the `timeago` jQuery extension. If this method is called many
- # times, for example rendering hundreds of commits, it's advisable to disable
- # this behavior using the `skip_js` argument and re-initializing `timeago`
- # manually once all of the elements have been rendered.
- #
- # A `js-timeago` class is always added to the element, even when a custom
- # `html_class` argument is provided.
- #
- # Returns an HTML-safe String
- def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
- element = content_tag :time, time.to_s,
- class: "#{html_class} js-timeago",
- datetime: time.getutc.iso8601,
- title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
- data: { toggle: 'tooltip', placement: placement }
-
- element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
-
- element
- end
-
- def render_markup(file_name, file_content)
- if gitlab_markdown?(file_name)
- Haml::Helpers.preserve(markdown(file_content))
- elsif asciidoc?(file_name)
- asciidoc(file_content)
- elsif plain?(file_name)
- content_tag :pre, class: 'plain-readme' do
- file_content
- end
- else
- GitHub::Markup.render(file_name, file_content).
- force_encoding(file_content.encoding).html_safe
- end
- rescue RuntimeError
- simple_format(file_content)
- end
-
- def plain?(filename)
- Gitlab::MarkupHelper.plain?(filename)
- end
-
- def markup?(filename)
- Gitlab::MarkupHelper.markup?(filename)
- end
-
- def gitlab_markdown?(filename)
- Gitlab::MarkupHelper.gitlab_markdown?(filename)
- end
-
- def asciidoc?(filename)
- Gitlab::MarkupHelper.asciidoc?(filename)
- end
-
- def promo_host
- 'about.gitlab.com'
- end
-
- def promo_url
- 'https://' + promo_host
- end
-
- def page_filter_path(options = {})
- without = options.delete(:without)
-
- exist_opts = {
- state: params[:state],
- scope: params[:scope],
- label_name: params[:label_name],
- milestone_id: params[:milestone_id],
- assignee_id: params[:assignee_id],
- author_id: params[:author_id],
- sort: params[:sort],
- }
-
- options = exist_opts.merge(options)
-
- if without.present?
- without.each do |key|
- options.delete(key)
- end
- end
-
- path = request.path
- path << "?#{options.to_param}"
- path
- end
-
- def outdated_browser?
- browser.ie? && browser.version.to_i < 10
- end
-
- def path_to_key(key, admin = false)
- if admin
- admin_user_key_path(@user, key)
- else
- profile_key_path(key)
- end
- end
-
- def state_filters_text_for(entity, project)
- titles = {
- opened: "Open"
- }
-
- entity_title = titles[entity] || entity.to_s.humanize
-
- count =
- if project.nil?
- nil
- elsif current_controller?(:issues)
- project.issues.send(entity).count
- elsif current_controller?(:merge_requests)
- project.merge_requests.send(entity).count
- end
-
- html = content_tag :span, entity_title
-
- if count.present?
- html += " "
- html += content_tag :span, number_with_delimiter(count), class: 'badge'
- end
-
- html.html_safe
- end
-end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
deleted file mode 100644
index 7d6b58ee21a..00000000000
--- a/app/helpers/application_settings_helper.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-module ApplicationSettingsHelper
- def gravatar_enabled?
- current_application_settings.gravatar_enabled?
- end
-
- def twitter_sharing_enabled?
- current_application_settings.twitter_sharing_enabled?
- end
-
- def signup_enabled?
- current_application_settings.signup_enabled?
- end
-
- def signin_enabled?
- current_application_settings.signin_enabled?
- end
-
- def extra_sign_in_text
- current_application_settings.sign_in_text
- end
-
- def user_oauth_applications?
- current_application_settings.user_oauth_applications
- end
-
- # Return a group of checkboxes that use Bootstrap's button plugin for a
- # toggle button effect.
- def restricted_level_checkboxes(help_block_id)
- Gitlab::VisibilityLevel.options.map do |name, level|
- checked = restricted_visibility_levels(true).include?(level)
- css_class = 'btn'
- css_class += ' active' if checked
- checkbox_name = 'application_setting[restricted_visibility_levels][]'
-
- label_tag(checkbox_name, class: css_class) do
- check_box_tag(checkbox_name, level, checked,
- autocomplete: 'off',
- 'aria-describedby' => help_block_id) + name
- end
- end
- end
-
- # Return a group of checkboxes that use Bootstrap's button plugin for a
- # toggle button effect.
- def import_sources_checkboxes(help_block_id)
- Gitlab::ImportSources.options.map do |name, source|
- checked = current_application_settings.import_sources.include?(source)
- css_class = 'btn'
- css_class += ' active' if checked
- checkbox_name = 'application_setting[import_sources][]'
-
- label_tag(checkbox_name, class: css_class) do
- check_box_tag(checkbox_name, source, checked,
- autocomplete: 'off',
- 'aria-describedby' => help_block_id) + name
- end
- end
- end
-end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
deleted file mode 100644
index 0e7a37b4cc6..00000000000
--- a/app/helpers/auth_helper.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
- FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze
-
- def ldap_enabled?
- Gitlab.config.ldap.enabled
- end
-
- def provider_has_icon?(name)
- PROVIDERS_WITH_ICONS.include?(name.to_s)
- end
-
- def auth_providers
- Gitlab::OAuth::Provider.providers
- end
-
- def label_for_provider(name)
- Gitlab::OAuth::Provider.label_for(name)
- end
-
- def form_based_provider?(name)
- FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
- end
-
- def form_based_providers
- auth_providers.select { |provider| form_based_provider?(provider) }
- end
-
- def button_based_providers
- auth_providers.reject { |provider| form_based_provider?(provider) }
- end
-
- def provider_image_tag(provider, size = 64)
- label = label_for_provider(provider)
-
- if provider_has_icon?(provider)
- file_name = "#{provider.to_s.split('_').first}_#{size}.png"
-
- image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}")
- else
- label
- end
- end
-
- def auth_active?(provider)
- current_user.identities.exists?(provider: provider.to_s)
- end
-
- extend self
-end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
deleted file mode 100644
index 77d99140c43..00000000000
--- a/app/helpers/blob_helper.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-module BlobHelper
- def highlight(blob_name, blob_content, nowrap: false, continue: false)
- @formatter ||= Rouge::Formatters::HTMLGitlab.new(
- nowrap: nowrap,
- cssclass: 'code highlight',
- lineanchors: true,
- lineanchorsid: 'LC'
- )
-
- begin
- @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
- result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe
- rescue
- @lexer = Rouge::Lexers::PlainText
- result = @formatter.format(@lexer.lex(blob_content)).html_safe
- end
-
- result
- end
-
- def no_highlight_files
- %w(credits changelog news copying copyright license authors)
- end
-
- def edit_blob_link(project, ref, path, options = {})
- blob =
- begin
- project.repository.blob_at(ref, path)
- rescue
- nil
- end
-
- if blob && blob.text?
- text = 'Edit'
- after = options[:after] || ''
- from_mr = options[:from_merge_request_id]
- link_opts = {}
- link_opts[:from_merge_request_id] = from_mr if from_mr
- cls = 'btn btn-small'
- if allowed_tree_edit?(project, ref)
- link_to(text,
- namespace_project_edit_blob_path(project.namespace, project,
- tree_join(ref, path),
- link_opts),
- class: cls
- )
- else
- content_tag :span, text, class: cls + ' disabled'
- end + after.html_safe
- else
- ''
- end
- end
-
- def leave_edit_message
- "Leave edit mode?\nAll unsaved changes will be lost."
- end
-
- def editing_preview_title(filename)
- if Gitlab::MarkupHelper.previewable?(filename)
- 'Preview'
- else
- 'Preview changes'
- end
- end
-
- # Return an image icon depending on the file mode and extension
- #
- # mode - File unix mode
- # mode - File name
- def blob_icon(mode, name)
- icon("#{file_type_icon_class('file', mode, name)} fw")
- end
-end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
deleted file mode 100644
index d6eaa7d57bc..00000000000
--- a/app/helpers/branches_helper.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module BranchesHelper
- def can_remove_branch?(project, branch_name)
- if project.protected_branch? branch_name
- false
- elsif branch_name == project.repository.root_ref
- false
- else
- can?(current_user, :push_code, project)
- end
- end
-
- def can_push_branch?(project, branch_name)
- return false unless project.repository.branch_names.include?(branch_name)
-
- ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
- end
-end
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
deleted file mode 100644
index 6484dca6b55..00000000000
--- a/app/helpers/broadcast_messages_helper.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module BroadcastMessagesHelper
- def broadcast_styling(broadcast_message)
- styling = ''
-
- if broadcast_message.color.present?
- styling << "background-color: #{broadcast_message.color}"
- styling << '; ' if broadcast_message.font.present?
- end
-
- if broadcast_message.font.present?
- styling << "color: #{broadcast_message.font}"
- end
-
- styling
- end
-end
diff --git a/app/helpers/ci/application_helper.rb b/app/helpers/ci/application_helper.rb
new file mode 100644
index 00000000000..3198fe55f91
--- /dev/null
+++ b/app/helpers/ci/application_helper.rb
@@ -0,0 +1,140 @@
+module Ci
+ module ApplicationHelper
+ def loader_html
+ image_tag 'ci/loader.gif', alt: 'Loading'
+ end
+
+ # Navigation link helper
+ #
+ # Returns an `li` element with an 'active' class if the supplied
+ # controller(s) and/or action(s) are currently active. The content of the
+ # element is the value passed to the block.
+ #
+ # options - The options hash used to determine if the element is "active" (default: {})
+ # :controller - One or more controller names to check (optional).
+ # :action - One or more action names to check (optional).
+ # :path - A shorthand path, such as 'dashboard#index', to check (optional).
+ # :html_options - Extra options to be passed to the list element (optional).
+ # block - An optional block that will become the contents of the returned
+ # `li` element.
+ #
+ # When both :controller and :action are specified, BOTH must match in order
+ # to be marked as active. When only one is given, either can match.
+ #
+ # Examples
+ #
+ # # Assuming we're on TreeController#show
+ #
+ # # Controller matches, but action doesn't
+ # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" }
+ # # => '<li>Hello</li>'
+ #
+ # # Controller matches
+ # nav_link(controller: [:tree, :refs]) { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Shorthand path
+ # nav_link(path: 'tree#show') { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Supplying custom options for the list element
+ # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
+ # # => '<li class="home active">Hello</li>'
+ #
+ # Returns a list item element String
+ def nav_link(options = {}, &block)
+ if path = options.delete(:path)
+ if path.respond_to?(:each)
+ c = path.map { |p| p.split('#').first }
+ a = path.map { |p| p.split('#').last }
+ else
+ c, a, _ = path.split('#')
+ end
+ else
+ c = options.delete(:controller)
+ a = options.delete(:action)
+ end
+
+ if c && a
+ # When given both options, make sure BOTH are active
+ klass = current_controller?(*c) && current_action?(*a) ? 'active' : ''
+ else
+ # Otherwise check EITHER option
+ klass = current_controller?(*c) || current_action?(*a) ? 'active' : ''
+ end
+
+ # Add our custom class into the html_options, which may or may not exist
+ # and which may or may not already have a :class key
+ o = options.delete(:html_options) || {}
+ o[:class] ||= ''
+ o[:class] += ' ' + klass
+ o[:class].strip!
+
+ if block_given?
+ content_tag(:li, capture(&block), o)
+ else
+ content_tag(:li, nil, o)
+ end
+ end
+
+ # Check if a particular controller is the current one
+ #
+ # args - One or more controller names to check
+ #
+ # Examples
+ #
+ # # On TreeController
+ # current_controller?(:tree) # => true
+ # current_controller?(:commits) # => false
+ # current_controller?(:commits, :tree) # => true
+ def current_controller?(*args)
+ args.any? { |v| v.to_s.downcase == controller.controller_name }
+ end
+
+ # Check if a particular action is the current one
+ #
+ # args - One or more action names to check
+ #
+ # Examples
+ #
+ # # On Projects#new
+ # current_action?(:new) # => true
+ # current_action?(:create) # => false
+ # current_action?(:new, :create) # => true
+ def current_action?(*args)
+ args.any? { |v| v.to_s.downcase == action_name }
+ end
+
+ def date_from_to(from, to)
+ "#{from.to_s(:short)} - #{to.to_s(:short)}"
+ end
+
+ def body_data_page
+ path = controller.controller_path.split('/')
+ namespace = path.first if path.second
+
+ [namespace, controller.controller_name, controller.action_name].compact.join(":")
+ end
+
+ def duration_in_words(finished_at, started_at)
+ if finished_at && started_at
+ interval_in_seconds = finished_at.to_i - started_at.to_i
+ elsif started_at
+ interval_in_seconds = Time.now.to_i - started_at.to_i
+ end
+
+ time_interval_in_words(interval_in_seconds)
+ end
+
+ def time_interval_in_words(interval_in_seconds)
+ minutes = interval_in_seconds / 60
+ seconds = interval_in_seconds - minutes * 60
+
+ if minutes >= 1
+ "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
+ else
+ "#{pluralize(seconds, "second")}"
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb
new file mode 100644
index 00000000000..cdabdad17d2
--- /dev/null
+++ b/app/helpers/ci/builds_helper.rb
@@ -0,0 +1,41 @@
+module Ci
+ module BuildsHelper
+ def build_ref_link build
+ gitlab_ref_link build.project, build.ref
+ end
+
+ def build_compare_link build
+ gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha
+ end
+
+ def build_commit_link build
+ gitlab_commit_link build.project, build.short_sha
+ end
+
+ def build_url(build)
+ ci_project_build_url(build.project, build)
+ end
+
+ def build_status_alert_class(build)
+ if build.success?
+ 'alert-success'
+ elsif build.failed?
+ 'alert-danger'
+ elsif build.canceled?
+ 'alert-disabled'
+ else
+ 'alert-warning'
+ end
+ end
+
+ def build_icon_css_class(build)
+ if build.success?
+ 'fa-circle cgreen'
+ elsif build.failed?
+ 'fa-circle cred'
+ else
+ 'fa-circle light'
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb
new file mode 100644
index 00000000000..0479bc10594
--- /dev/null
+++ b/app/helpers/ci/commits_helper.rb
@@ -0,0 +1,26 @@
+module Ci
+ module CommitsHelper
+ def commit_status_alert_class(commit)
+ return unless commit
+
+ case commit.status
+ when 'success'
+ 'alert-success'
+ when 'failed', 'canceled'
+ 'alert-danger'
+ when 'skipped'
+ 'alert-disabled'
+ else
+ 'alert-warning'
+ end
+ end
+
+ def commit_link(commit)
+ link_to(commit.short_sha, ci_project_ref_commit_path(commit.project, commit.ref, commit.sha))
+ end
+
+ def truncate_first_line(message, length = 50)
+ truncate(message.each_line.first.chomp, length: length) if message
+ end
+ end
+end
diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb
new file mode 100644
index 00000000000..2b89a0ce93e
--- /dev/null
+++ b/app/helpers/ci/gitlab_helper.rb
@@ -0,0 +1,36 @@
+module Ci
+ module GitlabHelper
+ def no_turbolink
+ { :"data-no-turbolink" => "data-no-turbolink" }
+ end
+
+ def gitlab_ref_link project, ref
+ gitlab_url = project.gitlab_url.dup
+ gitlab_url << "/commits/#{ref}"
+ link_to ref, gitlab_url, no_turbolink
+ end
+
+ def gitlab_compare_link project, before, after
+ gitlab_url = project.gitlab_url.dup
+ gitlab_url << "/compare/#{before}...#{after}"
+
+ link_to "#{before}...#{after}", gitlab_url, no_turbolink
+ end
+
+ def gitlab_commit_link project, sha
+ gitlab_url = project.gitlab_url.dup
+ gitlab_url << "/commit/#{sha}"
+ link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink
+ end
+
+ def yaml_web_editor_link(project)
+ commits = project.commits
+
+ if commits.any? && commits.last.push_data[:ci_yaml_file]
+ "#{@project.gitlab_url}/edit/master/.gitlab-ci.yml"
+ else
+ "#{@project.gitlab_url}/new/master"
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/icons_helper.rb b/app/helpers/ci/icons_helper.rb
new file mode 100644
index 00000000000..ecb6ef7be45
--- /dev/null
+++ b/app/helpers/ci/icons_helper.rb
@@ -0,0 +1,11 @@
+module Ci
+ module IconsHelper
+ def boolean_to_icon(value)
+ if value.to_s == "true"
+ content_tag :i, nil, class: 'fa-circle cgreen'
+ else
+ content_tag :i, nil, class: 'fa-power-off clgray'
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb
new file mode 100644
index 00000000000..fd991a4165a
--- /dev/null
+++ b/app/helpers/ci/projects_helper.rb
@@ -0,0 +1,36 @@
+module Ci
+ module ProjectsHelper
+ def ref_tab_class ref = nil
+ 'active' if ref == @ref
+ end
+
+ def success_ratio(success_builds, failed_builds)
+ failed_builds = failed_builds.count(:all)
+ success_builds = success_builds.count(:all)
+
+ return 100 if failed_builds.zero?
+
+ ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
+ ratio.to_i
+ end
+
+ def markdown_badge_code(project, ref)
+ url = status_ci_project_url(project, ref: ref, format: 'png')
+ "[![build status](#{url})](#{ci_project_url(project, ref: ref)})"
+ end
+
+ def html_badge_code(project, ref)
+ url = status_ci_project_url(project, ref: ref, format: 'png')
+ "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>"
+ end
+
+ def project_uses_specific_runner?(project)
+ project.runners.any?
+ end
+
+ def no_runners_for_project?(project)
+ project.runners.blank? &&
+ Ci::Runner.shared.blank?
+ end
+ end
+end
diff --git a/app/helpers/ci/routes_helper.rb b/app/helpers/ci/routes_helper.rb
new file mode 100644
index 00000000000..f22d5023db5
--- /dev/null
+++ b/app/helpers/ci/routes_helper.rb
@@ -0,0 +1,29 @@
+module Ci
+ module RoutesHelper
+ class Base
+ include Gitlab::Application.routes.url_helpers
+
+ def default_url_options
+ {
+ host: Ci::Settings.gitlab_ci['host'],
+ protocol: Ci::Settings.gitlab_ci['https'] ? "https" : "http",
+ port: Ci::Settings.gitlab_ci['port']
+ }
+ end
+ end
+
+ def url_helpers
+ @url_helpers ||= Ci::Base.new
+ end
+
+ def self.method_missing(method, *args, &block)
+ @url_helpers ||= Ci::Base.new
+
+ if @url_helpers.respond_to?(method)
+ @url_helpers.send(method, *args, &block)
+ else
+ super method, *args, &block
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
new file mode 100644
index 00000000000..782208ddfe4
--- /dev/null
+++ b/app/helpers/ci/runners_helper.rb
@@ -0,0 +1,22 @@
+module Ci
+ module RunnersHelper
+ def runner_status_icon(runner)
+ unless runner.contacted_at
+ return content_tag :i, nil,
+ class: "fa-warning-sign",
+ title: "New runner. Has not connected yet"
+ end
+
+ status =
+ if runner.active?
+ runner.contacted_at > 3.hour.ago ? :online : :offline
+ else
+ :paused
+ end
+
+ content_tag :i, nil,
+ class: "fa-circle runner-status-#{status}",
+ title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
+ end
+ end
+end
diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb
new file mode 100644
index 00000000000..caff54c3520
--- /dev/null
+++ b/app/helpers/ci/triggers_helper.rb
@@ -0,0 +1,7 @@
+module Ci
+ module TriggersHelper
+ def build_trigger_url(project_id, ref_name)
+ "#{Ci::Settings.gitlab_ci.url}/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
+ end
+ end
+end
diff --git a/app/helpers/ci/user_helper.rb b/app/helpers/ci/user_helper.rb
new file mode 100644
index 00000000000..c332d6ed9cf
--- /dev/null
+++ b/app/helpers/ci/user_helper.rb
@@ -0,0 +1,15 @@
+module Ci
+ module UserHelper
+ def user_avatar_url(user = nil, size = nil, default = 'identicon')
+ size = 40 if size.nil? || size <= 0
+
+ if user.blank? || user.avatar_url.blank?
+ 'ci/no_avatar.png'
+ elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url
+ Regexp.last_match[0] + "?s=#{size}&d=#{default}"
+ else
+ user.avatar_url
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/user_sessions_helper.rb b/app/helpers/ci/user_sessions_helper.rb
new file mode 100644
index 00000000000..0296a74395c
--- /dev/null
+++ b/app/helpers/ci/user_sessions_helper.rb
@@ -0,0 +1,32 @@
+module Ci
+ module UserSessionsHelper
+ def generate_oauth_salt
+ SecureRandom.hex(16)
+ end
+
+ def generate_oauth_hmac(salt, return_to)
+ return unless return_to
+ digest = OpenSSL::Digest.new('sha256')
+ key = Gitlab::Application.secrets.db_key_base + salt
+ OpenSSL::HMAC.hexdigest(digest, key, return_to)
+ end
+
+ def generate_oauth_state(return_to)
+ return unless return_to
+ salt = generate_oauth_salt
+ hmac = generate_oauth_hmac(salt, return_to)
+ "#{salt}:#{hmac}:#{return_to}"
+ end
+
+ def get_ouath_state_return_to(state)
+ state.split(':', 3)[2] if state
+ end
+
+ def is_oauth_state_valid?(state)
+ return true unless state
+ salt, hmac, return_to = state.split(':', 3)
+ return false unless return_to
+ hmac == generate_oauth_hmac(salt, return_to)
+ end
+ end
+end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
deleted file mode 100644
index d13d80be293..00000000000
--- a/app/helpers/commits_helper.rb
+++ /dev/null
@@ -1,183 +0,0 @@
-# encoding: utf-8
-module CommitsHelper
- # Returns a link to the commit author. If the author has a matching user and
- # is a member of the current @project it will link to the team member page.
- # Otherwise it will link to the author email as specified in the commit.
- #
- # options:
- # avatar: true will prepend the avatar image
- # size: size of the avatar image in px
- def commit_author_link(commit, options = {})
- commit_person_link(commit, options.merge(source: :author))
- end
-
- # Just like #author_link but for the committer.
- def commit_committer_link(commit, options = {})
- commit_person_link(commit, options.merge(source: :committer))
- end
-
- def image_diff_class(diff)
- if diff.deleted_file
- "deleted"
- elsif diff.new_file
- "added"
- else
- nil
- end
- end
-
- def commit_to_html(commit, project, inline = true)
- template = inline ? "inline_commit" : "commit"
- escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil?
- end
-
- # Breadcrumb links for a Project and, if applicable, a tree path
- def commits_breadcrumbs
- return unless @project && @ref
-
- # Add the root project link and the arrow icon
- crumbs = content_tag(:li) do
- link_to(
- @project.path,
- namespace_project_commits_path(@project.namespace, @project, @ref)
- )
- end
-
- if @path
- parts = @path.split('/')
-
- parts.each_with_index do |part, i|
- crumbs << content_tag(:li) do
- # The text is just the individual part, but the link needs all the parts before it
- link_to(
- part,
- namespace_project_commits_path(
- @project.namespace,
- @project,
- tree_join(@ref, parts[0..i].join('/'))
- )
- )
- end
- end
- end
-
- crumbs.html_safe
- end
-
- # Return Project default branch, if it present in array
- # Else - first branch in array (mb last actual branch)
- def commit_default_branch(project, branches)
- branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
- end
-
- # Returns the sorted alphabetically links to branches, separated by a comma
- def commit_branches_links(project, branches)
- branches.sort.map do |branch|
- link_to(
- namespace_project_tree_path(project.namespace, project, branch)
- ) do
- content_tag :span, class: 'label label-gray' do
- icon('code-fork') + ' ' + branch
- end
- end
- end.join(" ").html_safe
- end
-
- # Returns the sorted links to tags, separated by a comma
- def commit_tags_links(project, tags)
- sorted = VersionSorter.rsort(tags)
- sorted.map do |tag|
- link_to(
- namespace_project_commits_path(project.namespace, project,
- project.repository.find_tag(tag).name)
- ) do
- content_tag :span, class: 'label label-gray' do
- icon('tag') + ' ' + tag
- end
- end
- end.join(" ").html_safe
- end
-
- def link_to_browse_code(project, commit)
- if current_controller?(:projects, :commits)
- if @repo.blob_at(commit.id, @path)
- return link_to(
- "Browse File »",
- namespace_project_blob_path(project.namespace, project,
- tree_join(commit.id, @path)),
- class: "pull-right"
- )
- elsif @path.present?
- return link_to(
- "Browse Dir »",
- namespace_project_tree_path(project.namespace, project,
- tree_join(commit.id, @path)),
- class: "pull-right"
- )
- end
- end
- link_to(
- "Browse Code »",
- namespace_project_tree_path(project.namespace, project, commit),
- class: "pull-right"
- )
- end
-
- protected
-
- # Private: Returns a link to a person. If the person has a matching user and
- # is a member of the current @project it will link to the team member page.
- # Otherwise it will link to the person email as specified in the commit.
- #
- # options:
- # source: one of :author or :committer
- # avatar: true will prepend the avatar image
- # size: size of the avatar image in px
- def commit_person_link(commit, options = {})
- user = commit.send(options[:source])
-
- source_name = clean(commit.send "#{options[:source]}_name".to_sym)
- source_email = clean(commit.send "#{options[:source]}_email".to_sym)
-
- person_name = user.try(:name) || source_name
- person_email = user.try(:email) || source_email
-
- text =
- if options[:avatar]
- avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
- %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
- else
- person_name
- end
-
- options = {
- class: "commit-#{options[:source]}-link has_tooltip",
- data: { :'original-title' => sanitize(source_email) }
- }
-
- if user.nil?
- mail_to(source_email, text.html_safe, options)
- else
- link_to(text.html_safe, user_path(user), options)
- end
- end
-
- def view_file_btn(commit_sha, diff, project)
- link_to(
- namespace_project_blob_path(project.namespace, project,
- tree_join(commit_sha, diff.new_path)),
- class: 'btn btn-small view-file js-view-file'
- ) do
- raw('View file @') + content_tag(:span, commit_sha[0..6],
- class: 'commit-short-id')
- end
- end
-
- def truncate_sha(sha)
- Commit.truncate_sha(sha)
- end
-
- def clean(string)
- Sanitize.clean(string, remove_contents: true)
- end
-end
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
deleted file mode 100644
index f1dc906cab4..00000000000
--- a/app/helpers/compare_helper.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module CompareHelper
- def create_mr_button?(from = params[:from], to = params[:to], project = @project)
- from.present? &&
- to.present? &&
- from != to &&
- project.merge_requests_enabled &&
- project.repository.branch_names.include?(from) &&
- project.repository.branch_names.include?(to)
- end
-
- def create_mr_path(from = params[:from], to = params[:to], project = @project)
- new_namespace_project_merge_request_path(
- project.namespace,
- project,
- merge_request: {
- source_branch: to,
- target_branch: from
- }
- )
- end
-end
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
deleted file mode 100644
index c25b54eadc6..00000000000
--- a/app/helpers/dashboard_helper.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module DashboardHelper
- def assigned_issues_dashboard_path
- issues_dashboard_path(assignee_id: current_user.id)
- end
-
- def assigned_mrs_dashboard_path
- merge_requests_dashboard_path(assignee_id: current_user.id)
- end
-end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
deleted file mode 100644
index 1bd3ec5e0e0..00000000000
--- a/app/helpers/diff_helper.rb
+++ /dev/null
@@ -1,170 +0,0 @@
-module DiffHelper
- def allowed_diff_size
- if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_FILES
- else
- Commit::DIFF_SAFE_FILES
- end
- end
-
- def allowed_diff_lines
- if diff_hard_limit_enabled?
- Commit::DIFF_HARD_LIMIT_LINES
- else
- Commit::DIFF_SAFE_LINES
- end
- end
-
- def safe_diff_files(diffs)
- lines = 0
- safe_files = []
- diffs.first(allowed_diff_size).each do |diff|
- lines += diff.diff.lines.count
- break if lines > allowed_diff_lines
- safe_files << Gitlab::Diff::File.new(diff)
- end
- safe_files
- end
-
- def diff_hard_limit_enabled?
- # Enabling hard limit allows user to see more diff information
- if params[:force_show_diff].present?
- true
- else
- false
- end
- end
-
- def generate_line_code(file_path, line)
- Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
- end
-
- def parallel_diff(diff_file, index)
- lines = []
- skip_next = false
-
- # Building array of lines
- #
- # [
- # left_type, left_line_number, left_line_content, left_line_code,
- # right_line_type, right_line_number, right_line_content, right_line_code
- # ]
- #
- diff_file.diff_lines.each do |line|
-
- full_line = line.text
- type = line.type
- line_code = generate_line_code(diff_file.file_path, line)
- line_new = line.new_pos
- line_old = line.old_pos
-
- next_line = diff_file.next_line(line.index)
-
- if next_line
- next_line_code = generate_line_code(diff_file.file_path, next_line)
- next_type = next_line.type
- next_line = next_line.text
- end
-
- if type == 'match' || type.nil?
- # line in the right panel is the same as in the left one
- line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code]
- lines.push(line)
- elsif type == 'old'
- if next_type == 'new'
- # Left side has text removed, right side has text added
- line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code]
- lines.push(line)
- skip_next = true
- elsif next_type == 'old' || next_type.nil?
- # Left side has text removed, right side doesn't have any change
- # No next line code, no new line number, no new line text
- line = [type, line_old, full_line, line_code, next_type, nil, "&nbsp;", nil]
- lines.push(line)
- end
- elsif type == 'new'
- if skip_next
- # Change has been already included in previous line so no need to do it again
- skip_next = false
- next
- else
- # Change is only on the right side, left side has no change
- line = [nil, nil, "&nbsp;", line_code, type, line_new, full_line, line_code]
- lines.push(line)
- end
- end
- end
- lines
- end
-
- def unfold_bottom_class(bottom)
- (bottom) ? 'js-unfold-bottom' : ''
- end
-
- def unfold_class(unfold)
- (unfold) ? 'unfold js-unfold' : ''
- end
-
- def diff_line_content(line)
- if line.blank?
- " &nbsp;"
- else
- line
- end
- end
-
- def line_comments
- @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
- end
-
- def organize_comments(type_left, type_right, line_code_left, line_code_right)
- comments_left = comments_right = nil
-
- unless type_left.nil? && type_right == 'new'
- comments_left = line_comments[line_code_left]
- end
-
- unless type_left.nil? && type_right.nil?
- comments_right = line_comments[line_code_right]
- end
-
- [comments_left, comments_right]
- end
-
- def inline_diff_btn
- params_copy = params.dup
- params_copy[:view] = 'inline'
- # Always use HTML to handle case where JSON diff rendered this button
- params_copy.delete(:format)
-
- link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
- 'Inline'
- end
- end
-
- def parallel_diff_btn
- params_copy = params.dup
- params_copy[:view] = 'parallel'
- # Always use HTML to handle case where JSON diff rendered this button
- params_copy.delete(:format)
-
- link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
- 'Side-by-side'
- end
- end
-
- def submodule_link(blob, ref, repository = @repository)
- tree, commit = submodule_links(blob, ref, repository)
- commit_id = if commit.nil?
- blob.id[0..10]
- else
- link_to "#{blob.id[0..10]}", commit
- end
-
- [
- content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
- '@',
- content_tag(:span, commit_id, class: 'monospace'),
- ].join(' ').html_safe
- end
-end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
deleted file mode 100644
index 45788ba95ac..00000000000
--- a/app/helpers/emails_helper.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-module EmailsHelper
-
- # Google Actions
- # https://developers.google.com/gmail/markup/reference/go-to-action
- def email_action(url)
- name = action_title(url)
- if name
- data = {
- "@context" => "http://schema.org",
- "@type" => "EmailMessage",
- "action" => {
- "@type" => "ViewAction",
- "name" => name,
- "url" => url,
- }
- }
-
- content_tag :script, type: 'application/ld+json' do
- data.to_json.html_safe
- end
- end
- end
-
- def action_title(url)
- return unless url
- ["merge_requests", "issues", "commit"].each do |action|
- if url.split("/").include?(action)
- return "View #{action.humanize.singularize}"
- end
- end
- end
-
- def color_email_diff(diffcontent)
- formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
- lexer = Rouge::Lexers::Diff
- raw formatter.format(lexer.lex(diffcontent))
- end
-
- def password_reset_token_valid_time
- valid_hours = Devise.reset_password_within / 60 / 60
- if valid_hours >= 24
- unit = 'day'
- valid_length = (valid_hours / 24).floor
- else
- unit = 'hour'
- valid_length = valid_hours.floor
- end
-
- pluralize(valid_length, unit)
- end
-
- def reset_token_expire_message
- link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email))
- msg = "This link is valid for #{password_reset_token_valid_time}. "
- msg << "After it expires, you can #{link_tag}."
- end
-end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
deleted file mode 100644
index 8428281f8f6..00000000000
--- a/app/helpers/events_helper.rb
+++ /dev/null
@@ -1,203 +0,0 @@
-module EventsHelper
- def link_to_author(event)
- author = event.author
-
- if author
- link_to author.name, user_path(author.username)
- else
- event.author_name
- end
- end
-
- def event_action_name(event)
- target = if event.target_type
- if event.note?
- event.note_target_type
- else
- event.target_type.titleize.downcase
- end
- else
- 'project'
- end
-
- [event.action_name, target].join(" ")
- end
-
- def event_filter_link(key, tooltip)
- key = key.to_s
- active = 'active' if @event_filter.active?(key)
- link_opts = {
- class: 'event_filter_link',
- id: "#{key}_event_filter",
- title: "Filter by #{tooltip.downcase}",
- data: { toggle: 'tooltip', placement: 'top' }
- }
-
- content_tag :li, class: "filter_icon #{active}" do
- link_to request.path, link_opts do
- icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
- end
- end
- end
-
- def icon_for_event
- {
- EventFilter.push => 'upload',
- EventFilter.merged => 'check-square-o',
- EventFilter.comments => 'comments',
- EventFilter.team => 'user',
- }
- end
-
- def event_feed_title(event)
- words = []
- words << event.author_name
- words << event_action_name(event)
-
- if event.push?
- words << event.ref_type
- words << event.ref_name
- words << "at"
- elsif event.commented?
- if event.note_commit?
- words << event.note_short_commit_id
- else
- words << "##{truncate event.note_target_iid}"
- end
- words << "at"
- elsif event.target
- words << "##{event.target_iid}:"
- words << event.target.title if event.target.respond_to?(:title)
- words << "at"
- end
-
- words << event.project_name
-
- words.join(" ")
- end
-
- def event_feed_url(event)
- if event.issue?
- namespace_project_issue_url(event.project.namespace, event.project,
- event.issue)
- elsif event.merge_request?
- namespace_project_merge_request_url(event.project.namespace,
- event.project, event.merge_request)
- elsif event.note? && event.note_commit?
- namespace_project_commit_url(event.project.namespace, event.project,
- event.note_target)
- elsif event.note?
- if event.note_target
- if event.note_commit?
- namespace_project_commit_path(event.project.namespace, event.project,
- event.note_commit_id,
- anchor: dom_id(event.target))
- elsif event.note_project_snippet?
- namespace_project_snippet_path(event.project.namespace,
- event.project, event.note_target)
- else
- event_note_target_path(event)
- end
- end
- elsif event.push?
- if event.push_with_commits? && event.md_ref?
- if event.commits_count > 1
- namespace_project_compare_url(event.project.namespace, event.project,
- from: event.commit_from, to:
- event.commit_to)
- else
- namespace_project_commit_url(event.project.namespace, event.project,
- id: event.commit_to)
- end
- else
- namespace_project_commits_url(event.project.namespace, event.project,
- event.ref_name)
- end
- end
- end
-
- def event_feed_summary(event)
- if event.issue?
- render "events/event_issue", issue: event.issue
- elsif event.push?
- render "events/event_push", event: event
- elsif event.merge_request?
- render "events/event_merge_request", merge_request: event.merge_request
- elsif event.note?
- render "events/event_note", note: event.note
- end
- end
-
- def event_note_target_path(event)
- if event.note? && event.note_commit?
- namespace_project_commit_path(event.project.namespace, event.project,
- event.note_target)
- else
- polymorphic_path([event.project.namespace.becomes(Namespace),
- event.project, event.note_target],
- anchor: dom_id(event.target))
- end
- end
-
- def event_note_title_html(event)
- if event.note_target
- if event.note_commit?
- link_to(
- namespace_project_commit_path(event.project.namespace, event.project,
- event.note_commit_id,
- anchor: dom_id(event.target)),
- class: "commit_short_id"
- ) do
- "#{event.note_target_type} #{event.note_short_commit_id}"
- end
- elsif event.note_project_snippet?
- link_to(namespace_project_snippet_path(event.project.namespace,
- event.project,
- event.note_target)) do
- "#{event.note_target_type} ##{truncate event.note_target_id}"
- end
- else
- link_to event_note_target_path(event) do
- "#{event.note_target_type} ##{truncate event.note_target_iid}"
- end
- end
- else
- content_tag :strong do
- "(deleted)"
- end
- end
- end
-
- def event_note(text, options = {})
- text = first_line_in_markdown(text, 150, options)
- sanitize(text, tags: %w(a img b pre code p span))
- end
-
- def event_commit_title(message)
- escape_once(truncate(message.split("\n").first, length: 70))
- rescue
- "--broken encoding"
- end
-
- def event_to_atom(xml, event)
- if event.proper?
- xml.entry do
- event_link = event_feed_url(event)
- event_title = event_feed_title(event)
- event_summary = event_feed_summary(event)
-
- xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
- xml.link href: event_link
- xml.title truncate(event_title, length: 80)
- xml.updated event.created_at.xmlschema
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email)
- xml.author do |author|
- xml.name event.author_name
- xml.email event.author_email
- end
-
- xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? }
- end
- end
- end
-end
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
deleted file mode 100644
index 0d291f9a87e..00000000000
--- a/app/helpers/explore_helper.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module ExploreHelper
- def explore_projects_filter_path(options={})
- exist_opts = {
- sort: params[:sort],
- scope: params[:scope],
- group: params[:group],
- tag: params[:tag],
- visibility_level: params[:visibility_level],
- }
-
- options = exist_opts.merge(options)
-
- path = explore_projects_path
- path << "?#{options.to_param}"
- path
- end
-end
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
deleted file mode 100644
index 838b85afdfe..00000000000
--- a/app/helpers/external_wiki_helper.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ExternalWikiHelper
- def get_project_wiki_path(project)
- external_wiki_service = project.services.
- select { |service| service.to_param == 'external_wiki' }.first
- if external_wiki_service.present? && external_wiki_service.active?
- external_wiki_service.properties['external_wiki_url']
- else
- namespace_project_wiki_path(project.namespace, project, :home)
- end
- end
-end
diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb
deleted file mode 100644
index 09684955233..00000000000
--- a/app/helpers/git_helper.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module GitHelper
- def strip_gpg_signature(text)
- text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
- end
-end
diff --git a/app/helpers/gitlab/appearances_helper.rb b/app/helpers/gitlab/appearances_helper.rb
new file mode 100644
index 00000000000..54cafcd9e40
--- /dev/null
+++ b/app/helpers/gitlab/appearances_helper.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module AppearancesHelper
+ def brand_item
+ nil
+ end
+
+ def brand_title
+ 'GitLab Community Edition'
+ end
+
+ def brand_image
+ nil
+ end
+
+ def brand_text
+ nil
+ end
+
+ def brand_header_logo
+ image_tag 'logo.svg'
+ end
+ end
+end
diff --git a/app/helpers/gitlab/application_helper.rb b/app/helpers/gitlab/application_helper.rb
new file mode 100644
index 00000000000..b019ffa5fe2
--- /dev/null
+++ b/app/helpers/gitlab/application_helper.rb
@@ -0,0 +1,317 @@
+require 'digest/md5'
+require 'uri'
+
+module Gitlab
+ module ApplicationHelper
+ # Check if a particular controller is the current one
+ #
+ # args - One or more controller names to check
+ #
+ # Examples
+ #
+ # # On TreeController
+ # current_controller?(:tree) # => true
+ # current_controller?(:commits) # => false
+ # current_controller?(:commits, :tree) # => true
+ def current_controller?(*args)
+ args.any? { |v| v.to_s.downcase == controller.controller_name }
+ end
+
+ # Check if a particular action is the current one
+ #
+ # args - One or more action names to check
+ #
+ # Examples
+ #
+ # # On Projects#new
+ # current_action?(:new) # => true
+ # current_action?(:create) # => false
+ # current_action?(:new, :create) # => true
+ def current_action?(*args)
+ args.any? { |v| v.to_s.downcase == action_name }
+ end
+
+ def project_icon(project_id, options = {})
+ project =
+ if project_id.is_a?(Project)
+ project = project_id
+ else
+ Project.find_with_namespace(project_id)
+ end
+
+ if project.avatar_url
+ image_tag project.avatar_url, options
+ else # generated icon
+ project_identicon(project, options)
+ end
+ end
+
+ def project_identicon(project, options = {})
+ allowed_colors = {
+ red: 'FFEBEE',
+ purple: 'F3E5F5',
+ indigo: 'E8EAF6',
+ blue: 'E3F2FD',
+ teal: 'E0F2F1',
+ orange: 'FBE9E7',
+ gray: 'EEEEEE'
+ }
+
+ options[:class] ||= ''
+ options[:class] << ' identicon'
+ bg_key = project.id % 7
+ style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555"
+
+ content_tag(:div, class: options[:class], style: style) do
+ project.name[0, 1].upcase
+ end
+ end
+
+ def avatar_icon(user_email = '', size = nil)
+ user = User.find_by(email: user_email)
+
+ if user
+ user.avatar_url(size) || default_avatar
+ else
+ gravatar_icon(user_email, size)
+ end
+ end
+
+ def gravatar_icon(user_email = '', size = nil)
+ GravatarService.new.execute(user_email, size) ||
+ default_avatar
+ end
+
+ def default_avatar
+ image_path('no_avatar.png')
+ end
+
+ def last_commit(project)
+ if project.repo_exists?
+ time_ago_with_tooltip(project.repository.commit.committed_date)
+ else
+ 'Never'
+ end
+ rescue
+ 'Never'
+ end
+
+ def grouped_options_refs
+ repository = @project.repository
+
+ options = [
+ ['Branches', repository.branch_names],
+ ['Tags', VersionSorter.rsort(repository.tag_names)]
+ ]
+
+ # If reference is commit id - we should add it to branch/tag selectbox
+ if(@ref && !options.flatten.include?(@ref) &&
+ @ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
+ options << ['Commit', [@ref]]
+ end
+
+ grouped_options_for_select(options, @ref || @project.default_branch)
+ end
+
+ def emoji_autocomplete_source
+ # should be an array of strings
+ # so to_s can be called, because it is sufficient and to_json is too slow
+ Emoji.names.to_s
+ end
+
+ # Define whenever show last push event
+ # with suggestion to create MR
+ def show_last_push_widget?(event)
+ # Skip if event is not about added or modified non-master branch
+ return false unless event && event.last_push_to_non_root? && !event.rm_ref?
+
+ project = event.project
+
+ # Skip if project repo is empty or MR disabled
+ return false unless project && !project.empty_repo? && project.merge_requests_enabled
+
+ # Skip if user already created appropriate MR
+ return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
+
+ # Skip if user removed branch right after that
+ return false unless project.repository.branch_names.include?(event.branch_name)
+
+ true
+ end
+
+ def hexdigest(string)
+ Digest::SHA1.hexdigest string
+ end
+
+ def simple_sanitize(str)
+ sanitize(str, tags: %w(a span))
+ end
+
+ def body_data_page
+ path = controller.controller_path.split('/')
+ namespace = path.first if path.second
+
+ [namespace, controller.controller_name, controller.action_name].compact.join(':')
+ end
+
+ # shortcut for gitlab config
+ def gitlab_config
+ Gitlab.config.gitlab
+ end
+
+ # shortcut for gitlab extra config
+ def extra_config
+ Gitlab.config.extra
+ end
+
+ def search_placeholder
+ if @project && @project.persisted?
+ 'Search in this project'
+ elsif @snippet || @snippets || @show_snippets
+ 'Search snippets'
+ elsif @group && @group.persisted?
+ 'Search in this group'
+ else
+ 'Search'
+ end
+ end
+
+ def broadcast_message
+ BroadcastMessage.current
+ end
+
+ # Render a `time` element with Javascript-based relative date and tooltip
+ #
+ # time - Time object
+ # placement - Tooltip placement String (default: "top")
+ # html_class - Custom class for `time` element (default: "time_ago")
+ # skip_js - When true, exclude the `script` tag (default: false)
+ #
+ # By default also includes a `script` element with Javascript necessary to
+ # initialize the `timeago` jQuery extension. If this method is called many
+ # times, for example rendering hundreds of commits, it's advisable to disable
+ # this behavior using the `skip_js` argument and re-initializing `timeago`
+ # manually once all of the elements have been rendered.
+ #
+ # A `js-timeago` class is always added to the element, even when a custom
+ # `html_class` argument is provided.
+ #
+ # Returns an HTML-safe String
+ def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
+ element = content_tag :time, time.to_s,
+ class: "#{html_class} js-timeago",
+ datetime: time.getutc.iso8601,
+ title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
+ data: { toggle: 'tooltip', placement: placement }
+
+ element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
+
+ element
+ end
+
+ def render_markup(file_name, file_content)
+ if gitlab_markdown?(file_name)
+ Haml::Helpers.preserve(markdown(file_content))
+ elsif asciidoc?(file_name)
+ asciidoc(file_content)
+ elsif plain?(file_name)
+ content_tag :pre, class: 'plain-readme' do
+ file_content
+ end
+ else
+ GitHub::Markup.render(file_name, file_content).
+ force_encoding(file_content.encoding).html_safe
+ end
+ rescue RuntimeError
+ simple_format(file_content)
+ end
+
+ def plain?(filename)
+ Gitlab::MarkupHelper.plain?(filename)
+ end
+
+ def markup?(filename)
+ Gitlab::MarkupHelper.markup?(filename)
+ end
+
+ def gitlab_markdown?(filename)
+ Gitlab::MarkupHelper.gitlab_markdown?(filename)
+ end
+
+ def asciidoc?(filename)
+ Gitlab::MarkupHelper.asciidoc?(filename)
+ end
+
+ def promo_host
+ 'about.gitlab.com'
+ end
+
+ def promo_url
+ 'https://' + promo_host
+ end
+
+ def page_filter_path(options = {})
+ without = options.delete(:without)
+
+ exist_opts = {
+ state: params[:state],
+ scope: params[:scope],
+ label_name: params[:label_name],
+ milestone_id: params[:milestone_id],
+ assignee_id: params[:assignee_id],
+ author_id: params[:author_id],
+ sort: params[:sort],
+ }
+
+ options = exist_opts.merge(options)
+
+ if without.present?
+ without.each do |key|
+ options.delete(key)
+ end
+ end
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
+
+ def outdated_browser?
+ browser.ie? && browser.version.to_i < 10
+ end
+
+ def path_to_key(key, admin = false)
+ if admin
+ admin_user_key_path(@user, key)
+ else
+ profile_key_path(key)
+ end
+ end
+
+ def state_filters_text_for(entity, project)
+ titles = {
+ opened: "Open"
+ }
+
+ entity_title = titles[entity] || entity.to_s.humanize
+
+ count =
+ if project.nil?
+ nil
+ elsif current_controller?(:issues)
+ project.issues.send(entity).count
+ elsif current_controller?(:merge_requests)
+ project.merge_requests.send(entity).count
+ end
+
+ html = content_tag :span, entity_title
+
+ if count.present?
+ html += " "
+ html += content_tag :span, number_with_delimiter(count), class: 'badge'
+ end
+
+ html.html_safe
+ end
+ end
+end
diff --git a/app/helpers/gitlab/application_settings_helper.rb b/app/helpers/gitlab/application_settings_helper.rb
new file mode 100644
index 00000000000..7132d3dcdad
--- /dev/null
+++ b/app/helpers/gitlab/application_settings_helper.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ApplicationSettingsHelper
+ def gravatar_enabled?
+ current_application_settings.gravatar_enabled?
+ end
+
+ def twitter_sharing_enabled?
+ current_application_settings.twitter_sharing_enabled?
+ end
+
+ def signup_enabled?
+ current_application_settings.signup_enabled?
+ end
+
+ def signin_enabled?
+ current_application_settings.signin_enabled?
+ end
+
+ def extra_sign_in_text
+ current_application_settings.sign_in_text
+ end
+
+ def user_oauth_applications?
+ current_application_settings.user_oauth_applications
+ end
+
+ # Return a group of checkboxes that use Bootstrap's button plugin for a
+ # toggle button effect.
+ def restricted_level_checkboxes(help_block_id)
+ Gitlab::VisibilityLevel.options.map do |name, level|
+ checked = restricted_visibility_levels(true).include?(level)
+ css_class = 'btn'
+ css_class += ' active' if checked
+ checkbox_name = 'application_setting[restricted_visibility_levels][]'
+
+ label_tag(checkbox_name, class: css_class) do
+ check_box_tag(checkbox_name, level, checked,
+ autocomplete: 'off',
+ 'aria-describedby' => help_block_id) + name
+ end
+ end
+ end
+
+ # Return a group of checkboxes that use Bootstrap's button plugin for a
+ # toggle button effect.
+ def import_sources_checkboxes(help_block_id)
+ Gitlab::ImportSources.options.map do |name, source|
+ checked = current_application_settings.import_sources.include?(source)
+ css_class = 'btn'
+ css_class += ' active' if checked
+ checkbox_name = 'application_setting[import_sources][]'
+
+ label_tag(checkbox_name, class: css_class) do
+ check_box_tag(checkbox_name, source, checked,
+ autocomplete: 'off',
+ 'aria-describedby' => help_block_id) + name
+ end
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/auth_helper.rb b/app/helpers/gitlab/auth_helper.rb
new file mode 100644
index 00000000000..fbd52dbca3d
--- /dev/null
+++ b/app/helpers/gitlab/auth_helper.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module AuthHelper
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
+ FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze
+
+ def ldap_enabled?
+ Gitlab.config.ldap.enabled
+ end
+
+ def provider_has_icon?(name)
+ PROVIDERS_WITH_ICONS.include?(name.to_s)
+ end
+
+ def auth_providers
+ Gitlab::OAuth::Provider.providers
+ end
+
+ def label_for_provider(name)
+ Gitlab::OAuth::Provider.label_for(name)
+ end
+
+ def form_based_provider?(name)
+ FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
+ end
+
+ def form_based_providers
+ auth_providers.select { |provider| form_based_provider?(provider) }
+ end
+
+ def button_based_providers
+ auth_providers.reject { |provider| form_based_provider?(provider) }
+ end
+
+ def provider_image_tag(provider, size = 64)
+ label = label_for_provider(provider)
+
+ if provider_has_icon?(provider)
+ file_name = "#{provider.to_s.split('_').first}_#{size}.png"
+
+ image_tag("auth_buttons/#{file_name}", alt: label, title: "Sign in with #{label}")
+ else
+ label
+ end
+ end
+
+ def auth_active?(provider)
+ current_user.identities.exists?(provider: provider.to_s)
+ end
+
+ extend self
+ end
+end
diff --git a/app/helpers/gitlab/blob_helper.rb b/app/helpers/gitlab/blob_helper.rb
new file mode 100644
index 00000000000..8b53ba8b54f
--- /dev/null
+++ b/app/helpers/gitlab/blob_helper.rb
@@ -0,0 +1,76 @@
+module Gitlab
+ module BlobHelper
+ def highlight(blob_name, blob_content, nowrap: false, continue: false)
+ @formatter ||= Rouge::Formatters::HTMLGitlab.new(
+ nowrap: nowrap,
+ cssclass: 'code highlight',
+ lineanchors: true,
+ lineanchorsid: 'LC'
+ )
+
+ begin
+ @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
+ result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe
+ rescue
+ @lexer = Rouge::Lexers::PlainText
+ result = @formatter.format(@lexer.lex(blob_content)).html_safe
+ end
+
+ result
+ end
+
+ def no_highlight_files
+ %w(credits changelog news copying copyright license authors)
+ end
+
+ def edit_blob_link(project, ref, path, options = {})
+ blob =
+ begin
+ project.repository.blob_at(ref, path)
+ rescue
+ nil
+ end
+
+ if blob && blob.text?
+ text = 'Edit'
+ after = options[:after] || ''
+ from_mr = options[:from_merge_request_id]
+ link_opts = {}
+ link_opts[:from_merge_request_id] = from_mr if from_mr
+ cls = 'btn btn-small'
+ if allowed_tree_edit?(project, ref)
+ link_to(text,
+ namespace_project_edit_blob_path(project.namespace, project,
+ tree_join(ref, path),
+ link_opts),
+ class: cls
+ )
+ else
+ content_tag :span, text, class: cls + ' disabled'
+ end + after.html_safe
+ else
+ ''
+ end
+ end
+
+ def leave_edit_message
+ "Leave edit mode?\nAll unsaved changes will be lost."
+ end
+
+ def editing_preview_title(filename)
+ if Gitlab::MarkupHelper.previewable?(filename)
+ 'Preview'
+ else
+ 'Preview changes'
+ end
+ end
+
+ # Return an image icon depending on the file mode and extension
+ #
+ # mode - File unix mode
+ # mode - File name
+ def blob_icon(mode, name)
+ icon("#{file_type_icon_class('file', mode, name)} fw")
+ end
+ end
+end
diff --git a/app/helpers/gitlab/branches_helper.rb b/app/helpers/gitlab/branches_helper.rb
new file mode 100644
index 00000000000..ecc56002e84
--- /dev/null
+++ b/app/helpers/gitlab/branches_helper.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module BranchesHelper
+ def can_remove_branch?(project, branch_name)
+ if project.protected_branch? branch_name
+ false
+ elsif branch_name == project.repository.root_ref
+ false
+ else
+ can?(current_user, :push_code, project)
+ end
+ end
+
+ def can_push_branch?(project, branch_name)
+ return false unless project.repository.branch_names.include?(branch_name)
+
+ ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
+ end
+ end
+end
diff --git a/app/helpers/gitlab/broadcast_messages_helper.rb b/app/helpers/gitlab/broadcast_messages_helper.rb
new file mode 100644
index 00000000000..93f0b0ec5ae
--- /dev/null
+++ b/app/helpers/gitlab/broadcast_messages_helper.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module BroadcastMessagesHelper
+ def broadcast_styling(broadcast_message)
+ styling = ''
+
+ if broadcast_message.color.present?
+ styling << "background-color: #{broadcast_message.color}"
+ styling << '; ' if broadcast_message.font.present?
+ end
+
+ if broadcast_message.font.present?
+ styling << "color: #{broadcast_message.font}"
+ end
+
+ styling
+ end
+ end
+end
diff --git a/app/helpers/gitlab/commits_helper.rb b/app/helpers/gitlab/commits_helper.rb
new file mode 100644
index 00000000000..8a3de838b39
--- /dev/null
+++ b/app/helpers/gitlab/commits_helper.rb
@@ -0,0 +1,185 @@
+# encoding: utf-8
+module Gitlab
+ module CommitsHelper
+ # Returns a link to the commit author. If the author has a matching user and
+ # is a member of the current @project it will link to the team member page.
+ # Otherwise it will link to the author email as specified in the commit.
+ #
+ # options:
+ # avatar: true will prepend the avatar image
+ # size: size of the avatar image in px
+ def commit_author_link(commit, options = {})
+ commit_person_link(commit, options.merge(source: :author))
+ end
+
+ # Just like #author_link but for the committer.
+ def commit_committer_link(commit, options = {})
+ commit_person_link(commit, options.merge(source: :committer))
+ end
+
+ def image_diff_class(diff)
+ if diff.deleted_file
+ "deleted"
+ elsif diff.new_file
+ "added"
+ else
+ nil
+ end
+ end
+
+ def commit_to_html(commit, project, inline = true)
+ template = inline ? "inline_commit" : "commit"
+ escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil?
+ end
+
+ # Breadcrumb links for a Project and, if applicable, a tree path
+ def commits_breadcrumbs
+ return unless @project && @ref
+
+ # Add the root project link and the arrow icon
+ crumbs = content_tag(:li) do
+ link_to(
+ @project.path,
+ namespace_project_commits_path(@project.namespace, @project, @ref)
+ )
+ end
+
+ if @path
+ parts = @path.split('/')
+
+ parts.each_with_index do |part, i|
+ crumbs << content_tag(:li) do
+ # The text is just the individual part, but the link needs all the parts before it
+ link_to(
+ part,
+ namespace_project_commits_path(
+ @project.namespace,
+ @project,
+ tree_join(@ref, parts[0..i].join('/'))
+ )
+ )
+ end
+ end
+ end
+
+ crumbs.html_safe
+ end
+
+ # Return Project default branch, if it present in array
+ # Else - first branch in array (mb last actual branch)
+ def commit_default_branch(project, branches)
+ branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
+ end
+
+ # Returns the sorted alphabetically links to branches, separated by a comma
+ def commit_branches_links(project, branches)
+ branches.sort.map do |branch|
+ link_to(
+ namespace_project_tree_path(project.namespace, project, branch)
+ ) do
+ content_tag :span, class: 'label label-gray' do
+ icon('code-fork') + ' ' + branch
+ end
+ end
+ end.join(" ").html_safe
+ end
+
+ # Returns the sorted links to tags, separated by a comma
+ def commit_tags_links(project, tags)
+ sorted = VersionSorter.rsort(tags)
+ sorted.map do |tag|
+ link_to(
+ namespace_project_commits_path(project.namespace, project,
+ project.repository.find_tag(tag).name)
+ ) do
+ content_tag :span, class: 'label label-gray' do
+ icon('tag') + ' ' + tag
+ end
+ end
+ end.join(" ").html_safe
+ end
+
+ def link_to_browse_code(project, commit)
+ if current_controller?(:projects, :commits)
+ if @repo.blob_at(commit.id, @path)
+ return link_to(
+ "Browse File »",
+ namespace_project_blob_path(project.namespace, project,
+ tree_join(commit.id, @path)),
+ class: "pull-right"
+ )
+ elsif @path.present?
+ return link_to(
+ "Browse Dir »",
+ namespace_project_tree_path(project.namespace, project,
+ tree_join(commit.id, @path)),
+ class: "pull-right"
+ )
+ end
+ end
+ link_to(
+ "Browse Code »",
+ namespace_project_tree_path(project.namespace, project, commit),
+ class: "pull-right"
+ )
+ end
+
+ protected
+
+ # Private: Returns a link to a person. If the person has a matching user and
+ # is a member of the current @project it will link to the team member page.
+ # Otherwise it will link to the person email as specified in the commit.
+ #
+ # options:
+ # source: one of :author or :committer
+ # avatar: true will prepend the avatar image
+ # size: size of the avatar image in px
+ def commit_person_link(commit, options = {})
+ user = commit.send(options[:source])
+
+ source_name = clean(commit.send "#{options[:source]}_name".to_sym)
+ source_email = clean(commit.send "#{options[:source]}_email".to_sym)
+
+ person_name = user.try(:name) || source_name
+ person_email = user.try(:email) || source_email
+
+ text =
+ if options[:avatar]
+ avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
+ %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
+ else
+ person_name
+ end
+
+ options = {
+ class: "commit-#{options[:source]}-link has_tooltip",
+ data: { :'original-title' => sanitize(source_email) }
+ }
+
+ if user.nil?
+ mail_to(source_email, text.html_safe, options)
+ else
+ link_to(text.html_safe, user_path(user), options)
+ end
+ end
+
+ def view_file_btn(commit_sha, diff, project)
+ link_to(
+ namespace_project_blob_path(project.namespace, project,
+ tree_join(commit_sha, diff.new_path)),
+ class: 'btn btn-small view-file js-view-file'
+ ) do
+ raw('View file @') + content_tag(:span, commit_sha[0..6],
+ class: 'commit-short-id')
+ end
+ end
+
+ def truncate_sha(sha)
+ Commit.truncate_sha(sha)
+ end
+
+ def clean(string)
+ Sanitize.clean(string, remove_contents: true)
+ end
+ end
+end
diff --git a/app/helpers/gitlab/compare_helper.rb b/app/helpers/gitlab/compare_helper.rb
new file mode 100644
index 00000000000..407d25d3102
--- /dev/null
+++ b/app/helpers/gitlab/compare_helper.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module CompareHelper
+ def create_mr_button?(from = params[:from], to = params[:to], project = @project)
+ from.present? &&
+ to.present? &&
+ from != to &&
+ project.merge_requests_enabled &&
+ project.repository.branch_names.include?(from) &&
+ project.repository.branch_names.include?(to)
+ end
+
+ def create_mr_path(from = params[:from], to = params[:to], project = @project)
+ new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_branch: to,
+ target_branch: from
+ }
+ )
+ end
+ end
+end
diff --git a/app/helpers/gitlab/dashboard_helper.rb b/app/helpers/gitlab/dashboard_helper.rb
new file mode 100644
index 00000000000..2211c93999e
--- /dev/null
+++ b/app/helpers/gitlab/dashboard_helper.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module DashboardHelper
+ def assigned_issues_dashboard_path
+ issues_dashboard_path(assignee_id: current_user.id)
+ end
+
+ def assigned_mrs_dashboard_path
+ merge_requests_dashboard_path(assignee_id: current_user.id)
+ end
+ end
+end
diff --git a/app/helpers/gitlab/diff_helper.rb b/app/helpers/gitlab/diff_helper.rb
new file mode 100644
index 00000000000..02907eb80f3
--- /dev/null
+++ b/app/helpers/gitlab/diff_helper.rb
@@ -0,0 +1,172 @@
+module Gitlab
+ module DiffHelper
+ def allowed_diff_size
+ if diff_hard_limit_enabled?
+ Commit::DIFF_HARD_LIMIT_FILES
+ else
+ Commit::DIFF_SAFE_FILES
+ end
+ end
+
+ def allowed_diff_lines
+ if diff_hard_limit_enabled?
+ Commit::DIFF_HARD_LIMIT_LINES
+ else
+ Commit::DIFF_SAFE_LINES
+ end
+ end
+
+ def safe_diff_files(diffs)
+ lines = 0
+ safe_files = []
+ diffs.first(allowed_diff_size).each do |diff|
+ lines += diff.diff.lines.count
+ break if lines > allowed_diff_lines
+ safe_files << Gitlab::Diff::File.new(diff)
+ end
+ safe_files
+ end
+
+ def diff_hard_limit_enabled?
+ # Enabling hard limit allows user to see more diff information
+ if params[:force_show_diff].present?
+ true
+ else
+ false
+ end
+ end
+
+ def generate_line_code(file_path, line)
+ Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
+ end
+
+ def parallel_diff(diff_file, index)
+ lines = []
+ skip_next = false
+
+ # Building array of lines
+ #
+ # [
+ # left_type, left_line_number, left_line_content, left_line_code,
+ # right_line_type, right_line_number, right_line_content, right_line_code
+ # ]
+ #
+ diff_file.diff_lines.each do |line|
+
+ full_line = line.text
+ type = line.type
+ line_code = generate_line_code(diff_file.file_path, line)
+ line_new = line.new_pos
+ line_old = line.old_pos
+
+ next_line = diff_file.next_line(line.index)
+
+ if next_line
+ next_line_code = generate_line_code(diff_file.file_path, next_line)
+ next_type = next_line.type
+ next_line = next_line.text
+ end
+
+ if type == 'match' || type.nil?
+ # line in the right panel is the same as in the left one
+ line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code]
+ lines.push(line)
+ elsif type == 'old'
+ if next_type == 'new'
+ # Left side has text removed, right side has text added
+ line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code]
+ lines.push(line)
+ skip_next = true
+ elsif next_type == 'old' || next_type.nil?
+ # Left side has text removed, right side doesn't have any change
+ # No next line code, no new line number, no new line text
+ line = [type, line_old, full_line, line_code, next_type, nil, "&nbsp;", nil]
+ lines.push(line)
+ end
+ elsif type == 'new'
+ if skip_next
+ # Change has been already included in previous line so no need to do it again
+ skip_next = false
+ next
+ else
+ # Change is only on the right side, left side has no change
+ line = [nil, nil, "&nbsp;", line_code, type, line_new, full_line, line_code]
+ lines.push(line)
+ end
+ end
+ end
+ lines
+ end
+
+ def unfold_bottom_class(bottom)
+ (bottom) ? 'js-unfold-bottom' : ''
+ end
+
+ def unfold_class(unfold)
+ (unfold) ? 'unfold js-unfold' : ''
+ end
+
+ def diff_line_content(line)
+ if line.blank?
+ " &nbsp;"
+ else
+ line
+ end
+ end
+
+ def line_comments
+ @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
+ end
+
+ def organize_comments(type_left, type_right, line_code_left, line_code_right)
+ comments_left = comments_right = nil
+
+ unless type_left.nil? && type_right == 'new'
+ comments_left = line_comments[line_code_left]
+ end
+
+ unless type_left.nil? && type_right.nil?
+ comments_right = line_comments[line_code_right]
+ end
+
+ [comments_left, comments_right]
+ end
+
+ def inline_diff_btn
+ params_copy = params.dup
+ params_copy[:view] = 'inline'
+ # Always use HTML to handle case where JSON diff rendered this button
+ params_copy.delete(:format)
+
+ link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
+ 'Inline'
+ end
+ end
+
+ def parallel_diff_btn
+ params_copy = params.dup
+ params_copy[:view] = 'parallel'
+ # Always use HTML to handle case where JSON diff rendered this button
+ params_copy.delete(:format)
+
+ link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
+ 'Side-by-side'
+ end
+ end
+
+ def submodule_link(blob, ref, repository = @repository)
+ tree, commit = submodule_links(blob, ref, repository)
+ commit_id = if commit.nil?
+ blob.id[0..10]
+ else
+ link_to "#{blob.id[0..10]}", commit
+ end
+
+ [
+ content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
+ '@',
+ content_tag(:span, commit_id, class: 'monospace'),
+ ].join(' ').html_safe
+ end
+ end
+end
diff --git a/app/helpers/gitlab/emails_helper.rb b/app/helpers/gitlab/emails_helper.rb
new file mode 100644
index 00000000000..84f106dd536
--- /dev/null
+++ b/app/helpers/gitlab/emails_helper.rb
@@ -0,0 +1,59 @@
+module Gitlab
+ module EmailsHelper
+
+ # Google Actions
+ # https://developers.google.com/gmail/markup/reference/go-to-action
+ def email_action(url)
+ name = action_title(url)
+ if name
+ data = {
+ "@context" => "http://schema.org",
+ "@type" => "EmailMessage",
+ "action" => {
+ "@type" => "ViewAction",
+ "name" => name,
+ "url" => url,
+ }
+ }
+
+ content_tag :script, type: 'application/ld+json' do
+ data.to_json.html_safe
+ end
+ end
+ end
+
+ def action_title(url)
+ return unless url
+ ["merge_requests", "issues", "commit"].each do |action|
+ if url.split("/").include?(action)
+ return "View #{action.humanize.singularize}"
+ end
+ end
+ end
+
+ def color_email_diff(diffcontent)
+ formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
+ lexer = Rouge::Lexers::Diff
+ raw formatter.format(lexer.lex(diffcontent))
+ end
+
+ def password_reset_token_valid_time
+ valid_hours = Devise.reset_password_within / 60 / 60
+ if valid_hours >= 24
+ unit = 'day'
+ valid_length = (valid_hours / 24).floor
+ else
+ unit = 'hour'
+ valid_length = valid_hours.floor
+ end
+
+ pluralize(valid_length, unit)
+ end
+
+ def reset_token_expire_message
+ link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email))
+ msg = "This link is valid for #{password_reset_token_valid_time}. "
+ msg << "After it expires, you can #{link_tag}."
+ end
+ end
+end
diff --git a/app/helpers/gitlab/events_helper.rb b/app/helpers/gitlab/events_helper.rb
new file mode 100644
index 00000000000..65522dae533
--- /dev/null
+++ b/app/helpers/gitlab/events_helper.rb
@@ -0,0 +1,205 @@
+module Gitlab
+ module EventsHelper
+ def link_to_author(event)
+ author = event.author
+
+ if author
+ link_to author.name, user_path(author.username)
+ else
+ event.author_name
+ end
+ end
+
+ def event_action_name(event)
+ target = if event.target_type
+ if event.note?
+ event.note_target_type
+ else
+ event.target_type.titleize.downcase
+ end
+ else
+ 'project'
+ end
+
+ [event.action_name, target].join(" ")
+ end
+
+ def event_filter_link(key, tooltip)
+ key = key.to_s
+ active = 'active' if @event_filter.active?(key)
+ link_opts = {
+ class: 'event_filter_link',
+ id: "#{key}_event_filter",
+ title: "Filter by #{tooltip.downcase}",
+ data: { toggle: 'tooltip', placement: 'top' }
+ }
+
+ content_tag :li, class: "filter_icon #{active}" do
+ link_to request.path, link_opts do
+ icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
+ end
+ end
+ end
+
+ def icon_for_event
+ {
+ EventFilter.push => 'upload',
+ EventFilter.merged => 'check-square-o',
+ EventFilter.comments => 'comments',
+ EventFilter.team => 'user',
+ }
+ end
+
+ def event_feed_title(event)
+ words = []
+ words << event.author_name
+ words << event_action_name(event)
+
+ if event.push?
+ words << event.ref_type
+ words << event.ref_name
+ words << "at"
+ elsif event.commented?
+ if event.note_commit?
+ words << event.note_short_commit_id
+ else
+ words << "##{truncate event.note_target_iid}"
+ end
+ words << "at"
+ elsif event.target
+ words << "##{event.target_iid}:"
+ words << event.target.title if event.target.respond_to?(:title)
+ words << "at"
+ end
+
+ words << event.project_name
+
+ words.join(" ")
+ end
+
+ def event_feed_url(event)
+ if event.issue?
+ namespace_project_issue_url(event.project.namespace, event.project,
+ event.issue)
+ elsif event.merge_request?
+ namespace_project_merge_request_url(event.project.namespace,
+ event.project, event.merge_request)
+ elsif event.note? && event.note_commit?
+ namespace_project_commit_url(event.project.namespace, event.project,
+ event.note_target)
+ elsif event.note?
+ if event.note_target
+ if event.note_commit?
+ namespace_project_commit_path(event.project.namespace, event.project,
+ event.note_commit_id,
+ anchor: dom_id(event.target))
+ elsif event.note_project_snippet?
+ namespace_project_snippet_path(event.project.namespace,
+ event.project, event.note_target)
+ else
+ event_note_target_path(event)
+ end
+ end
+ elsif event.push?
+ if event.push_with_commits? && event.md_ref?
+ if event.commits_count > 1
+ namespace_project_compare_url(event.project.namespace, event.project,
+ from: event.commit_from, to:
+ event.commit_to)
+ else
+ namespace_project_commit_url(event.project.namespace, event.project,
+ id: event.commit_to)
+ end
+ else
+ namespace_project_commits_url(event.project.namespace, event.project,
+ event.ref_name)
+ end
+ end
+ end
+
+ def event_feed_summary(event)
+ if event.issue?
+ render "events/event_issue", issue: event.issue
+ elsif event.push?
+ render "events/event_push", event: event
+ elsif event.merge_request?
+ render "events/event_merge_request", merge_request: event.merge_request
+ elsif event.note?
+ render "events/event_note", note: event.note
+ end
+ end
+
+ def event_note_target_path(event)
+ if event.note? && event.note_commit?
+ namespace_project_commit_path(event.project.namespace, event.project,
+ event.note_target)
+ else
+ polymorphic_path([event.project.namespace.becomes(Namespace),
+ event.project, event.note_target],
+ anchor: dom_id(event.target))
+ end
+ end
+
+ def event_note_title_html(event)
+ if event.note_target
+ if event.note_commit?
+ link_to(
+ namespace_project_commit_path(event.project.namespace, event.project,
+ event.note_commit_id,
+ anchor: dom_id(event.target)),
+ class: "commit_short_id"
+ ) do
+ "#{event.note_target_type} #{event.note_short_commit_id}"
+ end
+ elsif event.note_project_snippet?
+ link_to(namespace_project_snippet_path(event.project.namespace,
+ event.project,
+ event.note_target)) do
+ "#{event.note_target_type} ##{truncate event.note_target_id}"
+ end
+ else
+ link_to event_note_target_path(event) do
+ "#{event.note_target_type} ##{truncate event.note_target_iid}"
+ end
+ end
+ else
+ content_tag :strong do
+ "(deleted)"
+ end
+ end
+ end
+
+ def event_note(text, options = {})
+ text = first_line_in_markdown(text, 150, options)
+ sanitize(text, tags: %w(a img b pre code p span))
+ end
+
+ def event_commit_title(message)
+ escape_once(truncate(message.split("\n").first, length: 70))
+ rescue
+ "--broken encoding"
+ end
+
+ def event_to_atom(xml, event)
+ if event.proper?
+ xml.entry do
+ event_link = event_feed_url(event)
+ event_title = event_feed_title(event)
+ event_summary = event_feed_summary(event)
+
+ xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
+ xml.link href: event_link
+ xml.title truncate(event_title, length: 80)
+ xml.updated event.created_at.xmlschema
+ xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email)
+ xml.author do |author|
+ xml.name event.author_name
+ xml.email event.author_email
+ end
+
+ xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? }
+ end
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/explore_helper.rb b/app/helpers/gitlab/explore_helper.rb
new file mode 100644
index 00000000000..b8e0f482b94
--- /dev/null
+++ b/app/helpers/gitlab/explore_helper.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module ExploreHelper
+ def explore_projects_filter_path(options={})
+ exist_opts = {
+ sort: params[:sort],
+ scope: params[:scope],
+ group: params[:group],
+ tag: params[:tag],
+ visibility_level: params[:visibility_level],
+ }
+
+ options = exist_opts.merge(options)
+
+ path = explore_projects_path
+ path << "?#{options.to_param}"
+ path
+ end
+ end
+end
diff --git a/app/helpers/gitlab/external_wiki_helper.rb b/app/helpers/gitlab/external_wiki_helper.rb
new file mode 100644
index 00000000000..710cdc727d0
--- /dev/null
+++ b/app/helpers/gitlab/external_wiki_helper.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module ExternalWikiHelper
+ def get_project_wiki_path(project)
+ external_wiki_service = project.services.
+ select { |service| service.to_param == 'external_wiki' }.first
+ if external_wiki_service.present? && external_wiki_service.active?
+ external_wiki_service.properties['external_wiki_url']
+ else
+ namespace_project_wiki_path(project.namespace, project, :home)
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/git_helper.rb b/app/helpers/gitlab/git_helper.rb
new file mode 100644
index 00000000000..867b30b8c74
--- /dev/null
+++ b/app/helpers/gitlab/git_helper.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module GitHelper
+ def strip_gpg_signature(text)
+ text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
+ end
+ end
+end
diff --git a/app/helpers/gitlab/gitlab_markdown_helper.rb b/app/helpers/gitlab/gitlab_markdown_helper.rb
new file mode 100644
index 00000000000..265cb4672fe
--- /dev/null
+++ b/app/helpers/gitlab/gitlab_markdown_helper.rb
@@ -0,0 +1,195 @@
+require 'nokogiri'
+
+module Gitlab
+ module GitlabMarkdownHelper
+ include Gitlab::Markdown
+ include PreferencesHelper
+
+ # Use this in places where you would normally use link_to(gfm(...), ...).
+ #
+ # It solves a problem occurring with nested links (i.e.
+ # "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be
+ # interpreted as intended. Browsers will parse something like
+ # "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is
+ # not linked any more). link_to_gfm corrects that. It wraps all parts to
+ # explicitly produce the correct linking behavior (i.e.
+ # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
+ def link_to_gfm(body, url, html_options = {})
+ return "" if body.blank?
+
+ escaped_body = if body =~ /\A\<img/
+ body
+ else
+ escape_once(body)
+ end
+
+ gfm_body = gfm(escaped_body, {}, html_options)
+
+ fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body)
+ if fragment.children.size == 1 && fragment.children[0].name == 'a'
+ # Fragment has only one node, and it's a link generated by `gfm`.
+ # Replace it with our requested link.
+ text = fragment.children[0].text
+ fragment.children[0].replace(link_to(text, url, html_options))
+ else
+ # Traverse the fragment's first generation of children looking for pure
+ # text, wrapping anything found in the requested link
+ fragment.children.each do |node|
+ next unless node.text?
+ node.replace(link_to(node.text, url, html_options))
+ end
+ end
+
+ fragment.to_html.html_safe
+ end
+
+ MARKDOWN_OPTIONS = {
+ no_intra_emphasis: true,
+ tables: true,
+ fenced_code_blocks: true,
+ strikethrough: true,
+ lax_spacing: true,
+ space_after_headers: true,
+ superscript: true,
+ footnotes: true
+ }.freeze
+
+ def markdown(text, options={})
+ unless @markdown && options == @options
+ @options = options
+
+ # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch
+ rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options)
+
+ # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
+ @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS)
+ end
+
+ @markdown.render(text).html_safe
+ end
+
+ def asciidoc(text)
+ Gitlab::Asciidoc.render(text, {
+ commit: @commit,
+ project: @project,
+ project_wiki: @project_wiki,
+ requested_path: @path,
+ ref: @ref
+ })
+ end
+
+ # Return the first line of +text+, up to +max_chars+, after parsing the line
+ # as Markdown. HTML tags in the parsed output are not counted toward the
+ # +max_chars+ limit. If the length limit falls within a tag's contents, then
+ # the tag contents are truncated without removing the closing tag.
+ def first_line_in_markdown(text, max_chars = nil, options = {})
+ md = markdown(text, options).strip
+
+ truncate_visible(md, max_chars || md.length) if md.present?
+ end
+
+ def render_wiki_content(wiki_page)
+ case wiki_page.format
+ when :markdown
+ markdown(wiki_page.content)
+ when :asciidoc
+ asciidoc(wiki_page.content)
+ else
+ wiki_page.formatted_content.html_safe
+ end
+ end
+
+ MARKDOWN_TIPS = [
+ "End a line with two or more spaces for a line-break, or soft-return",
+ "Inline code can be denoted by `surrounding it with backticks`",
+ "Blocks of code can be denoted by three backticks ``` or four leading spaces",
+ "Emoji can be added by :emoji_name:, for example :thumbsup:",
+ "Notify other participants using @user_name",
+ "Notify a specific group using @group_name",
+ "Notify the entire team using @all",
+ "Reference an issue using a hash, for example issue #123",
+ "Reference a merge request using an exclamation point, for example MR !123",
+ "Italicize words or phrases using *asterisks* or _underscores_",
+ "Bold words or phrases using **double asterisks** or __double underscores__",
+ "Strikethrough words or phrases using ~~two tildes~~",
+ "Make a bulleted list using + pluses, - minuses, or * asterisks",
+ "Denote blockquotes using > at the beginning of a line",
+ "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___"
+ ].freeze
+
+ # Returns a random markdown tip for use as a textarea placeholder
+ def random_markdown_tip
+ MARKDOWN_TIPS.sample
+ end
+
+ private
+
+ # Return +text+, truncated to +max_chars+ characters, excluding any HTML
+ # tags.
+ def truncate_visible(text, max_chars)
+ doc = Nokogiri::HTML.fragment(text)
+ content_length = 0
+ truncated = false
+
+ doc.traverse do |node|
+ if node.text? || node.content.empty?
+ if truncated
+ node.remove
+ next
+ end
+
+ # Handle line breaks within a node
+ if node.content.strip.lines.length > 1
+ node.content = "#{node.content.lines.first.chomp}..."
+ truncated = true
+ end
+
+ num_remaining = max_chars - content_length
+ if node.content.length > num_remaining
+ node.content = node.content.truncate(num_remaining)
+ truncated = true
+ end
+ content_length += node.content.length
+ end
+
+ truncated = truncate_if_block(node, truncated)
+ end
+
+ doc.to_html
+ end
+
+ # Used by #truncate_visible. If +node+ is the first block element, and the
+ # text hasn't already been truncated, then append "..." to the node contents
+ # and return true. Otherwise return false.
+ def truncate_if_block(node, truncated)
+ if node.element? && node.description.block? && !truncated
+ node.content = "#{node.content}..." if node.next_sibling
+ true
+ else
+ truncated
+ end
+ end
+
+ # Returns the text necessary to reference `entity` across projects
+ #
+ # project - Project to reference
+ # entity - Object that responds to `to_reference`
+ #
+ # Examples:
+ #
+ # cross_project_reference(project, project.issues.first)
+ # # => 'namespace1/project1#123'
+ #
+ # cross_project_reference(project, project.merge_requests.first)
+ # # => 'namespace1/project1!345'
+ #
+ # Returns a String
+ def cross_project_reference(project, entity)
+ if entity.respond_to?(:to_reference)
+ "#{project.to_reference}#{entity.to_reference}"
+ else
+ ''
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/gitlab_routing_helper.rb b/app/helpers/gitlab/gitlab_routing_helper.rb
new file mode 100644
index 00000000000..7f1e455d5de
--- /dev/null
+++ b/app/helpers/gitlab/gitlab_routing_helper.rb
@@ -0,0 +1,69 @@
+# Shorter routing method for project and project items
+# Since update to rails 4.1.9 we are now allowed to use `/` in project routing
+# so we use nested routing for project resources which include project and
+# project namespace. To avoid writing long methods every time we define shortcuts for
+# some of routing.
+#
+# For example instead of this:
+#
+# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request)
+#
+# We can simply use shortcut:
+#
+# merge_request_path(merge_request)
+#
+module Gitlab
+ module GitlabRoutingHelper
+ def project_path(project, *args)
+ namespace_project_path(project.namespace, project, *args)
+ end
+
+ def activity_project_path(project, *args)
+ activity_namespace_project_path(project.namespace, project, *args)
+ end
+
+ def edit_project_path(project, *args)
+ edit_namespace_project_path(project.namespace, project, *args)
+ end
+
+ def issue_path(entity, *args)
+ namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
+ end
+
+ def merge_request_path(entity, *args)
+ namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args)
+ end
+
+ def milestone_path(entity, *args)
+ namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
+ end
+
+ def project_url(project, *args)
+ namespace_project_url(project.namespace, project, *args)
+ end
+
+ def edit_project_url(project, *args)
+ edit_namespace_project_url(project.namespace, project, *args)
+ end
+
+ def issue_url(entity, *args)
+ namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
+ end
+
+ def merge_request_url(entity, *args)
+ namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
+ end
+
+ def project_snippet_url(entity, *args)
+ namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
+ end
+
+ def toggle_subscription_path(entity, *args)
+ if entity.is_a?(Issue)
+ toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity)
+ else
+ toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/graph_helper.rb b/app/helpers/gitlab/graph_helper.rb
new file mode 100644
index 00000000000..047f5c19095
--- /dev/null
+++ b/app/helpers/gitlab/graph_helper.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module GraphHelper
+ def get_refs(repo, commit)
+ refs = ""
+ refs << commit.ref_names(repo).join(' ')
+
+ # append note count
+ refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0
+
+ refs
+ end
+
+ def parents_zip_spaces(parents, parent_spaces)
+ ids = parents.map { |p| p.id }
+ ids.zip(parent_spaces)
+ end
+ end
+end
diff --git a/app/helpers/gitlab/groups_helper.rb b/app/helpers/gitlab/groups_helper.rb
new file mode 100644
index 00000000000..8172c617249
--- /dev/null
+++ b/app/helpers/gitlab/groups_helper.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module GroupsHelper
+ def remove_user_from_group_message(group, member)
+ if member.user
+ "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
+ else
+ "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
+ end
+ end
+
+ def leave_group_message(group)
+ "Are you sure you want to leave \"#{group}\" group?"
+ end
+
+ def should_user_see_group_roles?(user, group)
+ if user
+ user.is_admin? || group.members.exists?(user_id: user.id)
+ else
+ false
+ end
+ end
+
+ def group_icon(group)
+ if group.is_a?(String)
+ group = Group.find_by(path: group)
+ end
+
+ if group && group.avatar.present?
+ group.avatar.url
+ else
+ image_path('no_group_avatar.png')
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/icons_helper.rb b/app/helpers/gitlab/icons_helper.rb
new file mode 100644
index 00000000000..e815d237bb1
--- /dev/null
+++ b/app/helpers/gitlab/icons_helper.rb
@@ -0,0 +1,87 @@
+module Gitlab
+ module IconsHelper
+ include FontAwesome::Rails::IconHelper
+
+ # Creates an icon tag given icon name(s) and possible icon modifiers.
+ #
+ # Right now this method simply delegates directly to `fa_icon` from the
+ # font-awesome-rails gem, but should we ever use a different icon pack in the
+ # future we won't have to change hundreds of method calls.
+ def icon(names, options = {})
+ fa_icon(names, options)
+ end
+
+ def spinner(text = nil, visible = false)
+ css_class = 'loading'
+ css_class << ' hide' unless visible
+
+ content_tag :div, class: css_class do
+ icon('spinner spin') + text
+ end
+ end
+
+ def boolean_to_icon(value)
+ if value
+ icon('circle', class: 'cgreen')
+ else
+ icon('power-off', class: 'clgray')
+ end
+ end
+
+ def public_icon
+ icon('globe fw')
+ end
+
+ def internal_icon
+ icon('shield fw')
+ end
+
+ def private_icon
+ icon('lock fw')
+ end
+
+ def file_type_icon_class(type, mode, name)
+ if type == 'folder'
+ icon_class = 'folder'
+ elsif mode == '120000'
+ icon_class = 'share'
+ else
+ # Guess which icon to choose based on file extension.
+ # If you think a file extension is missing, feel free to add it on PR
+
+ case File.extname(name).downcase
+ when '.pdf'
+ icon_class = 'file-pdf-o'
+ when '.jpg', '.jpeg', '.jif', '.jfif',
+ '.jp2', '.jpx', '.j2k', '.j2c',
+ '.png', '.gif', '.tif', '.tiff',
+ '.svg', '.ico', '.bmp'
+ icon_class = 'file-image-o'
+ when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip',
+ '.xz', '.rar', '.7z'
+ icon_class = 'file-archive-o'
+ when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac'
+ icon_class = 'file-audio-o'
+ when '.mp4', '.m4p', '.m4v',
+ '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv',
+ '.mpg', '.mpeg', '.m2v',
+ '.avi', '.mkv', '.flv', '.ogv', '.mov',
+ '.3gp', '.3g2'
+ icon_class = 'file-video-o'
+ when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb'
+ icon_class = 'file-word-o'
+ when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm',
+ '.xlsb', '.xla', '.xlam', '.xll', '.xlw'
+ icon_class = 'file-excel-o'
+ when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm',
+ '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm'
+ icon_class = 'file-powerpoint-o'
+ else
+ icon_class = 'file-text-o'
+ end
+ end
+
+ icon_class
+ end
+ end
+end
diff --git a/app/helpers/gitlab/issues_helper.rb b/app/helpers/gitlab/issues_helper.rb
new file mode 100644
index 00000000000..67238926555
--- /dev/null
+++ b/app/helpers/gitlab/issues_helper.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module IssuesHelper
+ def issue_css_classes(issue)
+ classes = "issue"
+ classes << " closed" if issue.closed?
+ classes << " today" if issue.today?
+ classes
+ end
+
+ # Returns an OpenStruct object suitable for use by <tt>options_from_collection_for_select</tt>
+ # to allow filtering issues by an unassigned User or Milestone
+ def unassigned_filter
+ # Milestone uses :title, Issue uses :name
+ OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned')
+ end
+
+ def url_for_project_issues(project = @project, options = {})
+ return '' if project.nil?
+
+ if options[:only_path]
+ project.issues_tracker.project_path
+ else
+ project.issues_tracker.project_url
+ end
+ end
+
+ def url_for_new_issue(project = @project, options = {})
+ return '' if project.nil?
+
+ if options[:only_path]
+ project.issues_tracker.new_issue_path
+ else
+ project.issues_tracker.new_issue_url
+ end
+ end
+
+ def url_for_issue(issue_iid, project = @project, options = {})
+ return '' if project.nil?
+
+ if options[:only_path]
+ project.issues_tracker.issue_path(issue_iid)
+ else
+ project.issues_tracker.issue_url(issue_iid)
+ end
+ end
+
+ def bulk_update_milestone_options
+ options_for_select([['None (backlog)', -1]]) +
+ options_from_collection_for_select(project_active_milestones, 'id',
+ 'title', params[:milestone_id])
+ end
+
+ def milestone_options(object)
+ options_from_collection_for_select(object.project.milestones.active,
+ 'id', 'title', object.milestone_id)
+ end
+
+ def issue_box_class(item)
+ if item.respond_to?(:expired?) && item.expired?
+ 'issue-box-expired'
+ elsif item.respond_to?(:merged?) && item.merged?
+ 'issue-box-merged'
+ elsif item.closed?
+ 'issue-box-closed'
+ else
+ 'issue-box-open'
+ end
+ end
+
+ def issue_to_atom(xml, issue)
+ xml.entry do
+ xml.id namespace_project_issue_url(issue.project.namespace,
+ issue.project, issue)
+ xml.link href: namespace_project_issue_url(issue.project.namespace,
+ issue.project, issue)
+ xml.title truncate(issue.title, length: 80)
+ xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email)
+ xml.author do |author|
+ xml.name issue.author_name
+ xml.email issue.author_email
+ end
+ xml.summary issue.title
+ end
+ end
+
+ # Required for Gitlab::Markdown::IssueReferenceFilter
+ module_function :url_for_issue
+ end
+end
diff --git a/app/helpers/gitlab/labels_helper.rb b/app/helpers/gitlab/labels_helper.rb
new file mode 100644
index 00000000000..aa16d71f42c
--- /dev/null
+++ b/app/helpers/gitlab/labels_helper.rb
@@ -0,0 +1,103 @@
+module Gitlab
+ module LabelsHelper
+ include ActionView::Helpers::TagHelper
+
+ # Link to a Label
+ #
+ # label - Label object to link to
+ # project - Project object which will be used as the context for the label's
+ # link. If omitted, defaults to `@project`, or the label's own
+ # project.
+ # block - An optional block that will be passed to `link_to`, forming the
+ # body of the link element. If omitted, defaults to
+ # `render_colored_label`.
+ #
+ # Examples:
+ #
+ # # Allow the generated link to use the label's own project
+ # link_to_label(label)
+ #
+ # # Force the generated link to use @project
+ # @project = Project.first
+ # link_to_label(label)
+ #
+ # # Force the generated link to use a provided project
+ # link_to_label(label, project: Project.last)
+ #
+ # # Customize link body with a block
+ # link_to_label(label) { "My Custom Label Text" }
+ #
+ # Returns a String
+ def link_to_label(label, project: nil, &block)
+ project ||= @project || label.project
+ link = namespace_project_issues_path(project.namespace, project,
+ label_name: label.name)
+
+ if block_given?
+ link_to link, &block
+ else
+ link_to render_colored_label(label), link
+ end
+ end
+
+ def project_label_names
+ @project.labels.pluck(:title)
+ end
+
+ def render_colored_label(label)
+ label_color = label.color || Label::DEFAULT_COLOR
+ text_color = text_color_for_bg(label_color)
+
+ # Intentionally not using content_tag here so that this method can be called
+ # by LabelReferenceFilter
+ span = %(<span class="label color-label") +
+ %( style="background-color: #{label_color}; color: #{text_color}">) +
+ escape_once(label.name) + '</span>'
+
+ span.html_safe
+ end
+
+ def suggested_colors
+ [
+ '#0033CC',
+ '#428BCA',
+ '#44AD8E',
+ '#A8D695',
+ '#5CB85C',
+ '#69D100',
+ '#004E00',
+ '#34495E',
+ '#7F8C8D',
+ '#A295D6',
+ '#5843AD',
+ '#8E44AD',
+ '#FFECDB',
+ '#AD4363',
+ '#D10069',
+ '#CC0033',
+ '#FF0000',
+ '#D9534F',
+ '#D1D100',
+ '#F0AD4E',
+ '#AD8D43'
+ ]
+ end
+
+ def text_color_for_bg(bg_color)
+ r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
+
+ if (r + g + b) > 500
+ '#333333'
+ else
+ '#FFFFFF'
+ end
+ end
+
+ def project_labels_options(project)
+ options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
+ end
+
+ # Required for Gitlab::Markdown::LabelReferenceFilter
+ module_function :render_colored_label, :text_color_for_bg, :escape_once
+ end
+end
diff --git a/app/helpers/gitlab/merge_requests_helper.rb b/app/helpers/gitlab/merge_requests_helper.rb
new file mode 100644
index 00000000000..361f6b2fdac
--- /dev/null
+++ b/app/helpers/gitlab/merge_requests_helper.rb
@@ -0,0 +1,76 @@
+module Gitlab
+ module MergeRequestsHelper
+ def new_mr_path_from_push_event(event)
+ target_project = event.project.forked_from_project || event.project
+ new_namespace_project_merge_request_path(
+ event.project.namespace,
+ event.project,
+ new_mr_from_push_event(event, target_project)
+ )
+ end
+
+ def new_mr_path_for_fork_from_push_event(event)
+ new_namespace_project_merge_request_path(
+ event.project.namespace,
+ event.project,
+ new_mr_from_push_event(event, event.project.forked_from_project)
+ )
+ end
+
+ def new_mr_from_push_event(event, target_project)
+ {
+ merge_request: {
+ source_project_id: event.project.id,
+ target_project_id: target_project.id,
+ source_branch: event.branch_name,
+ target_branch: target_project.repository.root_ref
+ }
+ }
+ end
+
+ def mr_css_classes(mr)
+ classes = "merge-request"
+ classes << " closed" if mr.closed?
+ classes << " merged" if mr.merged?
+ classes
+ end
+
+ def ci_build_details_path(merge_request)
+ merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
+ end
+
+ def merge_path_description(merge_request, separator)
+ if merge_request.for_fork?
+ "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}"
+ else
+ "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}"
+ end
+ end
+
+ def issues_sentence(issues)
+ issues.map { |i| "##{i.iid}" }.to_sentence
+ end
+
+ def mr_change_branches_path(merge_request)
+ new_namespace_project_merge_request_path(
+ @project.namespace, @project,
+ merge_request: {
+ source_project_id: @merge_request.source_project_id,
+ target_project_id: @merge_request.target_project_id,
+ source_branch: @merge_request.source_branch,
+ target_branch: nil
+ }
+ )
+ end
+
+ def source_branch_with_namespace(merge_request)
+ if merge_request.for_fork?
+ namespace = link_to(merge_request.source_project_namespace,
+ project_path(merge_request.source_project))
+ namespace + ":#{merge_request.source_branch}"
+ else
+ merge_request.source_branch
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/milestones_helper.rb b/app/helpers/gitlab/milestones_helper.rb
new file mode 100644
index 00000000000..116967d4946
--- /dev/null
+++ b/app/helpers/gitlab/milestones_helper.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module MilestonesHelper
+ def milestones_filter_path(opts = {})
+ if @project
+ namespace_project_milestones_path(@project.namespace, @project, opts)
+ elsif @group
+ group_milestones_path(@group, opts)
+ else
+ dashboard_milestones_path(opts)
+ end
+ end
+
+ def milestone_progress_bar(milestone)
+ options = {
+ class: 'progress-bar progress-bar-success',
+ style: "width: #{milestone.percent_complete}%;"
+ }
+
+ content_tag :div, class: 'progress' do
+ content_tag :div, nil, options
+ end
+ end
+
+ def projects_milestones_options
+ milestones =
+ if @project
+ @project.milestones
+ else
+ Milestone.where(project_id: @projects)
+ end.active
+
+ grouped_milestones = Milestones::GroupService.new(milestones).execute
+ grouped_milestones.unshift(Milestone::None)
+
+ options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title])
+ end
+ end
+end
diff --git a/app/helpers/gitlab/namespaces_helper.rb b/app/helpers/gitlab/namespaces_helper.rb
new file mode 100644
index 00000000000..b1caaac3f63
--- /dev/null
+++ b/app/helpers/gitlab/namespaces_helper.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module NamespacesHelper
+ def namespaces_options(selected = :current_user, scope = :default)
+ groups = current_user.owned_groups + current_user.masters_groups
+ users = [current_user.namespace]
+
+ group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
+ users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ]
+
+ options = []
+ options << group_opts
+ options << users_opts
+
+ if selected == :current_user && current_user.namespace
+ selected = current_user.namespace.id
+ end
+
+ grouped_options_for_select(options, selected)
+ end
+
+ def namespace_select_tag(id, opts = {})
+ css_class = "ajax-namespace-select "
+ css_class << "multiselect " if opts[:multiple]
+ css_class << (opts[:class] || '')
+ value = opts[:selected] || ''
+
+ hidden_field_tag(id, value, class: css_class)
+ end
+
+ def namespace_icon(namespace, size = 40)
+ if namespace.kind_of?(Group)
+ group_icon(namespace)
+ else
+ avatar_icon(namespace.owner.email, size)
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/nav_helper.rb b/app/helpers/gitlab/nav_helper.rb
new file mode 100644
index 00000000000..14106d70840
--- /dev/null
+++ b/app/helpers/gitlab/nav_helper.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module NavHelper
+ def nav_menu_collapsed?
+ cookies[:collapsed_nav] == 'true'
+ end
+
+ def nav_sidebar_class
+ if nav_menu_collapsed?
+ "page-sidebar-collapsed"
+ else
+ "page-sidebar-expanded"
+ end
+ end
+
+ def nav_header_class
+ if nav_menu_collapsed?
+ "header-collapsed"
+ else
+ "header-expanded"
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/notes_helper.rb b/app/helpers/gitlab/notes_helper.rb
new file mode 100644
index 00000000000..15076148b02
--- /dev/null
+++ b/app/helpers/gitlab/notes_helper.rb
@@ -0,0 +1,78 @@
+module Gitlab
+ module NotesHelper
+ # Helps to distinguish e.g. commit notes in mr notes list
+ def note_for_main_target?(note)
+ (@noteable.class.name == note.noteable_type && !note.for_diff_line?)
+ end
+
+ def note_target_fields(note)
+ hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
+ hidden_field_tag(:target_id, note.noteable.id)
+ end
+
+ def note_editable?(note)
+ note.editable? && can?(current_user, :admin_note, note)
+ end
+
+ def link_to_commit_diff_line_note(note)
+ if note.for_commit_diff_line?
+ link_to(
+ "#{note.diff_file_name}:L#{note.diff_new_line}",
+ namespace_project_commit_path(@project.namespace, @project,
+ note.noteable, anchor: note.line_code)
+ )
+ end
+ end
+
+ def noteable_json(noteable)
+ {
+ id: noteable.id,
+ class: noteable.class.name,
+ resources: noteable.class.table_name,
+ project_id: noteable.project.id,
+ }.to_json
+ end
+
+ def link_to_new_diff_note(line_code, line_type = nil)
+ discussion_id = Note.build_discussion_id(
+ @comments_target[:noteable_type],
+ @comments_target[:noteable_id] || @comments_target[:commit_id],
+ line_code
+ )
+
+ data = {
+ noteable_type: @comments_target[:noteable_type],
+ noteable_id: @comments_target[:noteable_id],
+ commit_id: @comments_target[:commit_id],
+ line_code: line_code,
+ discussion_id: discussion_id,
+ line_type: line_type
+ }
+
+ button_tag(class: 'btn add-diff-note js-add-diff-note-button',
+ data: data,
+ title: 'Add a comment to this line') do
+ icon('comment-o')
+ end
+ end
+
+ def link_to_reply_diff(note, line_type = nil)
+ return unless current_user
+
+ data = {
+ noteable_type: note.noteable_type,
+ noteable_id: note.noteable_id,
+ commit_id: note.commit_id,
+ line_code: note.line_code,
+ discussion_id: note.discussion_id,
+ line_type: line_type
+ }
+
+ button_tag class: 'btn reply-btn js-discussion-reply-button',
+ data: data, title: 'Add a reply' do
+ link_text = icon('comment')
+ link_text << ' Reply'
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/notifications_helper.rb b/app/helpers/gitlab/notifications_helper.rb
new file mode 100644
index 00000000000..b6324044ab1
--- /dev/null
+++ b/app/helpers/gitlab/notifications_helper.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module NotificationsHelper
+ include IconsHelper
+
+ def notification_icon(notification)
+ if notification.disabled?
+ icon('volume-off', class: 'ns-mute')
+ elsif notification.participating?
+ icon('volume-down', class: 'ns-part')
+ elsif notification.watch?
+ icon('volume-up', class: 'ns-watch')
+ else
+ icon('circle-o', class: 'ns-default')
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/page_layout_helper.rb b/app/helpers/gitlab/page_layout_helper.rb
new file mode 100644
index 00000000000..d7a85186155
--- /dev/null
+++ b/app/helpers/gitlab/page_layout_helper.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module PageLayoutHelper
+ def page_title(*titles)
+ @page_title ||= []
+
+ @page_title.push(*titles.compact) if titles.any?
+
+ @page_title.join(" | ")
+ end
+
+ def header_title(title = nil, title_url = nil)
+ if title
+ @header_title = title
+ @header_title_url = title_url
+ else
+ @header_title_url ? link_to(@header_title, @header_title_url) : @header_title
+ end
+ end
+
+ def sidebar(name = nil)
+ if name
+ @sidebar = name
+ else
+ @sidebar
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/preferences_helper.rb b/app/helpers/gitlab/preferences_helper.rb
new file mode 100644
index 00000000000..3eac5d51acd
--- /dev/null
+++ b/app/helpers/gitlab/preferences_helper.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ # Helper methods for per-User preferences
+ module PreferencesHelper
+ COLOR_SCHEMES = {
+ 1 => 'white',
+ 2 => 'dark',
+ 3 => 'solarized-light',
+ 4 => 'solarized-dark',
+ 5 => 'monokai',
+ }
+ COLOR_SCHEMES.default = 'white'
+
+ # Helper method to access the COLOR_SCHEMES
+ #
+ # The keys are the `color_scheme_ids`
+ # The values are the `name` of the scheme.
+ #
+ # The preview images are `name-scheme-preview.png`
+ # The stylesheets should use the css class `.name`
+ def color_schemes
+ COLOR_SCHEMES.freeze
+ end
+
+ # Maps `dashboard` values to more user-friendly option text
+ DASHBOARD_CHOICES = {
+ projects: 'Your Projects (default)',
+ stars: 'Starred Projects'
+ }.with_indifferent_access.freeze
+
+ # Returns an Array usable by a select field for more user-friendly option text
+ def dashboard_choices
+ defined = User.dashboards
+
+ if defined.size != DASHBOARD_CHOICES.size
+ # Ensure that anyone adding new options updates this method too
+ raise RuntimeError, "`User` defines #{defined.size} dashboard choices," +
+ " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
+ else
+ defined.map do |key, _|
+ # Use `fetch` so `KeyError` gets raised when a key is missing
+ [DASHBOARD_CHOICES.fetch(key), key]
+ end
+ end
+ end
+
+ def project_view_choices
+ [
+ ['Readme (default)', :readme],
+ ['Activity view', :activity]
+ ]
+ end
+
+ def user_application_theme
+ theme = Gitlab::Themes.by_id(current_user.try(:theme_id))
+ theme.css_class
+ end
+
+ def user_color_scheme_class
+ COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user)
+ end
+
+ def prefer_readme?
+ !current_user ||
+ current_user.project_view == 'readme'
+ end
+ end
+end
diff --git a/app/helpers/gitlab/projects_helper.rb b/app/helpers/gitlab/projects_helper.rb
new file mode 100644
index 00000000000..8a8cd6048df
--- /dev/null
+++ b/app/helpers/gitlab/projects_helper.rb
@@ -0,0 +1,332 @@
+module Gitlab
+ module ProjectsHelper
+ def remove_from_project_team_message(project, member)
+ if member.user
+ "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
+ else
+ "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
+ end
+ end
+
+ def link_to_project(project)
+ link_to [project.namespace.becomes(Namespace), project] do
+ title = content_tag(:span, project.name, class: 'project-name')
+
+ if project.namespace
+ namespace = content_tag(:span, "#{project.namespace.human_name} / ", class: 'namespace-name')
+ title = namespace + title
+ end
+
+ title
+ end
+ end
+
+ def link_to_member(project, author, opts = {})
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author' }
+ opts = default_opts.merge(opts)
+
+ return "(deleted)" unless author
+
+ author_html = ""
+
+ # Build avatar image tag
+ author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
+
+ # Build name span tag
+ author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+
+ author_html = author_html.html_safe
+
+ if opts[:name]
+ link_to(author_html, user_path(author), class: "author_link").html_safe
+ else
+ link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe
+ end
+ end
+
+ def project_title(project)
+ if project.group
+ content_tag :span do
+ link_to(
+ simple_sanitize(project.group.name), group_path(project.group)
+ ) + ' / ' +
+ link_to(simple_sanitize(project.name),
+ project_path(project))
+ end
+ else
+ owner = project.namespace.owner
+ content_tag :span do
+ link_to(
+ simple_sanitize(owner.name), user_path(owner)
+ ) + ' / ' +
+ link_to(simple_sanitize(project.name),
+ project_path(project))
+ end
+ end
+ end
+
+ def remove_project_message(project)
+ "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?"
+ end
+
+ def transfer_project_message(project)
+ "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+ end
+
+ def project_nav_tabs
+ @nav_tabs ||= get_project_nav_tabs(@project, current_user)
+ end
+
+ def project_nav_tab?(name)
+ project_nav_tabs.include? name
+ end
+
+ def project_active_milestones
+ @project.milestones.active.order("due_date, title ASC")
+ end
+
+ def project_for_deploy_key(deploy_key)
+ if deploy_key.projects.include?(@project)
+ @project
+ else
+ deploy_key.projects.find { |project| can?(current_user, :read_project, project) }
+ end
+ end
+
+ def can_change_visibility_level?(project, current_user)
+ return false unless can?(current_user, :change_visibility_level, project)
+
+ if project.forked?
+ project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE
+ else
+ true
+ end
+ end
+
+ private
+
+ def get_project_nav_tabs(project, current_user)
+ nav_tabs = [:home]
+
+ if !project.empty_repo? && can?(current_user, :download_code, project)
+ nav_tabs << [:files, :commits, :network, :graphs]
+ end
+
+ if project.repo_exists? && can?(current_user, :read_merge_request, project)
+ nav_tabs << :merge_requests
+ end
+
+ if can?(current_user, :admin_project, project)
+ nav_tabs << :settings
+ end
+
+ if can?(current_user, :read_issue, project)
+ nav_tabs << :issues
+ end
+
+ if can?(current_user, :read_wiki, project)
+ nav_tabs << :wiki
+ end
+
+ if can?(current_user, :read_project_snippet, project)
+ nav_tabs << :snippets
+ end
+
+ if can?(current_user, :read_label, project)
+ nav_tabs << :labels
+ end
+
+ if can?(current_user, :read_milestone, project)
+ nav_tabs << :milestones
+ end
+
+ nav_tabs.flatten
+ end
+
+ def git_user_name
+ if current_user
+ current_user.name
+ else
+ "Your name"
+ end
+ end
+
+ def git_user_email
+ if current_user
+ current_user.email
+ else
+ "your@email.com"
+ end
+ end
+
+ def repository_size(project = nil)
+ "#{(project || @project).repository_size} MB"
+ rescue
+ # In order to prevent 500 error
+ # when application cannot allocate memory
+ # to calculate repo size - just show 'Unknown'
+ 'unknown'
+ end
+
+ def default_url_to_repo(project = nil)
+ project = project || @project
+ current_user ? project.url_to_repo : project.http_url_to_repo
+ end
+
+ def default_clone_protocol
+ current_user ? "ssh" : "http"
+ end
+
+ def project_last_activity(project)
+ if project.last_activity_at
+ time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
+ else
+ "Never"
+ end
+ end
+
+ def add_contribution_guide_path(project)
+ if project && !project.repository.contribution_guide
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch,
+ file_name: "CONTRIBUTING.md",
+ commit_message: "Add contribution guide"
+ )
+ end
+ end
+
+ def add_changelog_path(project)
+ if project && !project.repository.changelog
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch,
+ file_name: "CHANGELOG",
+ commit_message: "Add changelog"
+ )
+ end
+ end
+
+ def add_license_path(project)
+ if project && !project.repository.license
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch,
+ file_name: "LICENSE",
+ commit_message: "Add license"
+ )
+ end
+ end
+
+ def contribution_guide_path(project)
+ if project && contribution_guide = project.repository.contribution_guide
+ namespace_project_blob_path(
+ project.namespace,
+ project,
+ tree_join(project.default_branch,
+ contribution_guide.name)
+ )
+ end
+ end
+
+ def readme_path(project)
+ filename_path(project, :readme)
+ end
+
+ def changelog_path(project)
+ filename_path(project, :changelog)
+ end
+
+ def license_path(project)
+ filename_path(project, :license)
+ end
+
+ def version_path(project)
+ filename_path(project, :version)
+ end
+
+ def hidden_pass_url(original_url)
+ result = URI(original_url)
+ result.password = '*****' unless result.password.nil?
+ result
+ rescue
+ original_url
+ end
+
+ def project_wiki_path_with_version(proj, page, version, is_newest)
+ url_params = is_newest ? {} : { version_id: version }
+ namespace_project_wiki_path(proj.namespace, proj, page, url_params)
+ end
+
+ def project_status_css_class(status)
+ case status
+ when "started"
+ "active"
+ when "failed"
+ "danger"
+ when "finished"
+ "success"
+ end
+ end
+
+ def user_max_access_in_project(user, project)
+ level = project.team.max_member_access(user)
+
+ if level
+ Gitlab::Access.options_with_owner.key(level)
+ end
+ end
+
+ def leave_project_message(project)
+ "Are you sure you want to leave \"#{project.name}\" project?"
+ end
+
+ def new_readme_path
+ ref = @repository.root_ref if @repository
+ ref ||= 'master'
+
+ namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
+ end
+
+ def last_push_event
+ if current_user
+ current_user.recent_push(@project.id)
+ end
+ end
+
+ def readme_cache_key
+ sha = @project.commit.try(:sha) || 'nil'
+ [@project.id, sha, "readme"].join('-')
+ end
+
+ def round_commit_count(project)
+ count = project.commit_count
+
+ if count > 10000
+ '10000+'
+ elsif count > 5000
+ '5000+'
+ elsif count > 1000
+ '1000+'
+ else
+ count
+ end
+ end
+
+ private
+
+ def filename_path(project, filename)
+ if project && blob = project.repository.send(filename)
+ namespace_project_blob_path(
+ project.namespace,
+ project,
+ tree_join(project.default_branch,
+ blob.name)
+ )
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/search_helper.rb b/app/helpers/gitlab/search_helper.rb
new file mode 100644
index 00000000000..f9caf8f2431
--- /dev/null
+++ b/app/helpers/gitlab/search_helper.rb
@@ -0,0 +1,114 @@
+module Gitlab
+ module SearchHelper
+ def search_autocomplete_opts(term)
+ return unless current_user
+
+ resources_results = [
+ groups_autocomplete(term),
+ projects_autocomplete(term)
+ ].flatten
+
+ generic_results = project_autocomplete + default_autocomplete + help_autocomplete
+ generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
+
+ [
+ resources_results,
+ generic_results
+ ].flatten.uniq do |item|
+ item[:label]
+ end
+ end
+
+ private
+
+ # Autocomplete results for various settings pages
+ def default_autocomplete
+ [
+ { label: "Profile settings", url: profile_path },
+ { label: "SSH Keys", url: profile_keys_path },
+ { label: "Dashboard", url: root_path },
+ { label: "Admin Section", url: admin_root_path },
+ ]
+ end
+
+ # Autocomplete results for internal help pages
+ def help_autocomplete
+ [
+ { label: "help: API Help", url: help_page_path("api", "README") },
+ { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") },
+ { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") },
+ { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") },
+ { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
+ { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
+ { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
+ { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
+ ]
+ end
+
+ # Autocomplete results for the current project, if it's defined
+ def project_autocomplete
+ if @project && @project.repository.exists? && @project.repository.root_ref
+ prefix = search_result_sanitize(@project.name_with_namespace)
+ ref = @ref || @project.repository.root_ref
+
+ [
+ { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
+ { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
+ { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
+ { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
+ { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) },
+ { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
+ { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
+ { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
+ { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
+ ]
+ else
+ []
+ end
+ end
+
+ # Autocomplete results for the current user's groups
+ def groups_autocomplete(term, limit = 5)
+ current_user.authorized_groups.search(term).limit(limit).map do |group|
+ {
+ label: "group: #{search_result_sanitize(group.name)}",
+ url: group_path(group)
+ }
+ end
+ end
+
+ # Autocomplete results for the current user's projects
+ def projects_autocomplete(term, limit = 5)
+ ProjectsFinder.new.execute(current_user).search_by_title(term).
+ sorted_by_stars.non_archived.limit(limit).map do |p|
+ {
+ label: "project: #{search_result_sanitize(p.name_with_namespace)}",
+ url: namespace_project_path(p.namespace, p)
+ }
+ end
+ end
+
+ def search_result_sanitize(str)
+ Sanitize.clean(str)
+ end
+
+ def search_filter_path(options={})
+ exist_opts = {
+ search: params[:search],
+ project_id: params[:project_id],
+ group_id: params[:group_id],
+ scope: params[:scope]
+ }
+
+ options = exist_opts.merge(options)
+ search_path(options)
+ end
+
+ # Sanitize html generated after parsing markdown from issue description or comment
+ def search_md_sanitize(html)
+ sanitize(html, tags: %w(a p ol ul li pre code))
+ end
+ end
+end
diff --git a/app/helpers/gitlab/selects_helper.rb b/app/helpers/gitlab/selects_helper.rb
new file mode 100644
index 00000000000..d52d670a1cf
--- /dev/null
+++ b/app/helpers/gitlab/selects_helper.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module SelectsHelper
+ def users_select_tag(id, opts = {})
+ css_class = "ajax-users-select "
+ css_class << "multiselect " if opts[:multiple]
+ css_class << (opts[:class] || '')
+ value = opts[:selected] || ''
+ placeholder = opts[:placeholder] || 'Search for a user'
+
+ null_user = opts[:null_user] || false
+ any_user = opts[:any_user] || false
+ email_user = opts[:email_user] || false
+ first_user = opts[:first_user] && current_user ? current_user.username : false
+ current_user = opts[:current_user] || false
+ project = opts[:project] || @project
+
+ html = {
+ class: css_class,
+ 'data-placeholder' => placeholder,
+ 'data-null-user' => null_user,
+ 'data-any-user' => any_user,
+ 'data-email-user' => email_user,
+ 'data-first-user' => first_user,
+ 'data-current-user' => current_user
+ }
+
+ unless opts[:scope] == :all
+ if project
+ html['data-project-id'] = project.id
+ elsif @group
+ html['data-group-id'] = @group.id
+ end
+ end
+
+ hidden_field_tag(id, value, html)
+ end
+
+ def groups_select_tag(id, opts = {})
+ css_class = "ajax-groups-select "
+ css_class << "multiselect " if opts[:multiple]
+ css_class << (opts[:class] || '')
+ value = opts[:selected] || ''
+
+ hidden_field_tag(id, value, class: css_class)
+ end
+ end
+end
diff --git a/app/helpers/gitlab/snippets_helper.rb b/app/helpers/gitlab/snippets_helper.rb
new file mode 100644
index 00000000000..aaf4d43f852
--- /dev/null
+++ b/app/helpers/gitlab/snippets_helper.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module SnippetsHelper
+ def lifetime_select_options
+ options = [
+ ['forever', nil],
+ ['1 day', "#{Date.current + 1.day}"],
+ ['1 week', "#{Date.current + 1.week}"],
+ ['1 month', "#{Date.current + 1.month}"]
+ ]
+ options_for_select(options)
+ end
+
+ def reliable_snippet_path(snippet)
+ if snippet.project_id?
+ namespace_project_snippet_path(snippet.project.namespace,
+ snippet.project, snippet)
+ else
+ snippet_path(snippet)
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/sorting_helper.rb b/app/helpers/gitlab/sorting_helper.rb
new file mode 100644
index 00000000000..29c63a0d129
--- /dev/null
+++ b/app/helpers/gitlab/sorting_helper.rb
@@ -0,0 +1,98 @@
+module Gitlab
+ module SortingHelper
+ def sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_milestone_soon => sort_title_milestone_soon,
+ sort_value_milestone_later => sort_title_milestone_later,
+ sort_value_largest_repo => sort_title_largest_repo,
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_oldest_signin => sort_title_oldest_signin,
+ }
+ end
+
+ def sort_title_oldest_updated
+ 'Oldest updated'
+ end
+
+ def sort_title_recently_updated
+ 'Recently updated'
+ end
+
+ def sort_title_oldest_created
+ 'Oldest created'
+ end
+
+ def sort_title_recently_created
+ 'Recently created'
+ end
+
+ def sort_title_milestone_soon
+ 'Milestone due soon'
+ end
+
+ def sort_title_milestone_later
+ 'Milestone due later'
+ end
+
+ def sort_title_name
+ 'Name'
+ end
+
+ def sort_title_largest_repo
+ 'Largest repository'
+ end
+
+ def sort_title_recently_signin
+ 'Recent sign in'
+ end
+
+ def sort_title_oldest_signin
+ 'Oldest sign in'
+ end
+
+ def sort_value_oldest_updated
+ 'updated_asc'
+ end
+
+ def sort_value_recently_updated
+ 'updated_desc'
+ end
+
+ def sort_value_oldest_created
+ 'created_asc'
+ end
+
+ def sort_value_recently_created
+ 'created_desc'
+ end
+
+ def sort_value_milestone_soon
+ 'milestone_due_asc'
+ end
+
+ def sort_value_milestone_later
+ 'milestone_due_desc'
+ end
+
+ def sort_value_name
+ 'name_asc'
+ end
+
+ def sort_value_largest_repo
+ 'repository_size_desc'
+ end
+
+ def sort_value_recently_signin
+ 'recent_sign_in'
+ end
+
+ def sort_value_oldest_signin
+ 'oldest_sign_in'
+ end
+ end
+end
diff --git a/app/helpers/gitlab/submodule_helper.rb b/app/helpers/gitlab/submodule_helper.rb
new file mode 100644
index 00000000000..c0fbebcb1d9
--- /dev/null
+++ b/app/helpers/gitlab/submodule_helper.rb
@@ -0,0 +1,76 @@
+module Gitlab
+ module SubmoduleHelper
+ include Gitlab::ShellAdapter
+
+ # links to files listing for submodule if submodule is a project on this server
+ def submodule_links(submodule_item, ref = nil, repository = @repository)
+ url = repository.submodule_url_for(ref, submodule_item.path)
+
+ return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
+
+ namespace = $1
+ project = $2
+ project.chomp!('.git')
+
+ if self_url?(url, namespace, project)
+ return namespace_project_path(namespace, project),
+ namespace_project_tree_path(namespace, project,
+ submodule_item.id)
+ elsif relative_self_url?(url)
+ relative_self_links(url, submodule_item.id)
+ elsif github_dot_com_url?(url)
+ standard_links('github.com', namespace, project, submodule_item.id)
+ elsif gitlab_dot_com_url?(url)
+ standard_links('gitlab.com', namespace, project, submodule_item.id)
+ else
+ return url, nil
+ end
+ end
+
+ protected
+
+ def github_dot_com_url?(url)
+ url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/
+ end
+
+ def gitlab_dot_com_url?(url)
+ url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/
+ end
+
+ def self_url?(url, namespace, project)
+ return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/',
+ project, '.git' ].join('')
+ url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
+ end
+
+ def relative_self_url?(url)
+ # (./)?(../repo.git) || (./)?(../../project/repo.git) )
+ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
+ end
+
+ def standard_links(host, namespace, project, commit)
+ base = [ 'https://', host, '/', namespace, '/', project ].join('')
+ [base, [ base, '/tree/', commit ].join('')]
+ end
+
+ def relative_self_links(url, commit)
+ # Map relative links to a namespace and project
+ # For example:
+ # ../bar.git -> same namespace, repo bar
+ # ../foo/bar.git -> namespace foo, repo bar
+ # ../../foo/bar/baz.git -> namespace bar, repo baz
+ components = url.split('/')
+ base = components.pop.gsub(/.git$/, '')
+ namespace = components.pop.gsub(/^\.\.$/, '')
+
+ if namespace.empty?
+ namespace = @project.namespace.path
+ end
+
+ [
+ namespace_project_path(namespace, base),
+ namespace_project_tree_path(namespace, base, commit)
+ ]
+ end
+ end
+end
diff --git a/app/helpers/gitlab/tab_helper.rb b/app/helpers/gitlab/tab_helper.rb
new file mode 100644
index 00000000000..01d36ff84fc
--- /dev/null
+++ b/app/helpers/gitlab/tab_helper.rb
@@ -0,0 +1,133 @@
+module Gitlab
+ module TabHelper
+ # Navigation link helper
+ #
+ # Returns an `li` element with an 'active' class if the supplied
+ # controller(s) and/or action(s) are currently active. The content of the
+ # element is the value passed to the block.
+ #
+ # options - The options hash used to determine if the element is "active" (default: {})
+ # :controller - One or more controller names to check (optional).
+ # :action - One or more action names to check (optional).
+ # :path - A shorthand path, such as 'dashboard#index', to check (optional).
+ # :html_options - Extra options to be passed to the list element (optional).
+ # block - An optional block that will become the contents of the returned
+ # `li` element.
+ #
+ # When both :controller and :action are specified, BOTH must match in order
+ # to be marked as active. When only one is given, either can match.
+ #
+ # Examples
+ #
+ # # Assuming we're on TreeController#show
+ #
+ # # Controller matches, but action doesn't
+ # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" }
+ # # => '<li>Hello</li>'
+ #
+ # # Controller matches
+ # nav_link(controller: [:tree, :refs]) { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Several paths
+ # nav_link(path: ['tree#show', 'profile#show']) { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Shorthand path
+ # nav_link(path: 'tree#show') { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Supplying custom options for the list element
+ # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
+ # # => '<li class="home active">Hello</li>'
+ #
+ # Returns a list item element String
+ def nav_link(options = {}, &block)
+ klass = active_nav_link?(options) ? 'active' : ''
+
+ # Add our custom class into the html_options, which may or may not exist
+ # and which may or may not already have a :class key
+ o = options.delete(:html_options) || {}
+ o[:class] ||= ''
+ o[:class] += ' ' + klass
+ o[:class].strip!
+
+ if block_given?
+ content_tag(:li, capture(&block), o)
+ else
+ content_tag(:li, nil, o)
+ end
+ end
+
+ def active_nav_link?(options)
+ if path = options.delete(:path)
+ unless path.respond_to?(:each)
+ path = [path]
+ end
+
+ path.any? do |single_path|
+ current_path?(single_path)
+ end
+ elsif page = options.delete(:page)
+ unless page.respond_to?(:each)
+ page = [page]
+ end
+
+ page.any? do |single_page|
+ current_page?(single_page)
+ end
+ else
+ c = options.delete(:controller)
+ a = options.delete(:action)
+
+ if c && a
+ # When given both options, make sure BOTH are true
+ current_controller?(*c) && current_action?(*a)
+ else
+ # Otherwise check EITHER option
+ current_controller?(*c) || current_action?(*a)
+ end
+ end
+ end
+
+ def current_path?(path)
+ c, a, _ = path.split('#')
+ current_controller?(c) && current_action?(a)
+ end
+
+ def project_tab_class
+ return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
+
+ if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
+ "active"
+ end
+ end
+
+ def branches_tab_class
+ if current_controller?(:protected_branches) ||
+ current_controller?(:branches) ||
+ current_page?(namespace_project_repository_path(@project.namespace,
+ @project))
+ 'active'
+ end
+ end
+
+ # Use nav_tab for save controller/action but different params
+ def nav_tab(key, value, &block)
+ o = {}
+ o[:class] = ""
+
+ if value.nil?
+ o[:class] << " active" if params[key].blank?
+ else
+ o[:class] << " active" if params[key] == value
+ end
+
+ if block_given?
+ content_tag(:li, capture(&block), o)
+ else
+ content_tag(:li, nil, o)
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/tags_helper.rb b/app/helpers/gitlab/tags_helper.rb
new file mode 100644
index 00000000000..d694b2c90ce
--- /dev/null
+++ b/app/helpers/gitlab/tags_helper.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module TagsHelper
+ def tag_path(tag)
+ "/tags/#{tag}"
+ end
+
+ def tag_list(project)
+ html = ''
+ project.tag_list.each do |tag|
+ html << link_to(tag, tag_path(tag))
+ end
+
+ html.html_safe
+ end
+ end
+end
diff --git a/app/helpers/gitlab/tree_helper.rb b/app/helpers/gitlab/tree_helper.rb
new file mode 100644
index 00000000000..dc48ff0e6e2
--- /dev/null
+++ b/app/helpers/gitlab/tree_helper.rb
@@ -0,0 +1,89 @@
+module Gitlab
+ module TreeHelper
+ # Sorts a repository's tree so that folders are before files and renders
+ # their corresponding partials
+ #
+ def render_tree(tree)
+ # Render Folders before Files/Submodules
+ folders, files, submodules = tree.trees, tree.blobs, tree.submodules
+
+ tree = ""
+
+ # Render folders if we have any
+ tree << render(partial: 'projects/tree/tree_item', collection: folders,
+ locals: { type: 'folder' }) if folders.present?
+
+ # Render files if we have any
+ tree << render(partial: 'projects/tree/blob_item', collection: files,
+ locals: { type: 'file' }) if files.present?
+
+ # Render submodules if we have any
+ tree << render(partial: 'projects/tree/submodule_item',
+ collection: submodules) if submodules.present?
+
+ tree.html_safe
+ end
+
+ def render_readme(readme)
+ render_markup(readme.name, readme.data)
+ end
+
+ # Return an image icon depending on the file type and mode
+ #
+ # type - String type of the tree item; either 'folder' or 'file'
+ # mode - File unix mode
+ # name - File name
+ def tree_icon(type, mode, name)
+ icon("#{file_type_icon_class(type, mode, name)} fw")
+ end
+
+ def tree_hex_class(content)
+ "file_#{hexdigest(content.name)}"
+ end
+
+ # Simple shortcut to File.join
+ def tree_join(*args)
+ File.join(*args)
+ end
+
+ def allowed_tree_edit?(project = nil, ref = nil)
+ project ||= @project
+ ref ||= @ref
+ return false unless project.repository.branch_names.include?(ref)
+
+ ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
+ end
+
+ def tree_breadcrumbs(tree, max_links = 2)
+ if @path.present?
+ part_path = ""
+ parts = @path.split('/')
+
+ yield('..', nil) if parts.count > max_links
+
+ parts.each do |part|
+ part_path = File.join(part_path, part) unless part_path.empty?
+ part_path = part if part_path.empty?
+
+ next unless parts.last(2).include?(part) if parts.count > max_links
+ yield(part, tree_join(@ref, part_path))
+ end
+ end
+ end
+
+ def up_dir_path
+ file = File.join(@path, "..")
+ tree_join(@ref, file)
+ end
+
+ # returns the relative path of the first subdir that doesn't have only one directory descendant
+ def flatten_tree(tree)
+ subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
+ if subtree.count == 1 && subtree.first.dir?
+ return tree_join(tree.name, flatten_tree(subtree.first))
+ else
+ return tree.name
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/version_check_helper.rb b/app/helpers/gitlab/version_check_helper.rb
new file mode 100644
index 00000000000..46a12cc8c60
--- /dev/null
+++ b/app/helpers/gitlab/version_check_helper.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module VersionCheckHelper
+ def version_status_badge
+ if Rails.env.production?
+ image_tag VersionCheck.new.url
+ end
+ end
+ end
+end
diff --git a/app/helpers/gitlab/visibility_level_helper.rb b/app/helpers/gitlab/visibility_level_helper.rb
new file mode 100644
index 00000000000..feba901f7d7
--- /dev/null
+++ b/app/helpers/gitlab/visibility_level_helper.rb
@@ -0,0 +1,97 @@
+module Gitlab
+ module VisibilityLevelHelper
+ def visibility_level_color(level)
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ 'vs-private'
+ when Gitlab::VisibilityLevel::INTERNAL
+ 'vs-internal'
+ when Gitlab::VisibilityLevel::PUBLIC
+ 'vs-public'
+ end
+ end
+
+ # Return the description for the +level+ argument.
+ #
+ # +level+ One of the Gitlab::VisibilityLevel constants
+ # +form_model+ Either a model object (Project, Snippet, etc.) or the name of
+ # a Project or Snippet class.
+ def visibility_level_description(level, form_model)
+ case form_model.is_a?(String) ? form_model : form_model.class.name
+ when 'PersonalSnippet', 'ProjectSnippet', 'Snippet'
+ snippet_visibility_level_description(level)
+ when 'Project'
+ project_visibility_level_description(level)
+ end
+ end
+
+ def project_visibility_level_description(level)
+ capture_haml do
+ haml_tag :span do
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ haml_concat "Project access must be granted explicitly for each user."
+ when Gitlab::VisibilityLevel::INTERNAL
+ haml_concat "The project can be cloned by"
+ haml_concat "any logged in user."
+ when Gitlab::VisibilityLevel::PUBLIC
+ haml_concat "The project can be cloned"
+ haml_concat "without any"
+ haml_concat "authentication."
+ end
+ end
+ end
+ end
+
+ def snippet_visibility_level_description(level)
+ capture_haml do
+ haml_tag :span do
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ haml_concat "The snippet is visible only for me."
+ when Gitlab::VisibilityLevel::INTERNAL
+ haml_concat "The snippet is visible for any logged in user."
+ when Gitlab::VisibilityLevel::PUBLIC
+ haml_concat "The snippet can be accessed"
+ haml_concat "without any"
+ haml_concat "authentication."
+ end
+ end
+ end
+ end
+
+ def visibility_level_icon(level)
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ private_icon
+ when Gitlab::VisibilityLevel::INTERNAL
+ internal_icon
+ when Gitlab::VisibilityLevel::PUBLIC
+ public_icon
+ end
+ end
+
+ def visibility_level_label(level)
+ Project.visibility_levels.key(level)
+ end
+
+ def restricted_visibility_levels(show_all = false)
+ return [] if current_user.is_admin? && !show_all
+ current_application_settings.restricted_visibility_levels || []
+ end
+
+ def default_project_visibility
+ current_application_settings.default_project_visibility
+ end
+
+ def default_snippet_visibility
+ current_application_settings.default_snippet_visibility
+ end
+
+ def skip_level?(form_model, level)
+ form_model.is_a?(Project) &&
+ form_model.forked? &&
+ !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level)
+ end
+ end
+end
diff --git a/app/helpers/gitlab/wiki_helper.rb b/app/helpers/gitlab/wiki_helper.rb
new file mode 100644
index 00000000000..02a1daf0019
--- /dev/null
+++ b/app/helpers/gitlab/wiki_helper.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module WikiHelper
+ # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The
+ # only way around this is to implement our own path generators.
+ def namespace_project_wiki_path(namespace, project, wiki_page, *args)
+ slug =
+ case wiki_page
+ when Symbol
+ wiki_page
+ when String
+ wiki_page
+ else
+ wiki_page.slug
+ end
+ namespace_project_path(namespace, project) + "/wikis/#{slug}"
+ end
+
+ def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args)
+ namespace_project_wiki_path(namespace, project, wiki_page) + '/edit'
+ end
+
+ def history_namespace_project_wiki_path(namespace, project, wiki_page, *args)
+ namespace_project_wiki_path(namespace, project, wiki_page) + '/history'
+ end
+ end
+end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
deleted file mode 100644
index eb3f72a307d..00000000000
--- a/app/helpers/gitlab_markdown_helper.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'nokogiri'
-
-module GitlabMarkdownHelper
- include Gitlab::Markdown
- include PreferencesHelper
-
- # Use this in places where you would normally use link_to(gfm(...), ...).
- #
- # It solves a problem occurring with nested links (i.e.
- # "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be
- # interpreted as intended. Browsers will parse something like
- # "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is
- # not linked any more). link_to_gfm corrects that. It wraps all parts to
- # explicitly produce the correct linking behavior (i.e.
- # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
- def link_to_gfm(body, url, html_options = {})
- return "" if body.blank?
-
- escaped_body = if body =~ /\A\<img/
- body
- else
- escape_once(body)
- end
-
- gfm_body = gfm(escaped_body, {}, html_options)
-
- fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body)
- if fragment.children.size == 1 && fragment.children[0].name == 'a'
- # Fragment has only one node, and it's a link generated by `gfm`.
- # Replace it with our requested link.
- text = fragment.children[0].text
- fragment.children[0].replace(link_to(text, url, html_options))
- else
- # Traverse the fragment's first generation of children looking for pure
- # text, wrapping anything found in the requested link
- fragment.children.each do |node|
- next unless node.text?
- node.replace(link_to(node.text, url, html_options))
- end
- end
-
- fragment.to_html.html_safe
- end
-
- MARKDOWN_OPTIONS = {
- no_intra_emphasis: true,
- tables: true,
- fenced_code_blocks: true,
- strikethrough: true,
- lax_spacing: true,
- space_after_headers: true,
- superscript: true,
- footnotes: true
- }.freeze
-
- def markdown(text, options={})
- unless @markdown && options == @options
- @options = options
-
- # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch
- rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options)
-
- # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
- @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS)
- end
-
- @markdown.render(text).html_safe
- end
-
- def asciidoc(text)
- Gitlab::Asciidoc.render(text, {
- commit: @commit,
- project: @project,
- project_wiki: @project_wiki,
- requested_path: @path,
- ref: @ref
- })
- end
-
- # Return the first line of +text+, up to +max_chars+, after parsing the line
- # as Markdown. HTML tags in the parsed output are not counted toward the
- # +max_chars+ limit. If the length limit falls within a tag's contents, then
- # the tag contents are truncated without removing the closing tag.
- def first_line_in_markdown(text, max_chars = nil, options = {})
- md = markdown(text, options).strip
-
- truncate_visible(md, max_chars || md.length) if md.present?
- end
-
- def render_wiki_content(wiki_page)
- case wiki_page.format
- when :markdown
- markdown(wiki_page.content)
- when :asciidoc
- asciidoc(wiki_page.content)
- else
- wiki_page.formatted_content.html_safe
- end
- end
-
- MARKDOWN_TIPS = [
- "End a line with two or more spaces for a line-break, or soft-return",
- "Inline code can be denoted by `surrounding it with backticks`",
- "Blocks of code can be denoted by three backticks ``` or four leading spaces",
- "Emoji can be added by :emoji_name:, for example :thumbsup:",
- "Notify other participants using @user_name",
- "Notify a specific group using @group_name",
- "Notify the entire team using @all",
- "Reference an issue using a hash, for example issue #123",
- "Reference a merge request using an exclamation point, for example MR !123",
- "Italicize words or phrases using *asterisks* or _underscores_",
- "Bold words or phrases using **double asterisks** or __double underscores__",
- "Strikethrough words or phrases using ~~two tildes~~",
- "Make a bulleted list using + pluses, - minuses, or * asterisks",
- "Denote blockquotes using > at the beginning of a line",
- "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___"
- ].freeze
-
- # Returns a random markdown tip for use as a textarea placeholder
- def random_markdown_tip
- MARKDOWN_TIPS.sample
- end
-
- private
-
- # Return +text+, truncated to +max_chars+ characters, excluding any HTML
- # tags.
- def truncate_visible(text, max_chars)
- doc = Nokogiri::HTML.fragment(text)
- content_length = 0
- truncated = false
-
- doc.traverse do |node|
- if node.text? || node.content.empty?
- if truncated
- node.remove
- next
- end
-
- # Handle line breaks within a node
- if node.content.strip.lines.length > 1
- node.content = "#{node.content.lines.first.chomp}..."
- truncated = true
- end
-
- num_remaining = max_chars - content_length
- if node.content.length > num_remaining
- node.content = node.content.truncate(num_remaining)
- truncated = true
- end
- content_length += node.content.length
- end
-
- truncated = truncate_if_block(node, truncated)
- end
-
- doc.to_html
- end
-
- # Used by #truncate_visible. If +node+ is the first block element, and the
- # text hasn't already been truncated, then append "..." to the node contents
- # and return true. Otherwise return false.
- def truncate_if_block(node, truncated)
- if node.element? && node.description.block? && !truncated
- node.content = "#{node.content}..." if node.next_sibling
- true
- else
- truncated
- end
- end
-
- # Returns the text necessary to reference `entity` across projects
- #
- # project - Project to reference
- # entity - Object that responds to `to_reference`
- #
- # Examples:
- #
- # cross_project_reference(project, project.issues.first)
- # # => 'namespace1/project1#123'
- #
- # cross_project_reference(project, project.merge_requests.first)
- # # => 'namespace1/project1!345'
- #
- # Returns a String
- def cross_project_reference(project, entity)
- if entity.respond_to?(:to_reference)
- "#{project.to_reference}#{entity.to_reference}"
- else
- ''
- end
- end
-end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
deleted file mode 100644
index d0fae255a04..00000000000
--- a/app/helpers/gitlab_routing_helper.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# Shorter routing method for project and project items
-# Since update to rails 4.1.9 we are now allowed to use `/` in project routing
-# so we use nested routing for project resources which include project and
-# project namespace. To avoid writing long methods every time we define shortcuts for
-# some of routing.
-#
-# For example instead of this:
-#
-# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request)
-#
-# We can simply use shortcut:
-#
-# merge_request_path(merge_request)
-#
-module GitlabRoutingHelper
- def project_path(project, *args)
- namespace_project_path(project.namespace, project, *args)
- end
-
- def activity_project_path(project, *args)
- activity_namespace_project_path(project.namespace, project, *args)
- end
-
- def edit_project_path(project, *args)
- edit_namespace_project_path(project.namespace, project, *args)
- end
-
- def issue_path(entity, *args)
- namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
- end
-
- def merge_request_path(entity, *args)
- namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args)
- end
-
- def milestone_path(entity, *args)
- namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
- end
-
- def project_url(project, *args)
- namespace_project_url(project.namespace, project, *args)
- end
-
- def edit_project_url(project, *args)
- edit_namespace_project_url(project.namespace, project, *args)
- end
-
- def issue_url(entity, *args)
- namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
- end
-
- def merge_request_url(entity, *args)
- namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
- end
-
- def project_snippet_url(entity, *args)
- namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
- end
-
- def toggle_subscription_path(entity, *args)
- if entity.is_a?(Issue)
- toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity)
- else
- toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
- end
- end
-end
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
deleted file mode 100644
index e1dda20de85..00000000000
--- a/app/helpers/graph_helper.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module GraphHelper
- def get_refs(repo, commit)
- refs = ""
- refs << commit.ref_names(repo).join(' ')
-
- # append note count
- refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0
-
- refs
- end
-
- def parents_zip_spaces(parents, parent_spaces)
- ids = parents.map { |p| p.id }
- ids.zip(parent_spaces)
- end
-end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
deleted file mode 100644
index b067cb54a43..00000000000
--- a/app/helpers/groups_helper.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module GroupsHelper
- def remove_user_from_group_message(group, member)
- if member.user
- "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
- else
- "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
- end
- end
-
- def leave_group_message(group)
- "Are you sure you want to leave \"#{group}\" group?"
- end
-
- def should_user_see_group_roles?(user, group)
- if user
- user.is_admin? || group.members.exists?(user_id: user.id)
- else
- false
- end
- end
-
- def group_icon(group)
- if group.is_a?(String)
- group = Group.find_by(path: group)
- end
-
- if group && group.avatar.present?
- group.avatar.url
- else
- image_path('no_group_avatar.png')
- end
- end
-end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
deleted file mode 100644
index 1cf5b96481a..00000000000
--- a/app/helpers/icons_helper.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-module IconsHelper
- include FontAwesome::Rails::IconHelper
-
- # Creates an icon tag given icon name(s) and possible icon modifiers.
- #
- # Right now this method simply delegates directly to `fa_icon` from the
- # font-awesome-rails gem, but should we ever use a different icon pack in the
- # future we won't have to change hundreds of method calls.
- def icon(names, options = {})
- fa_icon(names, options)
- end
-
- def spinner(text = nil, visible = false)
- css_class = 'loading'
- css_class << ' hide' unless visible
-
- content_tag :div, class: css_class do
- icon('spinner spin') + text
- end
- end
-
- def boolean_to_icon(value)
- if value
- icon('circle', class: 'cgreen')
- else
- icon('power-off', class: 'clgray')
- end
- end
-
- def public_icon
- icon('globe fw')
- end
-
- def internal_icon
- icon('shield fw')
- end
-
- def private_icon
- icon('lock fw')
- end
-
- def file_type_icon_class(type, mode, name)
- if type == 'folder'
- icon_class = 'folder'
- elsif mode == '120000'
- icon_class = 'share'
- else
- # Guess which icon to choose based on file extension.
- # If you think a file extension is missing, feel free to add it on PR
-
- case File.extname(name).downcase
- when '.pdf'
- icon_class = 'file-pdf-o'
- when '.jpg', '.jpeg', '.jif', '.jfif',
- '.jp2', '.jpx', '.j2k', '.j2c',
- '.png', '.gif', '.tif', '.tiff',
- '.svg', '.ico', '.bmp'
- icon_class = 'file-image-o'
- when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip',
- '.xz', '.rar', '.7z'
- icon_class = 'file-archive-o'
- when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac'
- icon_class = 'file-audio-o'
- when '.mp4', '.m4p', '.m4v',
- '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv',
- '.mpg', '.mpeg', '.m2v',
- '.avi', '.mkv', '.flv', '.ogv', '.mov',
- '.3gp', '.3g2'
- icon_class = 'file-video-o'
- when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb'
- icon_class = 'file-word-o'
- when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm',
- '.xlsb', '.xla', '.xlam', '.xll', '.xlw'
- icon_class = 'file-excel-o'
- when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm',
- '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm'
- icon_class = 'file-powerpoint-o'
- else
- icon_class = 'file-text-o'
- end
- end
-
- icon_class
- end
-end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
deleted file mode 100644
index 6ddb37cd0dc..00000000000
--- a/app/helpers/issues_helper.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-module IssuesHelper
- def issue_css_classes(issue)
- classes = "issue"
- classes << " closed" if issue.closed?
- classes << " today" if issue.today?
- classes
- end
-
- # Returns an OpenStruct object suitable for use by <tt>options_from_collection_for_select</tt>
- # to allow filtering issues by an unassigned User or Milestone
- def unassigned_filter
- # Milestone uses :title, Issue uses :name
- OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned')
- end
-
- def url_for_project_issues(project = @project, options = {})
- return '' if project.nil?
-
- if options[:only_path]
- project.issues_tracker.project_path
- else
- project.issues_tracker.project_url
- end
- end
-
- def url_for_new_issue(project = @project, options = {})
- return '' if project.nil?
-
- if options[:only_path]
- project.issues_tracker.new_issue_path
- else
- project.issues_tracker.new_issue_url
- end
- end
-
- def url_for_issue(issue_iid, project = @project, options = {})
- return '' if project.nil?
-
- if options[:only_path]
- project.issues_tracker.issue_path(issue_iid)
- else
- project.issues_tracker.issue_url(issue_iid)
- end
- end
-
- def bulk_update_milestone_options
- options_for_select([['None (backlog)', -1]]) +
- options_from_collection_for_select(project_active_milestones, 'id',
- 'title', params[:milestone_id])
- end
-
- def milestone_options(object)
- options_from_collection_for_select(object.project.milestones.active,
- 'id', 'title', object.milestone_id)
- end
-
- def issue_box_class(item)
- if item.respond_to?(:expired?) && item.expired?
- 'issue-box-expired'
- elsif item.respond_to?(:merged?) && item.merged?
- 'issue-box-merged'
- elsif item.closed?
- 'issue-box-closed'
- else
- 'issue-box-open'
- end
- end
-
- def issue_to_atom(xml, issue)
- xml.entry do
- xml.id namespace_project_issue_url(issue.project.namespace,
- issue.project, issue)
- xml.link href: namespace_project_issue_url(issue.project.namespace,
- issue.project, issue)
- xml.title truncate(issue.title, length: 80)
- xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email)
- xml.author do |author|
- xml.name issue.author_name
- xml.email issue.author_email
- end
- xml.summary issue.title
- end
- end
-
- # Required for Gitlab::Markdown::IssueReferenceFilter
- module_function :url_for_issue
-end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
deleted file mode 100644
index 8036303851b..00000000000
--- a/app/helpers/labels_helper.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-module LabelsHelper
- include ActionView::Helpers::TagHelper
-
- # Link to a Label
- #
- # label - Label object to link to
- # project - Project object which will be used as the context for the label's
- # link. If omitted, defaults to `@project`, or the label's own
- # project.
- # block - An optional block that will be passed to `link_to`, forming the
- # body of the link element. If omitted, defaults to
- # `render_colored_label`.
- #
- # Examples:
- #
- # # Allow the generated link to use the label's own project
- # link_to_label(label)
- #
- # # Force the generated link to use @project
- # @project = Project.first
- # link_to_label(label)
- #
- # # Force the generated link to use a provided project
- # link_to_label(label, project: Project.last)
- #
- # # Customize link body with a block
- # link_to_label(label) { "My Custom Label Text" }
- #
- # Returns a String
- def link_to_label(label, project: nil, &block)
- project ||= @project || label.project
- link = namespace_project_issues_path(project.namespace, project,
- label_name: label.name)
-
- if block_given?
- link_to link, &block
- else
- link_to render_colored_label(label), link
- end
- end
-
- def project_label_names
- @project.labels.pluck(:title)
- end
-
- def render_colored_label(label)
- label_color = label.color || Label::DEFAULT_COLOR
- text_color = text_color_for_bg(label_color)
-
- # Intentionally not using content_tag here so that this method can be called
- # by LabelReferenceFilter
- span = %(<span class="label color-label") +
- %( style="background-color: #{label_color}; color: #{text_color}">) +
- escape_once(label.name) + '</span>'
-
- span.html_safe
- end
-
- def suggested_colors
- [
- '#0033CC',
- '#428BCA',
- '#44AD8E',
- '#A8D695',
- '#5CB85C',
- '#69D100',
- '#004E00',
- '#34495E',
- '#7F8C8D',
- '#A295D6',
- '#5843AD',
- '#8E44AD',
- '#FFECDB',
- '#AD4363',
- '#D10069',
- '#CC0033',
- '#FF0000',
- '#D9534F',
- '#D1D100',
- '#F0AD4E',
- '#AD8D43'
- ]
- end
-
- def text_color_for_bg(bg_color)
- r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
-
- if (r + g + b) > 500
- '#333333'
- else
- '#FFFFFF'
- end
- end
-
- def project_labels_options(project)
- options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
- end
-
- # Required for Gitlab::Markdown::LabelReferenceFilter
- module_function :render_colored_label, :text_color_for_bg, :escape_once
-end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
deleted file mode 100644
index f8169b4f288..00000000000
--- a/app/helpers/merge_requests_helper.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-module MergeRequestsHelper
- def new_mr_path_from_push_event(event)
- target_project = event.project.forked_from_project || event.project
- new_namespace_project_merge_request_path(
- event.project.namespace,
- event.project,
- new_mr_from_push_event(event, target_project)
- )
- end
-
- def new_mr_path_for_fork_from_push_event(event)
- new_namespace_project_merge_request_path(
- event.project.namespace,
- event.project,
- new_mr_from_push_event(event, event.project.forked_from_project)
- )
- end
-
- def new_mr_from_push_event(event, target_project)
- {
- merge_request: {
- source_project_id: event.project.id,
- target_project_id: target_project.id,
- source_branch: event.branch_name,
- target_branch: target_project.repository.root_ref
- }
- }
- end
-
- def mr_css_classes(mr)
- classes = "merge-request"
- classes << " closed" if mr.closed?
- classes << " merged" if mr.merged?
- classes
- end
-
- def ci_build_details_path(merge_request)
- merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
- end
-
- def merge_path_description(merge_request, separator)
- if merge_request.for_fork?
- "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}"
- else
- "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}"
- end
- end
-
- def issues_sentence(issues)
- issues.map { |i| "##{i.iid}" }.to_sentence
- end
-
- def mr_change_branches_path(merge_request)
- new_namespace_project_merge_request_path(
- @project.namespace, @project,
- merge_request: {
- source_project_id: @merge_request.source_project_id,
- target_project_id: @merge_request.target_project_id,
- source_branch: @merge_request.source_branch,
- target_branch: nil
- }
- )
- end
-
- def source_branch_with_namespace(merge_request)
- if merge_request.for_fork?
- namespace = link_to(merge_request.source_project_namespace,
- project_path(merge_request.source_project))
- namespace + ":#{merge_request.source_branch}"
- else
- merge_request.source_branch
- end
- end
-end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
deleted file mode 100644
index 132a893e532..00000000000
--- a/app/helpers/milestones_helper.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module MilestonesHelper
- def milestones_filter_path(opts = {})
- if @project
- namespace_project_milestones_path(@project.namespace, @project, opts)
- elsif @group
- group_milestones_path(@group, opts)
- else
- dashboard_milestones_path(opts)
- end
- end
-
- def milestone_progress_bar(milestone)
- options = {
- class: 'progress-bar progress-bar-success',
- style: "width: #{milestone.percent_complete}%;"
- }
-
- content_tag :div, class: 'progress' do
- content_tag :div, nil, options
- end
- end
-
- def projects_milestones_options
- milestones =
- if @project
- @project.milestones
- else
- Milestone.where(project_id: @projects)
- end.active
-
- grouped_milestones = Milestones::GroupService.new(milestones).execute
- grouped_milestones.unshift(Milestone::None)
-
- options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title])
- end
-end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
deleted file mode 100644
index b3132a1f3ba..00000000000
--- a/app/helpers/namespaces_helper.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module NamespacesHelper
- def namespaces_options(selected = :current_user, scope = :default)
- groups = current_user.owned_groups + current_user.masters_groups
- users = [current_user.namespace]
-
- group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
- users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ]
-
- options = []
- options << group_opts
- options << users_opts
-
- if selected == :current_user && current_user.namespace
- selected = current_user.namespace.id
- end
-
- grouped_options_for_select(options, selected)
- end
-
- def namespace_select_tag(id, opts = {})
- css_class = "ajax-namespace-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << (opts[:class] || '')
- value = opts[:selected] || ''
-
- hidden_field_tag(id, value, class: css_class)
- end
-
- def namespace_icon(namespace, size = 40)
- if namespace.kind_of?(Group)
- group_icon(namespace)
- else
- avatar_icon(namespace.owner.email, size)
- end
- end
-end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
deleted file mode 100644
index 9b1dd8b8e54..00000000000
--- a/app/helpers/nav_helper.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module NavHelper
- def nav_menu_collapsed?
- cookies[:collapsed_nav] == 'true'
- end
-
- def nav_sidebar_class
- if nav_menu_collapsed?
- "page-sidebar-collapsed"
- else
- "page-sidebar-expanded"
- end
- end
-
- def nav_header_class
- if nav_menu_collapsed?
- "header-collapsed"
- else
- "header-expanded"
- end
- end
-end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
deleted file mode 100644
index 5f0c921413a..00000000000
--- a/app/helpers/notes_helper.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-module NotesHelper
- # Helps to distinguish e.g. commit notes in mr notes list
- def note_for_main_target?(note)
- (@noteable.class.name == note.noteable_type && !note.for_diff_line?)
- end
-
- def note_target_fields(note)
- hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
- hidden_field_tag(:target_id, note.noteable.id)
- end
-
- def note_editable?(note)
- note.editable? && can?(current_user, :admin_note, note)
- end
-
- def link_to_commit_diff_line_note(note)
- if note.for_commit_diff_line?
- link_to(
- "#{note.diff_file_name}:L#{note.diff_new_line}",
- namespace_project_commit_path(@project.namespace, @project,
- note.noteable, anchor: note.line_code)
- )
- end
- end
-
- def noteable_json(noteable)
- {
- id: noteable.id,
- class: noteable.class.name,
- resources: noteable.class.table_name,
- project_id: noteable.project.id,
- }.to_json
- end
-
- def link_to_new_diff_note(line_code, line_type = nil)
- discussion_id = Note.build_discussion_id(
- @comments_target[:noteable_type],
- @comments_target[:noteable_id] || @comments_target[:commit_id],
- line_code
- )
-
- data = {
- noteable_type: @comments_target[:noteable_type],
- noteable_id: @comments_target[:noteable_id],
- commit_id: @comments_target[:commit_id],
- line_code: line_code,
- discussion_id: discussion_id,
- line_type: line_type
- }
-
- button_tag(class: 'btn add-diff-note js-add-diff-note-button',
- data: data,
- title: 'Add a comment to this line') do
- icon('comment-o')
- end
- end
-
- def link_to_reply_diff(note, line_type = nil)
- return unless current_user
-
- data = {
- noteable_type: note.noteable_type,
- noteable_id: note.noteable_id,
- commit_id: note.commit_id,
- line_code: note.line_code,
- discussion_id: note.discussion_id,
- line_type: line_type
- }
-
- button_tag class: 'btn reply-btn js-discussion-reply-button',
- data: data, title: 'Add a reply' do
- link_text = icon('comment')
- link_text << ' Reply'
- end
- end
-end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
deleted file mode 100644
index 2f8e64c375f..00000000000
--- a/app/helpers/notifications_helper.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module NotificationsHelper
- include IconsHelper
-
- def notification_icon(notification)
- if notification.disabled?
- icon('volume-off', class: 'ns-mute')
- elsif notification.participating?
- icon('volume-down', class: 'ns-part')
- elsif notification.watch?
- icon('volume-up', class: 'ns-watch')
- else
- icon('circle-o', class: 'ns-default')
- end
- end
-end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
deleted file mode 100644
index 01b6a63552c..00000000000
--- a/app/helpers/page_layout_helper.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module PageLayoutHelper
- def page_title(*titles)
- @page_title ||= []
-
- @page_title.push(*titles.compact) if titles.any?
-
- @page_title.join(" | ")
- end
-
- def header_title(title = nil, title_url = nil)
- if title
- @header_title = title
- @header_title_url = title_url
- else
- @header_title_url ? link_to(@header_title, @header_title_url) : @header_title
- end
- end
-
- def sidebar(name = nil)
- if name
- @sidebar = name
- else
- @sidebar
- end
- end
-end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
deleted file mode 100644
index ea774e28ecf..00000000000
--- a/app/helpers/preferences_helper.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# Helper methods for per-User preferences
-module PreferencesHelper
- COLOR_SCHEMES = {
- 1 => 'white',
- 2 => 'dark',
- 3 => 'solarized-light',
- 4 => 'solarized-dark',
- 5 => 'monokai',
- }
- COLOR_SCHEMES.default = 'white'
-
- # Helper method to access the COLOR_SCHEMES
- #
- # The keys are the `color_scheme_ids`
- # The values are the `name` of the scheme.
- #
- # The preview images are `name-scheme-preview.png`
- # The stylesheets should use the css class `.name`
- def color_schemes
- COLOR_SCHEMES.freeze
- end
-
- # Maps `dashboard` values to more user-friendly option text
- DASHBOARD_CHOICES = {
- projects: 'Your Projects (default)',
- stars: 'Starred Projects'
- }.with_indifferent_access.freeze
-
- # Returns an Array usable by a select field for more user-friendly option text
- def dashboard_choices
- defined = User.dashboards
-
- if defined.size != DASHBOARD_CHOICES.size
- # Ensure that anyone adding new options updates this method too
- raise RuntimeError, "`User` defines #{defined.size} dashboard choices," +
- " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
- else
- defined.map do |key, _|
- # Use `fetch` so `KeyError` gets raised when a key is missing
- [DASHBOARD_CHOICES.fetch(key), key]
- end
- end
- end
-
- def project_view_choices
- [
- ['Readme (default)', :readme],
- ['Activity view', :activity]
- ]
- end
-
- def user_application_theme
- theme = Gitlab::Themes.by_id(current_user.try(:theme_id))
- theme.css_class
- end
-
- def user_color_scheme_class
- COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user)
- end
-
- def prefer_readme?
- !current_user ||
- current_user.project_view == 'readme'
- end
-end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
deleted file mode 100644
index ab9b068de05..00000000000
--- a/app/helpers/projects_helper.rb
+++ /dev/null
@@ -1,330 +0,0 @@
-module ProjectsHelper
- def remove_from_project_team_message(project, member)
- if member.user
- "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
- else
- "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
- end
- end
-
- def link_to_project(project)
- link_to [project.namespace.becomes(Namespace), project] do
- title = content_tag(:span, project.name, class: 'project-name')
-
- if project.namespace
- namespace = content_tag(:span, "#{project.namespace.human_name} / ", class: 'namespace-name')
- title = namespace + title
- end
-
- title
- end
- end
-
- def link_to_member(project, author, opts = {})
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author' }
- opts = default_opts.merge(opts)
-
- return "(deleted)" unless author
-
- author_html = ""
-
- # Build avatar image tag
- author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
-
- # Build name span tag
- author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
-
- author_html = author_html.html_safe
-
- if opts[:name]
- link_to(author_html, user_path(author), class: "author_link").html_safe
- else
- link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe
- end
- end
-
- def project_title(project)
- if project.group
- content_tag :span do
- link_to(
- simple_sanitize(project.group.name), group_path(project.group)
- ) + ' / ' +
- link_to(simple_sanitize(project.name),
- project_path(project))
- end
- else
- owner = project.namespace.owner
- content_tag :span do
- link_to(
- simple_sanitize(owner.name), user_path(owner)
- ) + ' / ' +
- link_to(simple_sanitize(project.name),
- project_path(project))
- end
- end
- end
-
- def remove_project_message(project)
- "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?"
- end
-
- def transfer_project_message(project)
- "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
- end
-
- def project_nav_tabs
- @nav_tabs ||= get_project_nav_tabs(@project, current_user)
- end
-
- def project_nav_tab?(name)
- project_nav_tabs.include? name
- end
-
- def project_active_milestones
- @project.milestones.active.order("due_date, title ASC")
- end
-
- def project_for_deploy_key(deploy_key)
- if deploy_key.projects.include?(@project)
- @project
- else
- deploy_key.projects.find { |project| can?(current_user, :read_project, project) }
- end
- end
-
- def can_change_visibility_level?(project, current_user)
- return false unless can?(current_user, :change_visibility_level, project)
-
- if project.forked?
- project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE
- else
- true
- end
- end
-
- private
-
- def get_project_nav_tabs(project, current_user)
- nav_tabs = [:home]
-
- if !project.empty_repo? && can?(current_user, :download_code, project)
- nav_tabs << [:files, :commits, :network, :graphs]
- end
-
- if project.repo_exists? && can?(current_user, :read_merge_request, project)
- nav_tabs << :merge_requests
- end
-
- if can?(current_user, :admin_project, project)
- nav_tabs << :settings
- end
-
- if can?(current_user, :read_issue, project)
- nav_tabs << :issues
- end
-
- if can?(current_user, :read_wiki, project)
- nav_tabs << :wiki
- end
-
- if can?(current_user, :read_project_snippet, project)
- nav_tabs << :snippets
- end
-
- if can?(current_user, :read_label, project)
- nav_tabs << :labels
- end
-
- if can?(current_user, :read_milestone, project)
- nav_tabs << :milestones
- end
-
- nav_tabs.flatten
- end
-
- def git_user_name
- if current_user
- current_user.name
- else
- "Your name"
- end
- end
-
- def git_user_email
- if current_user
- current_user.email
- else
- "your@email.com"
- end
- end
-
- def repository_size(project = nil)
- "#{(project || @project).repository_size} MB"
- rescue
- # In order to prevent 500 error
- # when application cannot allocate memory
- # to calculate repo size - just show 'Unknown'
- 'unknown'
- end
-
- def default_url_to_repo(project = nil)
- project = project || @project
- current_user ? project.url_to_repo : project.http_url_to_repo
- end
-
- def default_clone_protocol
- current_user ? "ssh" : "http"
- end
-
- def project_last_activity(project)
- if project.last_activity_at
- time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
- else
- "Never"
- end
- end
-
- def add_contribution_guide_path(project)
- if project && !project.repository.contribution_guide
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "CONTRIBUTING.md",
- commit_message: "Add contribution guide"
- )
- end
- end
-
- def add_changelog_path(project)
- if project && !project.repository.changelog
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "CHANGELOG",
- commit_message: "Add changelog"
- )
- end
- end
-
- def add_license_path(project)
- if project && !project.repository.license
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "LICENSE",
- commit_message: "Add license"
- )
- end
- end
-
- def contribution_guide_path(project)
- if project && contribution_guide = project.repository.contribution_guide
- namespace_project_blob_path(
- project.namespace,
- project,
- tree_join(project.default_branch,
- contribution_guide.name)
- )
- end
- end
-
- def readme_path(project)
- filename_path(project, :readme)
- end
-
- def changelog_path(project)
- filename_path(project, :changelog)
- end
-
- def license_path(project)
- filename_path(project, :license)
- end
-
- def version_path(project)
- filename_path(project, :version)
- end
-
- def hidden_pass_url(original_url)
- result = URI(original_url)
- result.password = '*****' unless result.password.nil?
- result
- rescue
- original_url
- end
-
- def project_wiki_path_with_version(proj, page, version, is_newest)
- url_params = is_newest ? {} : { version_id: version }
- namespace_project_wiki_path(proj.namespace, proj, page, url_params)
- end
-
- def project_status_css_class(status)
- case status
- when "started"
- "active"
- when "failed"
- "danger"
- when "finished"
- "success"
- end
- end
-
- def user_max_access_in_project(user, project)
- level = project.team.max_member_access(user)
-
- if level
- Gitlab::Access.options_with_owner.key(level)
- end
- end
-
- def leave_project_message(project)
- "Are you sure you want to leave \"#{project.name}\" project?"
- end
-
- def new_readme_path
- ref = @repository.root_ref if @repository
- ref ||= 'master'
-
- namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
- end
-
- def last_push_event
- if current_user
- current_user.recent_push(@project.id)
- end
- end
-
- def readme_cache_key
- sha = @project.commit.try(:sha) || 'nil'
- [@project.id, sha, "readme"].join('-')
- end
-
- def round_commit_count(project)
- count = project.commit_count
-
- if count > 10000
- '10000+'
- elsif count > 5000
- '5000+'
- elsif count > 1000
- '1000+'
- else
- count
- end
- end
-
- private
-
- def filename_path(project, filename)
- if project && blob = project.repository.send(filename)
- namespace_project_blob_path(
- project.namespace,
- project,
- tree_join(project.default_branch,
- blob.name)
- )
- end
- end
-end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
deleted file mode 100644
index c31a556ff7b..00000000000
--- a/app/helpers/search_helper.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-module SearchHelper
- def search_autocomplete_opts(term)
- return unless current_user
-
- resources_results = [
- groups_autocomplete(term),
- projects_autocomplete(term)
- ].flatten
-
- generic_results = project_autocomplete + default_autocomplete + help_autocomplete
- generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
-
- [
- resources_results,
- generic_results
- ].flatten.uniq do |item|
- item[:label]
- end
- end
-
- private
-
- # Autocomplete results for various settings pages
- def default_autocomplete
- [
- { label: "Profile settings", url: profile_path },
- { label: "SSH Keys", url: profile_keys_path },
- { label: "Dashboard", url: root_path },
- { label: "Admin Section", url: admin_root_path },
- ]
- end
-
- # Autocomplete results for internal help pages
- def help_autocomplete
- [
- { label: "help: API Help", url: help_page_path("api", "README") },
- { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") },
- { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") },
- { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") },
- { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
- { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
- { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") },
- { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
- ]
- end
-
- # Autocomplete results for the current project, if it's defined
- def project_autocomplete
- if @project && @project.repository.exists? && @project.repository.root_ref
- prefix = search_result_sanitize(@project.name_with_namespace)
- ref = @ref || @project.repository.root_ref
-
- [
- { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) },
- { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
- { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
- { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
- { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
- ]
- else
- []
- end
- end
-
- # Autocomplete results for the current user's groups
- def groups_autocomplete(term, limit = 5)
- current_user.authorized_groups.search(term).limit(limit).map do |group|
- {
- label: "group: #{search_result_sanitize(group.name)}",
- url: group_path(group)
- }
- end
- end
-
- # Autocomplete results for the current user's projects
- def projects_autocomplete(term, limit = 5)
- ProjectsFinder.new.execute(current_user).search_by_title(term).
- sorted_by_stars.non_archived.limit(limit).map do |p|
- {
- label: "project: #{search_result_sanitize(p.name_with_namespace)}",
- url: namespace_project_path(p.namespace, p)
- }
- end
- end
-
- def search_result_sanitize(str)
- Sanitize.clean(str)
- end
-
- def search_filter_path(options={})
- exist_opts = {
- search: params[:search],
- project_id: params[:project_id],
- group_id: params[:group_id],
- scope: params[:scope]
- }
-
- options = exist_opts.merge(options)
- search_path(options)
- end
-
- # Sanitize html generated after parsing markdown from issue description or comment
- def search_md_sanitize(html)
- sanitize(html, tags: %w(a p ol ul li pre code))
- end
-end
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
deleted file mode 100644
index 12fce8db701..00000000000
--- a/app/helpers/selects_helper.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module SelectsHelper
- def users_select_tag(id, opts = {})
- css_class = "ajax-users-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << (opts[:class] || '')
- value = opts[:selected] || ''
- placeholder = opts[:placeholder] || 'Search for a user'
-
- null_user = opts[:null_user] || false
- any_user = opts[:any_user] || false
- email_user = opts[:email_user] || false
- first_user = opts[:first_user] && current_user ? current_user.username : false
- current_user = opts[:current_user] || false
- project = opts[:project] || @project
-
- html = {
- class: css_class,
- 'data-placeholder' => placeholder,
- 'data-null-user' => null_user,
- 'data-any-user' => any_user,
- 'data-email-user' => email_user,
- 'data-first-user' => first_user,
- 'data-current-user' => current_user
- }
-
- unless opts[:scope] == :all
- if project
- html['data-project-id'] = project.id
- elsif @group
- html['data-group-id'] = @group.id
- end
- end
-
- hidden_field_tag(id, value, html)
- end
-
- def groups_select_tag(id, opts = {})
- css_class = "ajax-groups-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << (opts[:class] || '')
- value = opts[:selected] || ''
-
- hidden_field_tag(id, value, class: css_class)
- end
-end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
deleted file mode 100644
index 906cb12cd48..00000000000
--- a/app/helpers/snippets_helper.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module SnippetsHelper
- def lifetime_select_options
- options = [
- ['forever', nil],
- ['1 day', "#{Date.current + 1.day}"],
- ['1 week', "#{Date.current + 1.week}"],
- ['1 month', "#{Date.current + 1.month}"]
- ]
- options_for_select(options)
- end
-
- def reliable_snippet_path(snippet)
- if snippet.project_id?
- namespace_project_snippet_path(snippet.project.namespace,
- snippet.project, snippet)
- else
- snippet_path(snippet)
- end
- end
-end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
deleted file mode 100644
index bb12d43f397..00000000000
--- a/app/helpers/sorting_helper.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-module SortingHelper
- def sort_options_hash
- {
- sort_value_name => sort_title_name,
- sort_value_recently_updated => sort_title_recently_updated,
- sort_value_oldest_updated => sort_title_oldest_updated,
- sort_value_recently_created => sort_title_recently_created,
- sort_value_oldest_created => sort_title_oldest_created,
- sort_value_milestone_soon => sort_title_milestone_soon,
- sort_value_milestone_later => sort_title_milestone_later,
- sort_value_largest_repo => sort_title_largest_repo,
- sort_value_recently_signin => sort_title_recently_signin,
- sort_value_oldest_signin => sort_title_oldest_signin,
- }
- end
-
- def sort_title_oldest_updated
- 'Oldest updated'
- end
-
- def sort_title_recently_updated
- 'Recently updated'
- end
-
- def sort_title_oldest_created
- 'Oldest created'
- end
-
- def sort_title_recently_created
- 'Recently created'
- end
-
- def sort_title_milestone_soon
- 'Milestone due soon'
- end
-
- def sort_title_milestone_later
- 'Milestone due later'
- end
-
- def sort_title_name
- 'Name'
- end
-
- def sort_title_largest_repo
- 'Largest repository'
- end
-
- def sort_title_recently_signin
- 'Recent sign in'
- end
-
- def sort_title_oldest_signin
- 'Oldest sign in'
- end
-
- def sort_value_oldest_updated
- 'updated_asc'
- end
-
- def sort_value_recently_updated
- 'updated_desc'
- end
-
- def sort_value_oldest_created
- 'created_asc'
- end
-
- def sort_value_recently_created
- 'created_desc'
- end
-
- def sort_value_milestone_soon
- 'milestone_due_asc'
- end
-
- def sort_value_milestone_later
- 'milestone_due_desc'
- end
-
- def sort_value_name
- 'name_asc'
- end
-
- def sort_value_largest_repo
- 'repository_size_desc'
- end
-
- def sort_value_recently_signin
- 'recent_sign_in'
- end
-
- def sort_value_oldest_signin
- 'oldest_sign_in'
- end
-end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
deleted file mode 100644
index b3f50ceebe4..00000000000
--- a/app/helpers/submodule_helper.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-module SubmoduleHelper
- include Gitlab::ShellAdapter
-
- # links to files listing for submodule if submodule is a project on this server
- def submodule_links(submodule_item, ref = nil, repository = @repository)
- url = repository.submodule_url_for(ref, submodule_item.path)
-
- return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
-
- namespace = $1
- project = $2
- project.chomp!('.git')
-
- if self_url?(url, namespace, project)
- return namespace_project_path(namespace, project),
- namespace_project_tree_path(namespace, project,
- submodule_item.id)
- elsif relative_self_url?(url)
- relative_self_links(url, submodule_item.id)
- elsif github_dot_com_url?(url)
- standard_links('github.com', namespace, project, submodule_item.id)
- elsif gitlab_dot_com_url?(url)
- standard_links('gitlab.com', namespace, project, submodule_item.id)
- else
- return url, nil
- end
- end
-
- protected
-
- def github_dot_com_url?(url)
- url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/
- end
-
- def gitlab_dot_com_url?(url)
- url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/
- end
-
- def self_url?(url, namespace, project)
- return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/',
- project, '.git' ].join('')
- url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
- end
-
- def relative_self_url?(url)
- # (./)?(../repo.git) || (./)?(../../project/repo.git) )
- url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
- end
-
- def standard_links(host, namespace, project, commit)
- base = [ 'https://', host, '/', namespace, '/', project ].join('')
- [base, [ base, '/tree/', commit ].join('')]
- end
-
- def relative_self_links(url, commit)
- # Map relative links to a namespace and project
- # For example:
- # ../bar.git -> same namespace, repo bar
- # ../foo/bar.git -> namespace foo, repo bar
- # ../../foo/bar/baz.git -> namespace bar, repo baz
- components = url.split('/')
- base = components.pop.gsub(/.git$/, '')
- namespace = components.pop.gsub(/^\.\.$/, '')
-
- if namespace.empty?
- namespace = @project.namespace.path
- end
-
- [
- namespace_project_path(namespace, base),
- namespace_project_tree_path(namespace, base, commit)
- ]
- end
-end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
deleted file mode 100644
index 0e7d8065ac7..00000000000
--- a/app/helpers/tab_helper.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-module TabHelper
- # Navigation link helper
- #
- # Returns an `li` element with an 'active' class if the supplied
- # controller(s) and/or action(s) are currently active. The content of the
- # element is the value passed to the block.
- #
- # options - The options hash used to determine if the element is "active" (default: {})
- # :controller - One or more controller names to check (optional).
- # :action - One or more action names to check (optional).
- # :path - A shorthand path, such as 'dashboard#index', to check (optional).
- # :html_options - Extra options to be passed to the list element (optional).
- # block - An optional block that will become the contents of the returned
- # `li` element.
- #
- # When both :controller and :action are specified, BOTH must match in order
- # to be marked as active. When only one is given, either can match.
- #
- # Examples
- #
- # # Assuming we're on TreeController#show
- #
- # # Controller matches, but action doesn't
- # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" }
- # # => '<li>Hello</li>'
- #
- # # Controller matches
- # nav_link(controller: [:tree, :refs]) { "Hello" }
- # # => '<li class="active">Hello</li>'
- #
- # # Several paths
- # nav_link(path: ['tree#show', 'profile#show']) { "Hello" }
- # # => '<li class="active">Hello</li>'
- #
- # # Shorthand path
- # nav_link(path: 'tree#show') { "Hello" }
- # # => '<li class="active">Hello</li>'
- #
- # # Supplying custom options for the list element
- # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
- # # => '<li class="home active">Hello</li>'
- #
- # Returns a list item element String
- def nav_link(options = {}, &block)
- klass = active_nav_link?(options) ? 'active' : ''
-
- # Add our custom class into the html_options, which may or may not exist
- # and which may or may not already have a :class key
- o = options.delete(:html_options) || {}
- o[:class] ||= ''
- o[:class] += ' ' + klass
- o[:class].strip!
-
- if block_given?
- content_tag(:li, capture(&block), o)
- else
- content_tag(:li, nil, o)
- end
- end
-
- def active_nav_link?(options)
- if path = options.delete(:path)
- unless path.respond_to?(:each)
- path = [path]
- end
-
- path.any? do |single_path|
- current_path?(single_path)
- end
- elsif page = options.delete(:page)
- unless page.respond_to?(:each)
- page = [page]
- end
-
- page.any? do |single_page|
- current_page?(single_page)
- end
- else
- c = options.delete(:controller)
- a = options.delete(:action)
-
- if c && a
- # When given both options, make sure BOTH are true
- current_controller?(*c) && current_action?(*a)
- else
- # Otherwise check EITHER option
- current_controller?(*c) || current_action?(*a)
- end
- end
- end
-
- def current_path?(path)
- c, a, _ = path.split('#')
- current_controller?(c) && current_action?(a)
- end
-
- def project_tab_class
- return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
-
- if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name
- "active"
- end
- end
-
- def branches_tab_class
- if current_controller?(:protected_branches) ||
- current_controller?(:branches) ||
- current_page?(namespace_project_repository_path(@project.namespace,
- @project))
- 'active'
- end
- end
-
- # Use nav_tab for save controller/action but different params
- def nav_tab(key, value, &block)
- o = {}
- o[:class] = ""
-
- if value.nil?
- o[:class] << " active" if params[key].blank?
- else
- o[:class] << " active" if params[key] == value
- end
-
- if block_given?
- content_tag(:li, capture(&block), o)
- else
- content_tag(:li, nil, o)
- end
- end
-end
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
deleted file mode 100644
index fb85544df2d..00000000000
--- a/app/helpers/tags_helper.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-module TagsHelper
- def tag_path(tag)
- "/tags/#{tag}"
- end
-
- def tag_list(project)
- html = ''
- project.tag_list.each do |tag|
- html << link_to(tag, tag_path(tag))
- end
-
- html.html_safe
- end
-end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
deleted file mode 100644
index 03a49e119b8..00000000000
--- a/app/helpers/tree_helper.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-module TreeHelper
- # Sorts a repository's tree so that folders are before files and renders
- # their corresponding partials
- #
- # contents - A Grit::Tree object for the current tree
- def render_tree(tree)
- # Render Folders before Files/Submodules
- folders, files, submodules = tree.trees, tree.blobs, tree.submodules
-
- tree = ""
-
- # Render folders if we have any
- tree << render(partial: 'projects/tree/tree_item', collection: folders,
- locals: { type: 'folder' }) if folders.present?
-
- # Render files if we have any
- tree << render(partial: 'projects/tree/blob_item', collection: files,
- locals: { type: 'file' }) if files.present?
-
- # Render submodules if we have any
- tree << render(partial: 'projects/tree/submodule_item',
- collection: submodules) if submodules.present?
-
- tree.html_safe
- end
-
- def render_readme(readme)
- render_markup(readme.name, readme.data)
- end
-
- # Return an image icon depending on the file type and mode
- #
- # type - String type of the tree item; either 'folder' or 'file'
- # mode - File unix mode
- # name - File name
- def tree_icon(type, mode, name)
- icon("#{file_type_icon_class(type, mode, name)} fw")
- end
-
- def tree_hex_class(content)
- "file_#{hexdigest(content.name)}"
- end
-
- # Simple shortcut to File.join
- def tree_join(*args)
- File.join(*args)
- end
-
- def allowed_tree_edit?(project = nil, ref = nil)
- project ||= @project
- ref ||= @ref
- return false unless project.repository.branch_names.include?(ref)
-
- ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
- end
-
- def tree_breadcrumbs(tree, max_links = 2)
- if @path.present?
- part_path = ""
- parts = @path.split('/')
-
- yield('..', nil) if parts.count > max_links
-
- parts.each do |part|
- part_path = File.join(part_path, part) unless part_path.empty?
- part_path = part if part_path.empty?
-
- next unless parts.last(2).include?(part) if parts.count > max_links
- yield(part, tree_join(@ref, part_path))
- end
- end
- end
-
- def up_dir_path
- file = File.join(@path, "..")
- tree_join(@ref, file)
- end
-
- # returns the relative path of the first subdir that doesn't have only one directory descendant
- def flatten_tree(tree)
- subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
- if subtree.count == 1 && subtree.first.dir?
- return tree_join(tree.name, flatten_tree(subtree.first))
- else
- return tree.name
- end
- end
-end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
deleted file mode 100644
index f64d730b448..00000000000
--- a/app/helpers/version_check_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module VersionCheckHelper
- def version_status_badge
- if Rails.env.production?
- image_tag VersionCheck.new.url
- end
- end
-end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
deleted file mode 100644
index b52cd23aba2..00000000000
--- a/app/helpers/visibility_level_helper.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-module VisibilityLevelHelper
- def visibility_level_color(level)
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- 'vs-private'
- when Gitlab::VisibilityLevel::INTERNAL
- 'vs-internal'
- when Gitlab::VisibilityLevel::PUBLIC
- 'vs-public'
- end
- end
-
- # Return the description for the +level+ argument.
- #
- # +level+ One of the Gitlab::VisibilityLevel constants
- # +form_model+ Either a model object (Project, Snippet, etc.) or the name of
- # a Project or Snippet class.
- def visibility_level_description(level, form_model)
- case form_model.is_a?(String) ? form_model : form_model.class.name
- when 'PersonalSnippet', 'ProjectSnippet', 'Snippet'
- snippet_visibility_level_description(level)
- when 'Project'
- project_visibility_level_description(level)
- end
- end
-
- def project_visibility_level_description(level)
- capture_haml do
- haml_tag :span do
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "Project access must be granted explicitly for each user."
- when Gitlab::VisibilityLevel::INTERNAL
- haml_concat "The project can be cloned by"
- haml_concat "any logged in user."
- when Gitlab::VisibilityLevel::PUBLIC
- haml_concat "The project can be cloned"
- haml_concat "without any"
- haml_concat "authentication."
- end
- end
- end
- end
-
- def snippet_visibility_level_description(level)
- capture_haml do
- haml_tag :span do
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "The snippet is visible only for me."
- when Gitlab::VisibilityLevel::INTERNAL
- haml_concat "The snippet is visible for any logged in user."
- when Gitlab::VisibilityLevel::PUBLIC
- haml_concat "The snippet can be accessed"
- haml_concat "without any"
- haml_concat "authentication."
- end
- end
- end
- end
-
- def visibility_level_icon(level)
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- private_icon
- when Gitlab::VisibilityLevel::INTERNAL
- internal_icon
- when Gitlab::VisibilityLevel::PUBLIC
- public_icon
- end
- end
-
- def visibility_level_label(level)
- Project.visibility_levels.key(level)
- end
-
- def restricted_visibility_levels(show_all = false)
- return [] if current_user.is_admin? && !show_all
- current_application_settings.restricted_visibility_levels || []
- end
-
- def default_project_visibility
- current_application_settings.default_project_visibility
- end
-
- def default_snippet_visibility
- current_application_settings.default_snippet_visibility
- end
-
- def skip_level?(form_model, level)
- form_model.is_a?(Project) &&
- form_model.forked? &&
- !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level)
- end
-end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
deleted file mode 100644
index f8a96516e61..00000000000
--- a/app/helpers/wiki_helper.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module WikiHelper
- # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The
- # only way around this is to implement our own path generators.
- def namespace_project_wiki_path(namespace, project, wiki_page, *args)
- slug =
- case wiki_page
- when Symbol
- wiki_page
- when String
- wiki_page
- else
- wiki_page.slug
- end
- namespace_project_path(namespace, project) + "/wikis/#{slug}"
- end
-
- def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args)
- namespace_project_wiki_path(namespace, project, wiki_page) + '/edit'
- end
-
- def history_namespace_project_wiki_path(namespace, project, wiki_page, *args)
- namespace_project_wiki_path(namespace, project, wiki_page) + '/history'
- end
-end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index aedb0889185..2b650bc6eac 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,6 +1,6 @@
class BaseMailer < ActionMailer::Base
- add_template_helper ApplicationHelper
- add_template_helper GitlabMarkdownHelper
+ add_template_helper Gitlab::ApplicationHelper
+ add_template_helper Gitlab::GitlabMarkdownHelper
attr_accessor :current_user
helper_method :current_user, :can?
diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb
new file mode 100644
index 00000000000..6fb4fba85e5
--- /dev/null
+++ b/app/mailers/ci/emails/builds.rb
@@ -0,0 +1,17 @@
+module Ci
+ module Emails
+ module Builds
+ def build_fail_email(build_id, to)
+ @build = Ci::Build.find(build_id)
+ @project = @build.project
+ mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
+ end
+
+ def build_success_email(build_id, to)
+ @build = Ci::Build.find(build_id)
+ @project = @build.project
+ mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
+ end
+ end
+ end
+end
diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb
new file mode 100644
index 00000000000..44e490e9b36
--- /dev/null
+++ b/app/mailers/ci/notify.rb
@@ -0,0 +1,47 @@
+module Ci
+ class Notify < ActionMailer::Base
+ include Ci::Emails::Builds
+
+ add_template_helper Ci::ApplicationHelper
+ add_template_helper Ci::GitlabHelper
+
+ default_url_options[:host] = GitlabCi.config.gitlab_ci.host
+ default_url_options[:protocol] = GitlabCi.config.gitlab_ci.protocol
+ default_url_options[:port] = GitlabCi.config.gitlab_ci.port if GitlabCi.config.gitlab_ci_on_non_standard_port?
+ default_url_options[:script_name] = GitlabCi.config.gitlab_ci.relative_url_root
+
+ default from: GitlabCi.config.gitlab_ci.email_from
+
+ # Just send email with 3 seconds delay
+ def self.delay
+ delay_for(2.seconds)
+ end
+
+ private
+
+ # Formats arguments into a String suitable for use as an email subject
+ #
+ # extra - Extra Strings to be inserted into the subject
+ #
+ # Examples
+ #
+ # >> subject('Lorem ipsum')
+ # => "GitLab-CI | Lorem ipsum"
+ #
+ # # Automatically inserts Project name when @project is set
+ # >> @project = Project.last
+ # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
+ # >> subject('Lorem ipsum')
+ # => "GitLab-CI | Ruby on Rails | Lorem ipsum "
+ #
+ # # Accepts multiple arguments
+ # >> subject('Lorem ipsum', 'Dolor sit amet')
+ # => "GitLab-CI | Lorem ipsum | Dolor sit amet"
+ def subject(*extra)
+ subject = "GitLab-CI"
+ subject << (@project ? " | #{@project.name}" : "")
+ subject << " | " + extra.join(' | ') if extra.present?
+ subject
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5717c89e61d..38afb49c78c 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -8,8 +8,8 @@ class Notify < BaseMailer
include Emails::Profile
include Emails::Groups
- add_template_helper MergeRequestsHelper
- add_template_helper EmailsHelper
+ add_template_helper Gitlab::MergeRequestsHelper
+ add_template_helper Gitlab::EmailsHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb
new file mode 100644
index 00000000000..0ea2452e392
--- /dev/null
+++ b/app/models/ci/application_setting.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: application_settings
+#
+# id :integer not null, primary key
+# all_broken_builds :boolean
+# add_pusher :boolean
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class ApplicationSetting < ActiveRecord::Base
+ extend Ci::Model
+
+ def self.current
+ Ci::ApplicationSetting.last
+ end
+
+ def self.create_from_defaults
+ create(
+ all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: Ci::Settings.gitlab_ci['add_pusher'],
+ )
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
new file mode 100644
index 00000000000..64e7a600672
--- /dev/null
+++ b/app/models/ci/build.rb
@@ -0,0 +1,285 @@
+# == Schema Information
+#
+# Table name: builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# commit_id :integer
+# coverage :float
+# commands :text
+# job_id :integer
+# name :string(255)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# deploy :boolean default(FALSE)
+# trigger_request_id :integer
+#
+
+module Ci
+ class Build < ActiveRecord::Base
+ extend Ci::Model
+
+ LAZY_ATTRIBUTES = ['trace']
+
+ belongs_to :commit, class_name: 'Ci::Commit'
+ belongs_to :project, class_name: 'Ci::Project'
+ belongs_to :runner, class_name: 'Ci::Runner'
+ belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+
+ serialize :options
+
+ validates :commit, presence: true
+ validates :status, presence: true
+ validates :coverage, numericality: true, allow_blank: true
+
+ scope :running, ->() { where(status: "running") }
+ scope :pending, ->() { where(status: "pending") }
+ scope :success, ->() { where(status: "success") }
+ scope :failed, ->() { where(status: "failed") }
+ scope :unstarted, ->() { where(runner_id: nil) }
+ scope :running_or_pending, ->() { where(status:[:running, :pending]) }
+
+ acts_as_taggable
+
+ # To prevent db load megabytes of data from trace
+ default_scope -> { select(Ci::Build.columns_without_lazy) }
+
+ class << self
+ def columns_without_lazy
+ (column_names - LAZY_ATTRIBUTES).map do |column_name|
+ "#{table_name}.#{column_name}"
+ end
+ end
+
+ def last_month
+ where('created_at > ?', Date.today - 1.month)
+ end
+
+ def first_pending
+ pending.unstarted.order('created_at ASC').first
+ end
+
+ def create_from(build)
+ new_build = build.dup
+ new_build.status = :pending
+ new_build.runner_id = nil
+ new_build.save
+ end
+
+ def retry(build)
+ new_build = Ci::Build.new(status: :pending)
+ new_build.options = build.options
+ new_build.commands = build.commands
+ new_build.tag_list = build.tag_list
+ new_build.commit_id = build.commit_id
+ new_build.project_id = build.project_id
+ new_build.name = build.name
+ new_build.allow_failure = build.allow_failure
+ new_build.stage = build.stage
+ new_build.trigger_request = build.trigger_request
+ new_build.save
+ new_build
+ end
+ end
+
+ state_machine :status, initial: :pending do
+ event :run do
+ transition pending: :running
+ end
+
+ event :drop do
+ transition running: :failed
+ end
+
+ event :success do
+ transition running: :success
+ end
+
+ event :cancel do
+ transition [:pending, :running] => :canceled
+ end
+
+ after_transition pending: :running do |build, transition|
+ build.update_attributes started_at: Time.now
+ end
+
+ after_transition any => [:success, :failed, :canceled] do |build, transition|
+ build.update_attributes finished_at: Time.now
+ project = build.project
+
+ if project.web_hooks?
+ Ci::WebHookService.new.build_end(build)
+ end
+
+ if build.commit.success?
+ build.commit.create_next_builds(build.trigger_request)
+ end
+
+ project.execute_services(build)
+
+ if project.coverage_enabled?
+ build.update_coverage
+ end
+ end
+
+ state :pending, value: 'pending'
+ state :running, value: 'running'
+ state :failed, value: 'failed'
+ state :success, value: 'success'
+ state :canceled, value: 'canceled'
+ end
+
+ delegate :sha, :short_sha, :before_sha, :ref,
+ to: :commit, prefix: false
+
+ def trace_html
+ html = Ci::Ansi2html::convert(trace) if trace.present?
+ html ||= ''
+ end
+
+ def trace
+ if project && read_attribute(:trace).present?
+ read_attribute(:trace).gsub(project.token, 'xxxxxx')
+ end
+ end
+
+ def started?
+ !pending? && !canceled? && started_at
+ end
+
+ def active?
+ running? || pending?
+ end
+
+ def complete?
+ canceled? || success? || failed?
+ end
+
+ def ignored?
+ failed? && allow_failure?
+ end
+
+ def timeout
+ project.timeout
+ end
+
+ def variables
+ yaml_variables + project_variables + trigger_variables
+ end
+
+ def duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
+
+ def project
+ commit.project
+ end
+
+ def project_id
+ commit.project_id
+ end
+
+ def project_name
+ project.name
+ end
+
+ def repo_url
+ project.repo_url_with_auth
+ end
+
+ def allow_git_fetch
+ project.allow_git_fetch
+ end
+
+ def update_coverage
+ coverage = extract_coverage(trace, project.coverage_regex)
+
+ if coverage.is_a? Numeric
+ update_attributes(coverage: coverage)
+ end
+ end
+
+ def extract_coverage(text, regex)
+ begin
+ matches = text.gsub(Regexp.new(regex)).to_a.last
+ coverage = matches.gsub(/\d+(\.\d+)?/).first
+
+ if coverage.present?
+ coverage.to_f
+ end
+ rescue => ex
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
+ end
+ end
+
+ def trace
+ if File.exist?(path_to_trace)
+ File.read(path_to_trace)
+ else
+ # backward compatibility
+ read_attribute :trace
+ end
+ end
+
+ def trace=(trace)
+ unless Dir.exists? dir_to_trace
+ FileUtils.mkdir_p dir_to_trace
+ end
+
+ File.write(path_to_trace, trace)
+ end
+
+ def dir_to_trace
+ File.join(
+ Ci::Settings.gitlab_ci.builds_path,
+ created_at.utc.strftime("%Y_%m"),
+ project.id.to_s
+ )
+ end
+
+ def path_to_trace
+ "#{dir_to_trace}/#{id}.log"
+ end
+
+ private
+
+ def yaml_variables
+ if commit.config_processor
+ commit.config_processor.variables.map do |key, value|
+ { key: key, value: value, public: true }
+ end
+ else
+ []
+ end
+ end
+
+ def project_variables
+ project.variables.map do |variable|
+ { key: variable.key, value: variable.value, public: false }
+ end
+ end
+
+ def trigger_variables
+ if trigger_request && trigger_request.variables
+ trigger_request.variables.map do |key, value|
+ { key: key, value: value, public: false }
+ end
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
new file mode 100644
index 00000000000..23cd47dfe37
--- /dev/null
+++ b/app/models/ci/commit.rb
@@ -0,0 +1,267 @@
+# == Schema Information
+#
+# Table name: commits
+#
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+#
+
+module Ci
+ class Commit < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+ has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ serialize :push_data
+
+ validates_presence_of :ref, :sha, :before_sha, :push_data
+ validate :valid_commit_sha
+
+ def self.truncate_sha(sha)
+ sha[0...8]
+ end
+
+ def to_param
+ sha
+ end
+
+ def last_build
+ builds.order(:id).last
+ end
+
+ def retry
+ builds_without_retry.each do |build|
+ Ci::Build.retry(build)
+ end
+ end
+
+ def valid_commit_sha
+ if self.sha == Ci::Git::BLANK_SHA
+ self.errors.add(:sha, " cant be 00000000 (branch removal)")
+ end
+ end
+
+ def new_branch?
+ before_sha == Ci::Git::BLANK_SHA
+ end
+
+ def compare?
+ !new_branch?
+ end
+
+ def git_author_name
+ commit_data[:author][:name] if commit_data && commit_data[:author]
+ end
+
+ def git_author_email
+ commit_data[:author][:email] if commit_data && commit_data[:author]
+ end
+
+ def git_commit_message
+ commit_data[:message] if commit_data && commit_data[:message]
+ end
+
+ def short_before_sha
+ Ci::Commit.truncate_sha(before_sha)
+ end
+
+ def short_sha
+ Ci::Commit.truncate_sha(sha)
+ end
+
+ def commit_data
+ push_data[:commits].find do |commit|
+ commit[:id] == sha
+ end
+ rescue
+ nil
+ end
+
+ def project_recipients
+ recipients = project.email_recipients.split(' ')
+
+ if project.email_add_pusher? && push_data[:user_email].present?
+ recipients << push_data[:user_email]
+ end
+
+ recipients.uniq
+ end
+
+ def stage
+ return unless config_processor
+ stages = builds_without_retry.select(&:active?).map(&:stage)
+ config_processor.stages.find { |stage| stages.include? stage }
+ end
+
+ def create_builds_for_stage(stage, trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag)
+ builds_attrs.map do |build_attrs|
+ builds.create!({
+ project: project,
+ name: build_attrs[:name],
+ commands: build_attrs[:script],
+ tag_list: build_attrs[:tags],
+ options: build_attrs[:options],
+ allow_failure: build_attrs[:allow_failure],
+ stage: build_attrs[:stage],
+ trigger_request: trigger_request,
+ })
+ end
+ end
+
+ def create_next_builds(trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ stages = builds.where(trigger_request: trigger_request).group_by(&:stage)
+
+ config_processor.stages.any? do |stage|
+ !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present?
+ end
+ end
+
+ def create_builds(trigger_request = nil)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ config_processor.stages.any? do |stage|
+ create_builds_for_stage(stage, trigger_request).present?
+ end
+ end
+
+ def builds_without_retry
+ @builds_without_retry ||=
+ begin
+ grouped_builds = builds.group_by(&:name)
+ grouped_builds.map do |name, builds|
+ builds.sort_by(&:id).last
+ end
+ end
+ end
+
+ def builds_without_retry_sorted
+ return builds_without_retry unless config_processor
+
+ stages = config_processor.stages
+ builds_without_retry.sort_by do |build|
+ [stages.index(build.stage) || -1, build.name || ""]
+ end
+ end
+
+ def retried_builds
+ @retried_builds ||= (builds.order(id: :desc) - builds_without_retry)
+ end
+
+ def status
+ if skip_ci?
+ return 'skipped'
+ elsif yaml_errors.present?
+ return 'failed'
+ elsif builds.none?
+ return 'skipped'
+ elsif success?
+ 'success'
+ elsif pending?
+ 'pending'
+ elsif running?
+ 'running'
+ elsif canceled?
+ 'canceled'
+ else
+ 'failed'
+ end
+ end
+
+ def pending?
+ builds_without_retry.all? do |build|
+ build.pending?
+ end
+ end
+
+ def running?
+ builds_without_retry.any? do |build|
+ build.running? || build.pending?
+ end
+ end
+
+ def success?
+ builds_without_retry.all? do |build|
+ build.success? || build.ignored?
+ end
+ end
+
+ def failed?
+ status == 'failed'
+ end
+
+ def canceled?
+ builds_without_retry.all? do |build|
+ build.canceled?
+ end
+ end
+
+ def duration
+ @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
+ end
+
+ def finished_at
+ @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at)
+ end
+
+ def coverage
+ if project.coverage_enabled?
+ coverage_array = builds_without_retry.map(&:coverage).compact
+ if coverage_array.size >= 1
+ '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
+ end
+ end
+ end
+
+ def matrix?
+ builds_without_retry.size > 1
+ end
+
+ def config_processor
+ @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file] || project.generated_yaml_config)
+ rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ save_yaml_error(e.message)
+ nil
+ rescue Exception => e
+ logger.error e.message + "\n" + e.backtrace.join("\n")
+ save_yaml_error("Undefined yaml error")
+ nil
+ end
+
+ def skip_ci?
+ return false if builds.any?
+ commits = push_data[:commits]
+ commits.present? && commits.last[:message] =~ /(\[ci skip\])/
+ end
+
+ def update_committed!
+ update!(committed_at: DateTime.now)
+ end
+
+ private
+
+ def save_yaml_error(error)
+ return if self.yaml_errors?
+ self.yaml_errors = error
+ save
+ end
+ end
+end
diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb
new file mode 100644
index 00000000000..cac3a7a49c1
--- /dev/null
+++ b/app/models/ci/event.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: events
+#
+# id :integer not null, primary key
+# project_id :integer
+# user_id :integer
+# is_admin :integer
+# description :text
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class Event < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates :description,
+ presence: true,
+ length: { in: 5..200 }
+
+ scope :admin, ->(){ where(is_admin: true) }
+ scope :project_wide, ->(){ where(is_admin: false) }
+ end
+end
diff --git a/app/models/ci/network.rb b/app/models/ci/network.rb
new file mode 100644
index 00000000000..c307907e6b8
--- /dev/null
+++ b/app/models/ci/network.rb
@@ -0,0 +1,122 @@
+module Ci
+ class Network
+ class UnauthorizedError < StandardError; end
+
+ include HTTParty
+
+ API_PREFIX = '/api/v3/'
+
+ def authenticate(api_opts)
+ opts = {
+ query: api_opts
+ }
+
+ endpoint = File.join(url, API_PREFIX, 'user')
+ response = self.class.get(endpoint, default_opts.merge(opts))
+
+ build_response(response)
+ end
+
+ def projects(api_opts, scope = :owned)
+ # Dont load archived projects
+ api_opts.merge!(archived: false)
+
+ opts = {
+ query: api_opts
+ }
+
+ query = if scope == :owned
+ 'projects/owned.json'
+ else
+ 'projects.json'
+ end
+
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.get(endpoint, default_opts.merge(opts))
+
+ build_response(response)
+ end
+
+ def project(api_opts, project_id)
+ opts = {
+ query: api_opts
+ }
+
+ query = "projects/#{project_id}.json"
+
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.get(endpoint, default_opts.merge(opts))
+
+ build_response(response)
+ end
+
+ def project_hooks(api_opts, project_id)
+ opts = {
+ query: api_opts
+ }
+
+ query = "projects/#{project_id}/hooks.json"
+
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.get(endpoint, default_opts.merge(opts))
+
+ build_response(response)
+ end
+
+ def enable_ci(project_id, data, api_opts)
+ opts = {
+ body: data.to_json,
+ query: api_opts
+ }
+
+ query = "projects/#{project_id}/services/gitlab-ci.json"
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.put(endpoint, default_opts.merge(opts))
+
+ case response.code
+ when 200
+ true
+ when 401
+ raise UnauthorizedError
+ else
+ nil
+ end
+ end
+
+ def disable_ci(project_id, api_opts)
+ opts = {
+ query: api_opts
+ }
+
+ query = "projects/#{project_id}/services/gitlab-ci.json"
+
+ endpoint = File.join(url, API_PREFIX, query)
+ response = self.class.delete(endpoint, default_opts.merge(opts))
+
+ build_response(response)
+ end
+
+ private
+
+ def url
+ GitlabCi.config.gitlab_server.url
+ end
+
+ def default_opts
+ {
+ headers: { "Content-Type" => "application/json" },
+ }
+ end
+
+ def build_response(response)
+ case response.code
+ when 200
+ response.parsed_response
+ when 401
+ raise UnauthorizedError
+ else
+ nil
+ end
+ end
+ end
+end
diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb
new file mode 100644
index 00000000000..dceca7a275a
--- /dev/null
+++ b/app/models/ci/project.rb
@@ -0,0 +1,221 @@
+# == Schema Information
+#
+# Table name: projects
+#
+# id :integer not null, primary key
+# name :string(255) not null
+# timeout :integer default(3600), not null
+# created_at :datetime
+# updated_at :datetime
+# token :string(255)
+# default_ref :string(255)
+# path :string(255)
+# always_build :boolean default(FALSE), not null
+# polling_interval :integer
+# public :boolean default(FALSE), not null
+# ssh_url_to_repo :string(255)
+# gitlab_id :integer
+# allow_git_fetch :boolean default(TRUE), not null
+# email_recipients :string(255) default(""), not null
+# email_add_pusher :boolean default(TRUE), not null
+# email_only_broken_builds :boolean default(TRUE), not null
+# skip_refs :string(255)
+# coverage_regex :string(255)
+# shared_runners_enabled :boolean default(FALSE)
+# generated_yaml_config :text
+#
+
+module Ci
+ class Project < ActiveRecord::Base
+ extend Ci::Model
+
+ include Ci::ProjectStatus
+
+ has_many :commits, ->() { order(:committed_at) }, dependent: :destroy, class_name: 'Ci::Commit'
+ has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
+ has_many :runners, through: :runner_projects, class_name: 'Ci::Runner'
+ has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook'
+ has_many :events, dependent: :destroy, class_name: 'Ci::Event'
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
+
+ # Project services
+ has_many :services, dependent: :destroy, class_name: 'Ci::Service'
+ has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService'
+ has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService'
+ has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService'
+
+ accepts_nested_attributes_for :variables, allow_destroy: true
+
+ #
+ # Validations
+ #
+ validates_presence_of :name, :timeout, :token, :default_ref,
+ :path, :ssh_url_to_repo, :gitlab_id
+
+ validates_uniqueness_of :gitlab_id
+
+ validates :polling_interval,
+ presence: true,
+ if: ->(project) { project.always_build.present? }
+
+ scope :public_only, ->() { where(public: true) }
+
+ before_validation :set_default_values
+
+ class << self
+ include Ci::CurrentSettings
+
+ def base_build_script
+ <<-eos
+ git submodule update --init
+ ls -la
+ eos
+ end
+
+ def parse(project)
+ params = {
+ name: project.name_with_namespace,
+ gitlab_id: project.id,
+ path: project.path_with_namespace,
+ default_ref: project.default_branch || 'master',
+ ssh_url_to_repo: project.ssh_url_to_repo,
+ email_add_pusher: current_application_settings.add_pusher,
+ email_only_broken_builds: current_application_settings.all_broken_builds,
+ }
+
+ project = Ci::Project.new(params)
+ project.build_missing_services
+ project
+ end
+
+ def from_gitlab(user, scope = :owned, options)
+ opts = user.authenticate_options
+ opts.merge! options
+
+ projects = Ci::Network.new.projects(opts.compact, scope)
+
+ if projects
+ projects.map { |pr| OpenStruct.new(pr) }
+ else
+ []
+ end
+ end
+
+ def already_added?(project)
+ where(gitlab_id: project.id).any?
+ end
+
+ def unassigned(runner)
+ joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \
+ "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}").
+ where('#{Ci::RunnerProject.table_name}.project_id' => nil)
+ end
+
+ def ordered_by_last_commit_date
+ last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)"
+ joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id").
+ order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
+ end
+
+ def search(query)
+ where("LOWER(#{Ci::Project.table_name}.name) LIKE :query",
+ query: "%#{query.try(:downcase)}%")
+ end
+ end
+
+ def any_runners?
+ if runners.active.any?
+ return true
+ end
+
+ shared_runners_enabled && Ci::Runner.shared.active.any?
+ end
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def tracked_refs
+ @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip}
+ end
+
+ def valid_token? token
+ self.token && self.token == token
+ end
+
+ def no_running_builds?
+ # Get running builds not later than 3 days ago to ignore hangs
+ builds.running.where("updated_at > ?", 3.days.ago).empty?
+ end
+
+ def email_notification?
+ email_add_pusher || email_recipients.present?
+ end
+
+ def web_hooks?
+ web_hooks.any?
+ end
+
+ def services?
+ services.any?
+ end
+
+ def timeout_in_minutes
+ timeout / 60
+ end
+
+ def timeout_in_minutes=(value)
+ self.timeout = value.to_i * 60
+ end
+
+ def coverage_enabled?
+ coverage_regex.present?
+ end
+
+ # Build a clone-able repo url
+ # using http and basic auth
+ def repo_url_with_auth
+ auth = "gitlab-ci-token:#{token}@"
+ url = gitlab_url + ".git"
+ url.sub(/^https?:\/\//) do |prefix|
+ prefix + auth
+ end
+ end
+
+ def available_services_names
+ %w(slack mail hip_chat)
+ end
+
+ def build_missing_services
+ available_services_names.each do |service_name|
+ service = services.find { |service| service.to_param == service_name }
+
+ # If service is available but missing in db
+ # we should create an instance. Ex `create_gitlab_ci_service`
+ service = self.send :"create_#{service_name}_service" if service.nil?
+ end
+ end
+
+ def execute_services(data)
+ services.each do |service|
+
+ # Call service hook only if it is active
+ begin
+ service.execute(data) if service.active && service.can_execute?(data)
+ rescue => e
+ logger.error(e)
+ end
+ end
+ end
+
+ def gitlab_url
+ File.join(GitlabCi.config.gitlab_server.url, path)
+ end
+
+ def setup_finished?
+ commits.any?
+ end
+ end
+end
diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb
new file mode 100644
index 00000000000..6d5cafe81a2
--- /dev/null
+++ b/app/models/ci/project_status.rb
@@ -0,0 +1,47 @@
+module Ci
+ module ProjectStatus
+ def status
+ last_commit.status if last_commit
+ end
+
+ def broken?
+ last_commit.failed? if last_commit
+ end
+
+ def success?
+ last_commit.success? if last_commit
+ end
+
+ def broken_or_success?
+ broken? || success?
+ end
+
+ def last_commit
+ @last_commit ||= commits.last if commits.any?
+ end
+
+ def last_commit_date
+ last_commit.try(:created_at)
+ end
+
+ def human_status
+ status
+ end
+
+ # only check for toggling build status within same ref.
+ def last_commit_changed_status?
+ ref = last_commit.ref
+ last_commits = commits.where(ref: ref).last(2)
+
+ if last_commits.size < 2
+ false
+ else
+ last_commits[0].status != last_commits[1].status
+ end
+ end
+
+ def last_commit_for_ref(ref)
+ commits.where(ref: ref).last
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
new file mode 100644
index 00000000000..79c81df5eb2
--- /dev/null
+++ b/app/models/ci/runner.rb
@@ -0,0 +1,80 @@
+# == Schema Information
+#
+# Table name: runners
+#
+# id :integer not null, primary key
+# token :string(255)
+# created_at :datetime
+# updated_at :datetime
+# description :string(255)
+# contacted_at :datetime
+# active :boolean default(TRUE), not null
+# is_shared :boolean default(FALSE)
+# name :string(255)
+# version :string(255)
+# revision :string(255)
+# platform :string(255)
+# architecture :string(255)
+#
+
+module Ci
+ class Runner < ActiveRecord::Base
+ extend Ci::Model
+
+ has_many :builds, class_name: 'Ci::Build'
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
+ has_many :projects, through: :runner_projects, class_name: 'Ci::Project'
+
+ has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
+
+ before_validation :set_default_values
+
+ scope :specific, ->() { where(is_shared: false) }
+ scope :shared, ->() { where(is_shared: true) }
+ scope :active, ->() { where(active: true) }
+ scope :paused, ->() { where(active: false) }
+
+ acts_as_taggable
+
+ def self.search(query)
+ where('LOWER(runners.token) LIKE :query OR LOWER(runners.description) like :query',
+ query: "%#{query.try(:downcase)}%")
+ end
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def assign_to(project, current_user = nil)
+ self.is_shared = false if shared?
+ self.save
+ project.runner_projects.create!(runner_id: self.id)
+ end
+
+ def display_name
+ return token unless !description.blank?
+
+ description
+ end
+
+ def shared?
+ is_shared
+ end
+
+ def belongs_to_one_project?
+ runner_projects.count == 1
+ end
+
+ def specific?
+ !shared?
+ end
+
+ def only_for?(project)
+ projects == [project]
+ end
+
+ def short_sha
+ token[0...10]
+ end
+ end
+end
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
new file mode 100644
index 00000000000..44453ee4b41
--- /dev/null
+++ b/app/models/ci/runner_project.rb
@@ -0,0 +1,21 @@
+# == Schema Information
+#
+# Table name: runner_projects
+#
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class RunnerProject < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :runner, class_name: 'Ci::Runner'
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates_uniqueness_of :runner_id, scope: :project_id
+ end
+end
diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb
new file mode 100644
index 00000000000..ed5e3f940b6
--- /dev/null
+++ b/app/models/ci/service.rb
@@ -0,0 +1,105 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+# To add new service you should build a class inherited from Service
+# and implement a set of methods
+module Ci
+ class Service < ActiveRecord::Base
+ extend Ci::Model
+
+ serialize :properties, JSON
+
+ default_value_for :active, false
+
+ after_initialize :initialize_properties
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates :project_id, presence: true
+
+ def activated?
+ active
+ end
+
+ def category
+ :common
+ end
+
+ def initialize_properties
+ self.properties = {} if properties.nil?
+ end
+
+ def title
+ # implement inside child
+ end
+
+ def description
+ # implement inside child
+ end
+
+ def help
+ # implement inside child
+ end
+
+ def to_param
+ # implement inside child
+ end
+
+ def fields
+ # implement inside child
+ []
+ end
+
+ def can_test?
+ project.builds.any?
+ end
+
+ def can_execute?(build)
+ true
+ end
+
+ def execute(build)
+ # implement inside child
+ end
+
+ # Provide convenient accessor methods
+ # for each serialized property.
+ def self.prop_accessor(*args)
+ args.each do |arg|
+ class_eval %{
+ def #{arg}
+ (properties || {})['#{arg}']
+ end
+
+ def #{arg}=(value)
+ self.properties ||= {}
+ self.properties['#{arg}'] = value
+ end
+ }
+ end
+ end
+
+ def self.boolean_accessor(*args)
+ self.prop_accessor(*args)
+
+ args.each do |arg|
+ class_eval %{
+ def #{arg}?
+ ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
new file mode 100644
index 00000000000..84eab91e8ba
--- /dev/null
+++ b/app/models/ci/trigger.rb
@@ -0,0 +1,39 @@
+# == Schema Information
+#
+# Table name: triggers
+#
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer not null
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class Trigger < ActiveRecord::Base
+ extend Ci::Model
+
+ acts_as_paranoid
+
+ belongs_to :project, class_name: 'Ci::Trigger'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ validates_presence_of :token
+ validates_uniqueness_of :token
+
+ before_validation :set_default_values
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def last_trigger_request
+ trigger_requests.last
+ end
+
+ def short_token
+ token[0...10]
+ end
+ end
+end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
new file mode 100644
index 00000000000..29cd9553394
--- /dev/null
+++ b/app/models/ci/trigger_request.rb
@@ -0,0 +1,23 @@
+# == Schema Information
+#
+# Table name: trigger_requests
+#
+# id :integer not null, primary key
+# trigger_id :integer not null
+# variables :text
+# created_at :datetime
+# updated_at :datetime
+# commit_id :integer
+#
+
+module Ci
+ class TriggerRequest < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :trigger, class_name: 'Ci::Trigger'
+ belongs_to :commit, class_name: 'Ci::Commit'
+ has_many :builds, class_name: 'Ci::Build'
+
+ serialize :variables
+ end
+end
diff --git a/app/models/ci/user.rb b/app/models/ci/user.rb
new file mode 100644
index 00000000000..7456bd1a77b
--- /dev/null
+++ b/app/models/ci/user.rb
@@ -0,0 +1,97 @@
+# User object is stored in session
+module Ci
+ class User
+ DEVELOPER_ACCESS = 30
+
+ attr_reader :attributes
+
+ def initialize(hash)
+ @attributes = hash
+ end
+
+ def gitlab_projects(search = nil, page = 1, per_page = 100)
+ Rails.cache.fetch(cache_key(page, per_page, search)) do
+ Ci::Project.from_gitlab(self, :authorized, { page: page, per_page: per_page, search: search, ci_enabled_first: true })
+ end
+ end
+
+ def method_missing(meth, *args, &block)
+ if attributes.has_key?(meth.to_s)
+ attributes[meth.to_s]
+ else
+ super
+ end
+ end
+
+ def avatar_url
+ attributes['avatar_url']
+ end
+
+ def cache_key(*args)
+ "#{self.id}:#{args.join(":")}:#{sync_at.to_s}"
+ end
+
+ def sync_at
+ @sync_at ||= Time.now
+ end
+
+ def reset_cache
+ @sync_at = Time.now
+ end
+
+ def can_access_project?(project_gitlab_id)
+ !!project_info(project_gitlab_id)
+ end
+
+ # Indicate if user has developer access or higher
+ def has_developer_access?(project_gitlab_id)
+ data = project_info(project_gitlab_id)
+
+ return false unless data && data["permissions"]
+
+ permissions = data["permissions"]
+
+ if permissions["project_access"] && permissions["project_access"]["access_level"] >= DEVELOPER_ACCESS
+ return true
+ end
+
+ if permissions["group_access"] && permissions["group_access"]["access_level"] >= DEVELOPER_ACCESS
+ return true
+ end
+ end
+
+ def can_manage_project?(project_gitlab_id)
+ Rails.cache.fetch(cache_key('manage', project_gitlab_id, sync_at)) do
+ !!Ci::Network.new.project_hooks(authenticate_options, project_gitlab_id)
+ end
+ end
+
+ def authorized_runners
+ Ci::Runner.specific.includes(:runner_projects).
+ where(runner_projects: { project_id: authorized_projects } )
+ end
+
+ def authorized_projects
+ Ci::Project.where(gitlab_id: gitlab_projects.map(&:id)).select do |project|
+ # This is slow: it makes request to GitLab for each project to verify manage permission
+ can_manage_project?(project.gitlab_id)
+ end
+ end
+
+ def authenticate_options
+ if attributes['access_token']
+ { access_token: attributes['access_token'] }
+ else
+ { private_token: attributes['private_token'] }
+ end
+ end
+
+ private
+
+ def project_info(project_gitlab_id)
+ Rails.cache.fetch(cache_key("project_info", project_gitlab_id, sync_at)) do
+ Ci::Network.new.project(authenticate_options, project_gitlab_id)
+ end
+ end
+ end
+end
diff --git a/app/models/ci/user_session.rb b/app/models/ci/user_session.rb
new file mode 100644
index 00000000000..27c71e30591
--- /dev/null
+++ b/app/models/ci/user_session.rb
@@ -0,0 +1,23 @@
+module Ci
+ class UserSession
+ include ActiveModel::Conversion
+ include Ci::StaticModel
+ extend ActiveModel::Naming
+
+ def authenticate(auth_opts)
+ network = Ci::Network.new
+ user = network.authenticate(auth_opts)
+
+ if user
+ user["access_token"] = auth_opts[:access_token]
+ return Ci::User.new(user)
+ else
+ nil
+ end
+
+ user
+ rescue
+ nil
+ end
+ end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
new file mode 100644
index 00000000000..7a542802fa6
--- /dev/null
+++ b/app/models/ci/variable.rb
@@ -0,0 +1,25 @@
+# == Schema Information
+#
+# Table name: variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+#
+
+module Ci
+ class Variable < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates_presence_of :key
+ validates_uniqueness_of :key, scope: :project_id
+
+ attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
+ end
+end
diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb
new file mode 100644
index 00000000000..4b8c65a1a65
--- /dev/null
+++ b/app/models/ci/web_hook.rb
@@ -0,0 +1,44 @@
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255) not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class WebHook < ActiveRecord::Base
+ extend Ci::Model
+
+ include HTTParty
+
+ belongs_to :project, class_name: 'Ci::WebHook'
+
+ # HTTParty timeout
+ default_timeout 10
+
+ validates :url, presence: true,
+ format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
+
+ def execute(data)
+ parsed_url = URI.parse(url)
+ if parsed_url.userinfo.blank?
+ Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false)
+ else
+ post_url = url.gsub("#{parsed_url.userinfo}@", "")
+ auth = {
+ username: URI.decode(parsed_url.user),
+ password: URI.decode(parsed_url.password),
+ }
+ Ci::WebHook.post(post_url,
+ body: data.to_json,
+ headers: { "Content-Type" => "application/json" },
+ verify: false,
+ basic_auth: auth)
+ end
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 69f9af91c51..f14cd884c89 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -316,7 +316,7 @@ class Project < ActiveRecord::Base
end
def web_url
- Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self)
+ Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self)
end
def web_url_without_protocol
@@ -433,7 +433,7 @@ class Project < ActiveRecord::Base
if avatar.present?
[gitlab_config.url, avatar.url].join
elsif avatar_in_git
- Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
+ Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
end
end
diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb
new file mode 100644
index 00000000000..3e9f99e7eaf
--- /dev/null
+++ b/app/models/project_services/ci/hip_chat_message.rb
@@ -0,0 +1,78 @@
+module Ci
+ class HipChatMessage
+ attr_reader :build
+
+ def initialize(build)
+ @build = build
+ end
+
+ def to_s
+ lines = Array.new
+ lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_url(project)}\">#{project.name}</a> - ")
+
+ if commit.matrix?
+ lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_ref_commit_url(project, commit.ref, commit.sha)}\">Commit ##{commit.id}</a></br>")
+ else
+ first_build = commit.builds_without_retry.first
+ lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_build_url(project, first_build)}\">Build '#{first_build.name}' ##{first_build.id}</a></br>")
+ end
+
+ lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>")
+ lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).")
+ lines.join('')
+ end
+
+ def status_color(build_or_commit=nil)
+ build_or_commit ||= commit_status
+ case build_or_commit
+ when :success
+ 'green'
+ when :failed, :canceled
+ 'red'
+ else # :pending, :running or unknown
+ 'yellow'
+ end
+ end
+
+ def notify?
+ [:failed, :canceled].include?(commit_status)
+ end
+
+
+ private
+
+ def commit
+ build.commit
+ end
+
+ def project
+ commit.project
+ end
+
+ def build_status
+ build.status.to_sym
+ end
+
+ def commit_status
+ commit.status.to_sym
+ end
+
+ def humanized_status(build_or_commit=nil)
+ build_or_commit ||= commit_status
+ case build_or_commit
+ when :pending
+ "Pending"
+ when :running
+ "Running"
+ when :failed
+ "Failed"
+ when :success
+ "Successful"
+ when :canceled
+ "Canceled"
+ else
+ "Unknown"
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb
new file mode 100644
index 00000000000..68acf71251e
--- /dev/null
+++ b/app/models/project_services/ci/hip_chat_service.rb
@@ -0,0 +1,93 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+module Ci
+ class HipChatService < Service
+ prop_accessor :hipchat_token, :hipchat_room, :hipchat_server
+ boolean_accessor :notify_only_broken_builds
+ validates :hipchat_token, presence: true, if: :activated?
+ validates :hipchat_room, presence: true, if: :activated?
+ default_value_for :notify_only_broken_builds, true
+
+ def title
+ "HipChat"
+ end
+
+ def description
+ "Private group chat, video chat, instant messaging for teams"
+ end
+
+ def help
+ end
+
+ def to_param
+ 'hip_chat'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' },
+ { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' },
+ { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' },
+ { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
+ ]
+ end
+
+ def can_execute?(build)
+ return if build.allow_failure?
+
+ commit = build.commit
+ return unless commit
+ return unless commit.builds_without_retry.include? build
+
+ case commit.status.to_sym
+ when :failed
+ true
+ when :success
+ true unless notify_only_broken_builds?
+ else
+ false
+ end
+ end
+
+ def execute(build)
+ msg = Ci::HipChatMessage.new(build)
+ opts = default_options.merge(
+ token: hipchat_token,
+ room: hipchat_room,
+ server: server_url,
+ color: msg.status_color,
+ notify: msg.notify?
+ )
+ Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts)
+ end
+
+ private
+
+ def default_options
+ {
+ service_name: 'GitLab CI',
+ message_format: 'html'
+ }
+ end
+
+ def server_url
+ if hipchat_server.blank?
+ 'https://api.hipchat.com'
+ else
+ hipchat_server
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb
new file mode 100644
index 00000000000..3619a50fa96
--- /dev/null
+++ b/app/models/project_services/ci/mail_service.rb
@@ -0,0 +1,84 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+module Ci
+ class MailService < Service
+ delegate :email_recipients, :email_recipients=,
+ :email_add_pusher, :email_add_pusher=,
+ :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false
+
+ before_save :update_project
+
+ default_value_for :active, true
+
+ def title
+ 'Mail'
+ end
+
+ def description
+ 'Email notification'
+ end
+
+ def to_param
+ 'mail'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' },
+ { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' },
+ { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' }
+ ]
+ end
+
+ def can_execute?(build)
+ return if build.allow_failure?
+
+ # it doesn't make sense to send emails for retried builds
+ commit = build.commit
+ return unless commit
+ return unless commit.builds_without_retry.include?(build)
+
+ case build.status.to_sym
+ when :failed
+ true
+ when :success
+ true unless email_only_broken_builds
+ else
+ false
+ end
+ end
+
+ def execute(build)
+ build.commit.project_recipients.each do |recipient|
+ case build.status.to_sym
+ when :success
+ mailer.build_success_email(build.id, recipient)
+ when :failed
+ mailer.build_fail_email(build.id, recipient)
+ end
+ end
+ end
+
+ private
+
+ def update_project
+ project.save!
+ end
+
+ def mailer
+ Ci::Notify.delay
+ end
+ end
+end
diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb
new file mode 100644
index 00000000000..7d884849bf3
--- /dev/null
+++ b/app/models/project_services/ci/slack_message.rb
@@ -0,0 +1,97 @@
+require 'slack-notifier'
+
+module Ci
+ class SlackMessage
+ def initialize(commit)
+ @commit = commit
+ end
+
+ def pretext
+ ''
+ end
+
+ def color
+ attachment_color
+ end
+
+ def fallback
+ format(attachment_message)
+ end
+
+ def attachments
+ fields = []
+
+ if commit.matrix?
+ commit.builds_without_retry.each do |build|
+ next if build.allow_failure?
+ next unless build.failed?
+ fields << {
+ title: build.name,
+ value: "Build <#{Ci::RoutesHelper.ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
+ }
+ end
+ end
+
+ [{
+ text: attachment_message,
+ color: attachment_color,
+ fields: fields
+ }]
+ end
+
+ private
+
+ attr_reader :commit
+
+ def attachment_message
+ out = "<#{Ci::RoutesHelper.ci_project_url(project)}|#{project_name}>: "
+ if commit.matrix?
+ out << "Commit <#{Ci::RoutesHelper.ci_project_ref_commit_url(project, commit.ref, commit.sha)}|\##{commit.id}> "
+ else
+ build = commit.builds_without_retry.first
+ out << "Build <#{Ci::RoutesHelper.ci_project_build_url(project, build)}|\##{build.id}> "
+ end
+ out << "(<#{commit_sha_link}|#{commit.short_sha}>) "
+ out << "of <#{commit_ref_link}|#{commit.ref}> "
+ out << "by #{commit.git_author_name} " if commit.git_author_name
+ out << "#{commit_status} in "
+ out << "#{commit.duration} second(s)"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def project
+ commit.project
+ end
+
+ def project_name
+ project.name
+ end
+
+ def commit_sha_link
+ "#{project.gitlab_url}/commit/#{commit.sha}"
+ end
+
+ def commit_ref_link
+ "#{project.gitlab_url}/commits/#{commit.ref}"
+ end
+
+ def attachment_color
+ if commit.success?
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def commit_status
+ if commit.success?
+ 'succeeded'
+ else
+ 'failed'
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb
new file mode 100644
index 00000000000..c9a7f865a25
--- /dev/null
+++ b/app/models/project_services/ci/slack_service.rb
@@ -0,0 +1,81 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+module Ci
+ class SlackService < Service
+ prop_accessor :webhook
+ boolean_accessor :notify_only_broken_builds
+ validates :webhook, presence: true, if: :activated?
+
+ default_value_for :notify_only_broken_builds, true
+
+ def title
+ 'Slack'
+ end
+
+ def description
+ 'A team communication tool for the 21st century'
+ end
+
+ def to_param
+ 'slack'
+ end
+
+ def help
+ 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present?
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' },
+ { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
+ ]
+ end
+
+ def can_execute?(build)
+ return if build.allow_failure?
+
+ commit = build.commit
+ return unless commit
+ return unless commit.builds_without_retry.include?(build)
+
+ case commit.status.to_sym
+ when :failed
+ true
+ when :success
+ true unless notify_only_broken_builds?
+ else
+ false
+ end
+ end
+
+ def execute(build)
+ message = Ci::SlackMessage.new(build.commit)
+ options = default_options.merge(
+ color: message.color,
+ fallback: message.fallback,
+ attachments: message.attachments
+ )
+ Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options)
+ end
+
+ private
+
+ def default_options
+ {
+ username: 'GitLab CI'
+ }
+ end
+ end
+end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 0ebc0a3ba1a..9558292fea3 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -19,7 +19,7 @@
#
class GitlabIssueTrackerService < IssueTrackerService
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index bfa8fc7b860..35e30b1cb0b 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -19,7 +19,7 @@
#
class JiraService < IssueTrackerService
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb
new file mode 100644
index 00000000000..0a1abf89a95
--- /dev/null
+++ b/app/services/ci/create_commit_service.rb
@@ -0,0 +1,50 @@
+module Ci
+ class CreateCommitService
+ def execute(project, params)
+ before_sha = params[:before]
+ sha = params[:checkout_sha] || params[:after]
+ origin_ref = params[:ref]
+
+ unless origin_ref && sha.present?
+ return false
+ end
+
+ ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '')
+
+ # Skip branch removal
+ if sha == Ci::Git::BLANK_SHA
+ return false
+ end
+
+ commit = project.commits.find_by_sha_and_ref(sha, ref)
+
+ # Create commit if not exists yet
+ unless commit
+ data = {
+ ref: ref,
+ sha: sha,
+ tag: origin_ref.start_with?('refs/tags/'),
+ before_sha: before_sha,
+ push_data: {
+ before: before_sha,
+ after: sha,
+ ref: ref,
+ user_name: params[:user_name],
+ user_email: params[:user_email],
+ repository: params[:repository],
+ commits: params[:commits],
+ total_commits_count: params[:total_commits_count],
+ ci_yaml_file: params[:ci_yaml_file]
+ }
+ }
+
+ commit = project.commits.create(data)
+ end
+
+ commit.update_committed!
+ commit.create_builds unless commit.builds.any?
+
+ commit
+ end
+ end
+end
diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb
new file mode 100644
index 00000000000..049ac2e9181
--- /dev/null
+++ b/app/services/ci/create_project_service.rb
@@ -0,0 +1,35 @@
+module Ci
+ class CreateProjectService
+ include Gitlab::Application.routes.url_helpers
+
+ def execute(current_user, params, project_route, forked_project = nil)
+ @project = Ci::Project.parse(params)
+
+ Ci::Project.transaction do
+ @project.save!
+
+ data = {
+ token: @project.token,
+ project_url: project_route.gsub(":project_id", @project.id.to_s),
+ }
+
+ unless Ci::Network.new.enable_ci(@project.gitlab_id, data, current_user.authenticate_options)
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ if forked_project
+ # Copy settings
+ settings = forked_project.attributes.select do |attr_name, value|
+ ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name
+ end
+
+ @project.update(settings)
+ end
+
+ Ci::EventService.new.create_project(current_user, @project)
+
+ @project
+ end
+ end
+end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
new file mode 100644
index 00000000000..9bad09f2f54
--- /dev/null
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -0,0 +1,17 @@
+module Ci
+ class CreateTriggerRequestService
+ def execute(project, trigger, ref, variables = nil)
+ commit = project.commits.where(ref: ref).last
+ return unless commit
+
+ trigger_request = trigger.trigger_requests.create!(
+ commit: commit,
+ variables: variables
+ )
+
+ if commit.create_builds(trigger_request)
+ trigger_request
+ end
+ end
+ end
+end
diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb
new file mode 100644
index 00000000000..3f4e02dd26c
--- /dev/null
+++ b/app/services/ci/event_service.rb
@@ -0,0 +1,31 @@
+module Ci
+ class EventService
+ def remove_project(user, project)
+ create(
+ description: "Project \"#{project.name}\" has been removed by #{user.username}",
+ user_id: user.id,
+ is_admin: true
+ )
+ end
+
+ def create_project(user, project)
+ create(
+ description: "Project \"#{project.name}\" has been created by #{user.username}",
+ user_id: user.id,
+ is_admin: true
+ )
+ end
+
+ def change_project_settings(user, project)
+ create(
+ project_id: project.id,
+ user_id: user.id,
+ description: "User \"#{user.username}\" updated projects settings"
+ )
+ end
+
+ def create(*args)
+ Ci::Event.create!(*args)
+ end
+ end
+end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
new file mode 100644
index 00000000000..b95835ba093
--- /dev/null
+++ b/app/services/ci/image_for_build_service.rb
@@ -0,0 +1,31 @@
+module Ci
+ class ImageForBuildService
+ def execute(project, params)
+ image_name =
+ if params[:sha]
+ commit = project.commits.find_by(sha: params[:sha])
+ image_for_commit(commit)
+ elsif params[:ref]
+ commit = project.last_commit_for_ref(params[:ref])
+ image_for_commit(commit)
+ else
+ 'build-unknown.svg'
+ end
+
+ image_path = Rails.root.join('public/ci', image_name)
+
+ OpenStruct.new(
+ path: image_path,
+ name: image_name
+ )
+ end
+
+ private
+
+ def image_for_commit(commit)
+ return 'build-unknown.svg' unless commit
+
+ 'build-' + commit.status + ".svg"
+ end
+ end
+end
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
new file mode 100644
index 00000000000..7e0b58a5dc9
--- /dev/null
+++ b/app/services/ci/register_build_service.rb
@@ -0,0 +1,40 @@
+module Ci
+ # This class responsible for assigning
+ # proper pending build to runner on runner API request
+ class RegisterBuildService
+ def execute(current_runner)
+ builds = Ci::Build.pending.unstarted
+
+ builds =
+ if current_runner.shared?
+ # don't run projects which have not enables shared runners
+ builds.includes(:project).where(projects: { shared_runners_enabled: true })
+ else
+ # do run projects which are only assigned to this runner
+ builds.where(project_id: current_runner.projects)
+ end
+
+ builds = builds.order('created_at ASC')
+
+ build = builds.find do |build|
+ (build.tag_list - current_runner.tag_list).empty?
+ end
+
+
+ if build
+ # In case when 2 runners try to assign the same build, second runner will be declined
+ # with StateMachine::InvalidTransition in run! method.
+ build.with_lock do
+ build.runner_id = current_runner.id
+ build.save!
+ build.run!
+ end
+ end
+
+ build
+
+ rescue StateMachine::InvalidTransition
+ nil
+ end
+ end
+end
diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb
new file mode 100644
index 00000000000..3a17596aaeb
--- /dev/null
+++ b/app/services/ci/test_hook_service.rb
@@ -0,0 +1,7 @@
+module Ci
+ class TestHookService
+ def execute(hook, current_user)
+ Ci::WebHookService.new.build_end(hook.project.commits.last.last_build)
+ end
+ end
+end
diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb
new file mode 100644
index 00000000000..87984b20fa1
--- /dev/null
+++ b/app/services/ci/web_hook_service.rb
@@ -0,0 +1,36 @@
+module Ci
+ class WebHookService
+ def build_end(build)
+ execute_hooks(build.project, build_data(build))
+ end
+
+ def execute_hooks(project, data)
+ project.web_hooks.each do |web_hook|
+ async_execute_hook(web_hook, data)
+ end
+ end
+
+ def async_execute_hook(hook, data)
+ Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data)
+ end
+
+ def build_data(build)
+ project = build.project
+ data = {}
+ data.merge!({
+ build_id: build.id,
+ build_name: build.name,
+ build_status: build.status,
+ build_started_at: build.started_at,
+ build_finished_at: build.finished_at,
+ project_id: project.id,
+ project_name: project.name,
+ gitlab_url: project.gitlab_url,
+ ref: build.ref,
+ sha: build.sha,
+ before_sha: build.before_sha,
+ push_data: build.commit.push_data
+ })
+ end
+ end
+end
diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml
new file mode 100644
index 00000000000..634c9daa477
--- /dev/null
+++ b/app/views/ci/admin/application_settings/_form.html.haml
@@ -0,0 +1,24 @@
+= form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ - if @application_setting.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @application_setting.errors.full_messages.each do |msg|
+ %p= msg
+
+ %fieldset
+ %legend Default Project Settings
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :all_broken_builds do
+ = f.check_box :all_broken_builds
+ Send emails only on broken builds
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :add_pusher do
+ = f.check_box :add_pusher
+ Add pusher to recipients list
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml
new file mode 100644
index 00000000000..7ef0aa89ed6
--- /dev/null
+++ b/app/views/ci/admin/application_settings/show.html.haml
@@ -0,0 +1,3 @@
+%h3.page-title Settings
+%hr
+= render 'form'
diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml
new file mode 100644
index 00000000000..1766ca39760
--- /dev/null
+++ b/app/views/ci/admin/builds/_build.html.haml
@@ -0,0 +1,32 @@
+- if build.commit && build.project
+ %tr.build.alert{class: build_status_alert_class(build)}
+ %td.build-link
+ = link_to ci_build_url(build) do
+ %strong #{build.id}
+
+ %td.status
+ = build.status
+
+ %td.commit-link
+ = commit_link(build.commit)
+
+ %td.runner
+ - if build.runner
+ = link_to build.runner.id, ci_admin_runner_path(build.runner)
+
+ %td.build-project
+ = truncate build.project.name, length: 30
+
+ %td.build-message
+ %span= truncate(build.commit.git_commit_message, length: 30)
+
+ %td.build-branch
+ %span= truncate(build.ref, length: 25)
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_in_words build.finished_at} ago
diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml
new file mode 100644
index 00000000000..ab4ced54327
--- /dev/null
+++ b/app/views/ci/admin/builds/index.html.haml
@@ -0,0 +1,27 @@
+%ul.nav.nav-tabs.append-bottom-20
+ %li{class: ("active" if @scope.nil?)}
+ = link_to 'All builds', ci_admin_builds_path
+
+ %li{class: ("active" if @scope == "pending")}
+ = link_to "Pending", ci_admin_builds_path(scope: :pending)
+
+ %li{class: ("active" if @scope == "running")}
+ = link_to "Running", ci_admin_builds_path(scope: :running)
+
+
+%table.builds
+ %thead
+ %tr
+ %th Build
+ %th Status
+ %th Commit
+ %th Runner
+ %th Project
+ %th Message
+ %th Branch
+ %th Duration
+ %th Finished at
+
+ = render @builds
+
+= paginate @builds
diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml
new file mode 100644
index 00000000000..f9ab0994304
--- /dev/null
+++ b/app/views/ci/admin/events/index.html.haml
@@ -0,0 +1,17 @@
+%table.table
+ %thead
+ %tr
+ %th User ID
+ %th Description
+ %th When
+ - @events.each do |event|
+ %tr
+ %td
+ = event.user_id
+ %td
+ = event.description
+ %td.light
+ = time_ago_in_words event.updated_at
+ ago
+
+= paginate @events \ No newline at end of file
diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml
new file mode 100644
index 00000000000..e64bfe853d7
--- /dev/null
+++ b/app/views/ci/admin/projects/_project.html.haml
@@ -0,0 +1,28 @@
+- last_commit = project.last_commit
+%tr.alert{class: commit_status_alert_class(last_commit) }
+ %td
+ = project.id
+ %td
+ = link_to project do
+ %strong= project.name
+ %td
+ - if last_commit
+ #{last_commit.status} (#{commit_link(last_commit)})
+ - if project.last_commit_date
+ = time_ago_in_words project.last_commit_date
+ ago
+ - else
+ No builds yet
+ %td
+ - if project.public
+ %i.fa-globe
+ Public
+ - else
+ %i.fa-lock
+ Private
+ %td
+ = project.commits.count
+ %td
+ = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do
+ %i.fa-remove
+ Remove
diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml
new file mode 100644
index 00000000000..73956575a89
--- /dev/null
+++ b/app/views/ci/admin/projects/index.html.haml
@@ -0,0 +1,14 @@
+%table.table
+ %thead
+ %tr
+ %th ID
+ %th Name
+ %th Last build
+ %th Access
+ %th Builds
+ %th
+
+ = render @projects
+
+= paginate @projects
+
diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml
new file mode 100644
index 00000000000..f049b4f4c4e
--- /dev/null
+++ b/app/views/ci/admin/runner_projects/index.html.haml
@@ -0,0 +1,57 @@
+%p.lead
+ To register new runner visit #{link_to 'this page ', ci_runners_path}
+
+.row
+ .col-md-8
+ %h5 Activated:
+ %table.table
+ %tr
+ %th Runner ID
+ %th Runner Description
+ %th Last build
+ %th Builds Stats
+ %th Registered
+ %th
+
+ - @runner_projects.each do |runner_project|
+ - runner = runner_project.runner
+ - builds = runner.builds.where(project_id: @project.id)
+ %tr
+ %td
+ %span.badge.badge-info= runner.id
+ %td
+ = runner.display_name
+ %td
+ - last_build = builds.last
+ - if last_build
+ = link_to last_build.short_sha, [last_build.project, last_build]
+ - else
+ unknown
+ %td
+ %span.badge.badge-success
+ #{builds.success.count}
+ %span /
+ %span.badge.badge-important
+ #{builds.failed.count}
+ %td
+ #{time_ago_in_words(runner_project.created_at)} ago
+ %td
+ = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right'
+ .col-md-4
+ %h5 Available
+ %table.table
+ %tr
+ %th ID
+ %th Token
+ %th
+
+ - (Ci::Runner.all - @project.runners).each do |runner|
+ %tr
+ %td
+ = runner.id
+ %td
+ = runner.token
+ %td
+ = form_for [:ci, @project, @runner_project] do |f|
+ = f.hidden_field :runner_id, value: runner.id
+ = f.submit 'Add', class: 'btn btn-sm'
diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/ci/admin/runners/_runner.html.haml
new file mode 100644
index 00000000000..701782d26bb
--- /dev/null
+++ b/app/views/ci/admin/runners/_runner.html.haml
@@ -0,0 +1,48 @@
+%tr{id: dom_id(runner)}
+ %td
+ - if runner.shared?
+ %span.label.label-success shared
+ - else
+ %span.label.label-info specific
+ - unless runner.active?
+ %span.label.label-danger paused
+
+ %td
+ = link_to ci_admin_runner_path(runner) do
+ = runner.short_sha
+ %td
+ .runner-description
+ = runner.description
+ %span (#{link_to 'edit', '#', class: 'edit-runner-link'})
+ .runner-description-form.hide
+ = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f|
+ .form-group
+ = f.text_field :description, class: 'form-control'
+ = f.submit 'Save', class: 'btn'
+ %span (#{link_to 'cancel', '#', class: 'cancel'})
+ %td
+ - if runner.shared?
+ \-
+ - else
+ = runner.projects.count(:all)
+ %td
+ #{runner.builds.count(:all)}
+ %td
+ - runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ %td
+ - if runner.contacted_at
+ #{time_ago_in_words(runner.contacted_at)} ago
+ - else
+ Never
+ %td
+ .pull-right
+ = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm'
+ &nbsp;
+ - if runner.active?
+ = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm'
+ - else
+ = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm'
+ = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+
diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml
new file mode 100644
index 00000000000..f1ab3399dcc
--- /dev/null
+++ b/app/views/ci/admin/runners/index.html.haml
@@ -0,0 +1,51 @@
+%p.lead
+ %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication.
+ %code #{GitlabCi::REGISTRATION_TOKEN}
+
+.bs-callout
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+ %br
+
+ %div
+ %span Each runner can be in one of the following states:
+ %ul
+ %li
+ %span.label.label-success shared
+ \- run builds from all unassigned projects
+ %li
+ %span.label.label-info specific
+ \- run builds from assigned projects
+ %li
+ %span.label.label-danger paused
+ \- runner will not receive any new build
+
+.append-bottom-20.clearfix
+ .pull-left
+ = form_tag ci_admin_runners_path, class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token'
+ = submit_tag 'Search', class: 'btn'
+
+ .pull-right.light
+ Runners with last contact less than a minute ago: #{@active_runners_cnt}
+
+%br
+
+%table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
+
+ = render @runners
+= paginate @runners
diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml
new file mode 100644
index 00000000000..0270da53349
--- /dev/null
+++ b/app/views/ci/admin/runners/show.html.haml
@@ -0,0 +1,118 @@
+= content_for :title do
+ %h3.project-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+
+
+- if @runner.shared?
+ .bs-callout.bs-callout-success
+ %h4 This runner will process build from ALL UNASSIGNED projects
+ %p
+ If you want runners to build only specific projects, enable them in the table below.
+ Keep in mind that this is a one way transition.
+- else
+ .bs-callout.bs-callout-info
+ %h4 This runner will process build only from ASSIGNED projects
+ %p You can't make this a shared runner.
+%hr
+= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag :token, class: 'control-label' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group
+ = label_tag :description, class: 'control-label' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group
+ = label_tag :tag_list, class: 'control-label' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, class: 'form-control'
+ .help-block You can setup builds to only use runners with specific tags
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+
+.row
+ .col-md-6
+ %h4 Restrict projects for this runner
+ - if @runner.projects.any?
+ %table.table
+ %thead
+ %tr
+ %th Assigned projects
+ %th
+ - @runner.runner_projects.each do |runner_project|
+ - project = runner_project.project
+ %tr.alert-info
+ %td
+ %strong
+ = project.name
+ %td
+ .pull-right
+ = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+
+ %table.table
+ %thead
+ %tr
+ %th Project
+ %th
+ .pull-right
+ = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner),
+ class: 'btn btn-sm assign-all-runner',
+ title: 'Assign runner to all projects',
+ method: :put
+
+ %tr
+ %td
+ = form_tag ci_admin_runner_path(@runner), class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control'
+ = submit_tag 'Search', class: 'btn'
+
+ %td
+ - @projects.each do |project|
+ %tr
+ %td
+ = project.name
+ %td
+ .pull-right
+ = form_for [:ci, :admin, project, project.runner_projects.new] do |f|
+ = f.hidden_field :runner_id, value: @runner.id
+ = f.submit 'Enable', class: 'btn btn-xs'
+ = paginate @projects
+
+ .col-md-6
+ %h4 Recent builds served by this runner
+ %table.builds.runner-builds
+ %thead
+ %tr
+ %th Status
+ %th Project
+ %th Commit
+ %th Finished at
+
+ - @builds.each do |build|
+ %tr.build.alert{class: build_status_alert_class(build)}
+ %td.status
+ = build.status
+
+ %td.status
+ = build.project.name
+
+ %td.build-link
+ = link_to ci_project_build_path(build.project, build) do
+ %strong #{build.short_sha}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_in_words build.finished_at} ago
diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/ci/admin/runners/update.js.haml
new file mode 100644
index 00000000000..2b7d3067e20
--- /dev/null
+++ b/app/views/ci/admin/runners/update.js.haml
@@ -0,0 +1,2 @@
+:plain
+ $("#runner_#{@runner.id}").replaceWith("#{escape_javascript(render(@runner))}")
diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..ff9fdbbcb4e
--- /dev/null
+++ b/app/views/ci/builds/_build.html.haml
@@ -0,0 +1,45 @@
+%tr.build.alert{class: build_status_alert_class(build)}
+ %td.status
+ = build.status
+
+ %td.build-link
+ = link_to ci_project_build_path(build.project, build) do
+ %strong Build ##{build.id}
+
+ %td
+ = build.stage
+
+ %td
+ = build.name
+ .pull-right
+ - if build.tags.any?
+ - build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.trigger_request
+ %span.label.label-info triggered
+ - if build.allow_failure
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_in_words build.finished_at} ago
+
+ - if build.project.coverage_enabled?
+ %td.coverage
+ - if build.coverage
+ #{build.coverage}%
+
+ %td
+ - if defined?(controls) && current_user && current_user.has_developer_access?(@project.gitlab_id)
+ .pull-right
+ - if build.active?
+ = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do
+ %i.fa-remove.cred
+ - elsif build.commands.present?
+ = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do
+ %i.fa-repeat
diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml
new file mode 100644
index 00000000000..fed30847e73
--- /dev/null
+++ b/app/views/ci/builds/show.html.haml
@@ -0,0 +1,176 @@
+%h4.page-title
+ = link_to @project.name, @project
+ @
+ = @commit.short_sha
+
+%p
+ = link_to ci_project_ref_commit_path(@project, @commit.ref, @commit.sha) do
+ &larr; Back to project commit
+%hr
+#up-build-trace
+- if @commit.matrix?
+ %ul.nav.nav-tabs.append-bottom-10
+ - @commit.builds_without_retry_sorted.each do |build|
+ %li{class: ('active' if build == @build) }
+ = link_to ci_build_url(build) do
+ %i{class: build_icon_css_class(build)}
+ %span
+ Build ##{build.id}
+ - if build.name
+ &middot;
+ = build.name
+
+ - unless @commit.builds_without_retry.include?(@build)
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa-warning-sign
+ This build was retried.
+
+.row
+ .col-md-9
+ .build-head.alert{class: build_status_alert_class(@build)}
+ %h4
+ - if @build.commit.tag?
+ Build for tag
+ %code #{@build.ref}
+ - else
+ Build for commit
+ %code #{@build.short_sha}
+ from
+
+ = link_to ci_project_path(@build.project, ref: @build.ref) do
+ %span.label.label-primary= "#{@build.ref}"
+
+ - if @build.duration
+ .pull-right
+ %span
+ %i.fa-time
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+
+ .clearfix
+ = @build.status
+ .pull-right
+ = @build.updated_at.stamp('19:00 Aug 27')
+
+
+
+ .clearfix
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ .clearfix
+ .scroll-controls
+ = link_to '#up-build-trace', class: 'btn' do
+ %i.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa-angle-down
+
+ %pre.trace#build-trace
+ %code.bash
+ = preserve do
+ = raw @build.trace_html
+ %div#down-build-trace
+
+ .col-md-3
+ - if @build.coverage
+ .build-widget
+ %h4.title
+ Test coverage
+ %h1 #{@build.coverage}%
+
+
+ .build-widget
+ %h4.title
+ Build
+ - if current_user && current_user.has_developer_access?(@project.gitlab_id)
+ .pull-right
+ - if @build.active?
+ = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger'
+ - elsif @build.commands.present?
+ = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post
+
+ - if @build.duration
+ %p
+ %span.attr-name Duration:
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+ %p
+ %span.attr-name Created:
+ #{time_ago_in_words(@build.created_at)} ago
+ - if @build.finished_at
+ %p
+ %span.attr-name Finished:
+ #{time_ago_in_words(@build.finished_at)} ago
+ %p
+ %span.attr-name Runner:
+ - if @build.runner && current_user && current_user.is_admin
+ \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)}
+ - elsif @build.runner
+ \##{@build.runner.id}
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
+
+ %p
+ %span.attr-name Token:
+ #{@build.trigger_request.trigger.short_token}
+
+ - if @build.trigger_request.variables
+ %p
+ %span.attr-name Variables:
+
+ %code
+ - @build.trigger_request.variables.each do |key, value|
+ #{key}=#{value}
+
+ .build-widget
+ %h4.title
+ Commit
+ .pull-right
+ %small #{build_commit_link @build}
+
+ - if @build.commit.compare?
+ %p
+ %span.attr-name Compare:
+ #{build_compare_link @build}
+ %p
+ %span.attr-name Branch:
+ #{build_ref_link @build}
+ %p
+ %span.attr-name Author:
+ #{@build.commit.git_author_name}
+ %p
+ %span.attr-name Message:
+ #{@build.commit.git_commit_message}
+
+ - if @build.tags.any?
+ .build-widget
+ %h4.title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ - if @builds.present?
+ .build-widget
+ %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}:
+ %table.builds
+ - @builds.each_with_index do |build, i|
+ %tr.build.alert{class: build_status_alert_class(build)}
+ %td
+ = link_to ci_build_url(build) do
+ %span ##{build.id}
+ %td
+ - if build.name
+ = build.name
+ %td.status= build.status
+
+
+ = paginate @builds
+
+
+:javascript
+ new CiBuild("#{ci_build_url(@build)}", "#{@build.status}")
diff --git a/app/views/ci/charts/_build_times.haml b/app/views/ci/charts/_build_times.haml
new file mode 100644
index 00000000000..c3c2f572414
--- /dev/null
+++ b/app/views/ci/charts/_build_times.haml
@@ -0,0 +1,21 @@
+%fieldset
+ %legend
+ Commit duration in minutes for last 30 commits
+
+ %canvas#build_timesChart.padded{width: 800, height: 300}
+
+:javascript
+ var data = {
+ labels : #{@charts[:build_times].labels.to_json},
+ datasets : [
+ {
+ fillColor : "#4A3",
+ strokeColor : "rgba(151,187,205,1)",
+ pointColor : "rgba(151,187,205,1)",
+ pointStrokeColor : "#fff",
+ data : #{@charts[:build_times].build_times.to_json}
+ }
+ ]
+ }
+ var ctx = $("#build_timesChart").get(0).getContext("2d");
+ new Chart(ctx).Line(data,{"scaleOverlay": true});
diff --git a/app/views/ci/charts/_builds.haml b/app/views/ci/charts/_builds.haml
new file mode 100644
index 00000000000..1b0039fb834
--- /dev/null
+++ b/app/views/ci/charts/_builds.haml
@@ -0,0 +1,41 @@
+%fieldset
+ %legend
+ Builds chart for last week
+ (#{date_from_to(Date.today - 7.days, Date.today)})
+
+ %canvas#weekChart.padded{width: 800, height: 200}
+
+%fieldset
+ %legend
+ Builds chart for last month
+ (#{date_from_to(Date.today - 30.days, Date.today)})
+
+ %canvas#monthChart.padded{width: 800, height: 300}
+
+%fieldset
+ %legend Builds chart for last year
+ %canvas#yearChart.padded{width: 800, height: 400}
+
+- [:week, :month, :year].each do |scope|
+ :javascript
+ var data = {
+ labels : #{@charts[scope].labels.to_json},
+ datasets : [
+ {
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ pointColor : "rgba(220,220,220,1)",
+ pointStrokeColor : "#EEE",
+ data : #{@charts[scope].total.to_json}
+ },
+ {
+ fillColor : "#4A3",
+ strokeColor : "rgba(151,187,205,1)",
+ pointColor : "rgba(151,187,205,1)",
+ pointStrokeColor : "#fff",
+ data : #{@charts[scope].success.to_json}
+ }
+ ]
+ }
+ var ctx = $("##{scope}Chart").get(0).getContext("2d");
+ new Chart(ctx).Line(data,{"scaleOverlay": true});
diff --git a/app/views/ci/charts/_overall.haml b/app/views/ci/charts/_overall.haml
new file mode 100644
index 00000000000..f522f35a629
--- /dev/null
+++ b/app/views/ci/charts/_overall.haml
@@ -0,0 +1,21 @@
+%fieldset
+ %legend Overall
+ %p
+ Total:
+ %strong= pluralize @project.builds.count(:all), 'build'
+ %p
+ Successful:
+ %strong= pluralize @project.builds.success.count(:all), 'build'
+ %p
+ Failed:
+ %strong= pluralize @project.builds.failed.count(:all), 'build'
+
+ %p
+ Success ratio:
+ %strong
+ #{success_ratio(@project.builds.success, @project.builds.failed)}%
+
+ %p
+ Commits covered:
+ %strong
+ = @project.commits.count(:all)
diff --git a/app/views/ci/charts/show.html.haml b/app/views/ci/charts/show.html.haml
new file mode 100644
index 00000000000..b5fcfc1563c
--- /dev/null
+++ b/app/views/ci/charts/show.html.haml
@@ -0,0 +1,4 @@
+#charts
+ = render 'builds'
+ = render 'build_times'
+= render 'overall'
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
new file mode 100644
index 00000000000..a955a5b6479
--- /dev/null
+++ b/app/views/ci/commits/_commit.html.haml
@@ -0,0 +1,32 @@
+%tr.build.alert{class: commit_status_alert_class(commit)}
+ %td.status
+ = commit.status
+ - if commit.running?
+ &middot;
+ = commit.stage
+
+
+ %td.build-link
+ = link_to ci_project_ref_commit_path(commit.project, commit.ref, commit.sha) do
+ %strong #{commit.short_sha}
+
+ %td.build-message
+ %span= truncate_first_line(commit.git_commit_message)
+
+ %td.build-branch
+ - unless @ref
+ %span
+ = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref)
+
+ %td.duration
+ - if commit.duration > 0
+ #{time_interval_in_words commit.duration}
+
+ %td.timestamp
+ - if commit.finished_at
+ %span #{time_ago_in_words commit.finished_at} ago
+
+ - if commit.project.coverage_enabled?
+ %td.coverage
+ - if commit.coverage
+ #{commit.coverage}%
diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml
new file mode 100644
index 00000000000..4cf567c77e6
--- /dev/null
+++ b/app/views/ci/commits/show.html.haml
@@ -0,0 +1,96 @@
+%h4.page-title
+ = @project.name
+ @
+ #{gitlab_commit_link(@project, @commit.sha)}
+%p
+ = link_to ci_project_path(@project) do
+ &larr; Back to project commits
+%hr
+.commit-info
+ %pre.commit-message
+ #{@commit.git_commit_message}
+
+ .row
+ .col-sm-6
+ - if @commit.compare?
+ %p
+ %span.attr-name Compare:
+ #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)}
+ - else
+ %p
+ %span.attr-name Commit:
+ #{gitlab_commit_link(@project, @commit.sha)}
+
+ %p
+ %span.attr-name Branch:
+ #{gitlab_ref_link(@project, @commit.ref)}
+ .col-sm-6
+ %p
+ %span.attr-name Author:
+ #{@commit.git_author_name} (#{@commit.git_author_email})
+ - if @commit.created_at
+ %p
+ %span.attr-name Created at:
+ #{@commit.created_at.to_s(:short)}
+
+- if current_user && current_user.has_developer_access?(@project.gitlab_id)
+ .pull-right
+ - if @commit.builds.running_or_pending.any?
+ = link_to "Cancel", cancel_ci_project_ref_commit_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger'
+
+
+- if @commit.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @commit.yaml_errors.split(",").each do |error|
+ %li= error
+
+- unless @commit.push_data[:ci_yaml_file]
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+%h3 Status
+
+.build.alert{class: commit_status_alert_class(@commit)}
+ .status
+ = @commit.status.titleize
+
+%h3
+ Builds
+ - if @commit.duration > 0
+ %small.pull-right
+ %i.fa-time
+ #{time_interval_in_words @commit.duration}
+
+%table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @project.coverage_enabled?
+ %th Coverage
+ %th
+ = render @commit.builds_without_retry_sorted, controls: true
+
+- if @commit.retried_builds.any?
+ %h3
+ Retried builds
+
+ %table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @project.coverage_enabled?
+ %th Coverage
+ %th
+ = render @commit.retried_builds
diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml
new file mode 100644
index 00000000000..2788112c835
--- /dev/null
+++ b/app/views/ci/errors/show.haml
@@ -0,0 +1,2 @@
+%h3.error Error
+= @error
diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml
new file mode 100644
index 00000000000..779f49b3d3a
--- /dev/null
+++ b/app/views/ci/events/index.html.haml
@@ -0,0 +1,19 @@
+%h3.page-title Events
+
+%table.table
+ %thead
+ %tr
+ %th User ID
+ %th Description
+ %th When
+ - @events.each do |event|
+ %tr
+ %td
+ = event.user_id
+ %td
+ = event.description
+ %td.light
+ = time_ago_in_words event.updated_at
+ ago
+
+= paginate @events \ No newline at end of file
diff --git a/app/views/ci/helps/oauth2.html.haml b/app/views/ci/helps/oauth2.html.haml
new file mode 100644
index 00000000000..2031b7340d4
--- /dev/null
+++ b/app/views/ci/helps/oauth2.html.haml
@@ -0,0 +1,20 @@
+.welcome-block
+ %h1
+ Welcome to GitLab CI
+ %p
+ GitLab CI integrates with your GitLab installation and runs tests for your projects.
+
+ %h3 You need only 2 steps to set it up
+
+ %ol
+ %li
+ In the GitLab admin area under OAuth applications create a new entry. The redirect url should be
+ %code= callback_ci_user_sessions_url
+ %li
+ Update the GitLab CI config with the application id and the application secret from GitLab.
+ %li
+ Restart your GitLab CI instance
+ %li
+ Refresh this page when GitLab CI has started again
+
+
diff --git a/app/views/ci/helps/show.html.haml b/app/views/ci/helps/show.html.haml
new file mode 100644
index 00000000000..5acdf9fa98a
--- /dev/null
+++ b/app/views/ci/helps/show.html.haml
@@ -0,0 +1,40 @@
+.jumbotron
+ %h2
+ GitLab CI
+ %span= GitlabCi::VERSION
+ %small= GitlabCi::REVISION
+ %p
+ GitLab CI integrates with your GitLab installation and run tests for your projects.
+ %br
+ Login with your GitLab account, add a project with one click and enjoy running your tests.
+ %br
+ Read more about GitLab CI at #{link_to "about.gitlab.com/gitlab-ci", "https://about.gitlab.com/gitlab-ci/", target: "_blank"}.
+
+
+.bs-callout.bs-callout-success
+ %h4
+ = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/api' do
+ %i.fa-cogs
+ API
+ %p Explore how you can access GitLab CI via the API.
+
+.bs-callout.bs-callout-info
+ %h4
+ = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/tree/master/doc/examples' do
+ %i.fa-info-sign
+ Build script examples
+ %p This includes the build script we use to test GitLab CE.
+
+.bs-callout.bs-callout-danger
+ %h4
+ = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/issues' do
+ %i.fa-bug
+ Issue tracker
+ %p Reports about recent bugs and problems..
+
+.bs-callout.bs-callout-warning
+ %h4
+ = link_to 'http://feedback.gitlab.com/forums/176466-general/category/64310-gitlab-ci' do
+ %i.fa-thumbs-up
+ Feedback forum
+ %p Suggest improvements or new features for GitLab CI.
diff --git a/app/views/ci/kaminari/_first_page.html.haml b/app/views/ci/kaminari/_first_page.html.haml
new file mode 100644
index 00000000000..a1bbf18690c
--- /dev/null
+++ b/app/views/ci/kaminari/_first_page.html.haml
@@ -0,0 +1,2 @@
+%li
+ = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote
diff --git a/app/views/ci/kaminari/_gap.html.haml b/app/views/ci/kaminari/_gap.html.haml
new file mode 100644
index 00000000000..dfe33aac21d
--- /dev/null
+++ b/app/views/ci/kaminari/_gap.html.haml
@@ -0,0 +1,2 @@
+%li.disabled
+ = link_to raw(t 'views.pagination.truncate'), '#'
diff --git a/app/views/ci/kaminari/_last_page.html.haml b/app/views/ci/kaminari/_last_page.html.haml
new file mode 100644
index 00000000000..e70697d04ad
--- /dev/null
+++ b/app/views/ci/kaminari/_last_page.html.haml
@@ -0,0 +1,2 @@
+%li
+ = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote}
diff --git a/app/views/ci/kaminari/_next_page.html.haml b/app/views/ci/kaminari/_next_page.html.haml
new file mode 100644
index 00000000000..ea9af4539e0
--- /dev/null
+++ b/app/views/ci/kaminari/_next_page.html.haml
@@ -0,0 +1,2 @@
+%li
+ = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote
diff --git a/app/views/ci/kaminari/_page.html.haml b/app/views/ci/kaminari/_page.html.haml
new file mode 100644
index 00000000000..9df7ce02f8f
--- /dev/null
+++ b/app/views/ci/kaminari/_page.html.haml
@@ -0,0 +1,2 @@
+%li{class: "#{'active' if page.current?}"}
+ = link_to page, page.current? ? '#' : url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}
diff --git a/app/views/ci/kaminari/_paginator.html.haml b/app/views/ci/kaminari/_paginator.html.haml
new file mode 100644
index 00000000000..07fdb1e08a6
--- /dev/null
+++ b/app/views/ci/kaminari/_paginator.html.haml
@@ -0,0 +1,11 @@
+= paginator.render do
+ %ul.pagination
+ = first_page_tag unless current_page.first?
+ = prev_page_tag unless current_page.first?
+ - each_page do |page|
+ - if page.left_outer? || page.right_outer? || page.inside_window?
+ = page_tag page
+ - elsif !page.was_truncated?
+ = gap_tag
+ = next_page_tag unless current_page.last?
+ = last_page_tag unless current_page.last?
diff --git a/app/views/ci/kaminari/_prev_page.html.haml b/app/views/ci/kaminari/_prev_page.html.haml
new file mode 100644
index 00000000000..dab3b318dac
--- /dev/null
+++ b/app/views/ci/kaminari/_prev_page.html.haml
@@ -0,0 +1,2 @@
+%li
+ = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
new file mode 100644
index 00000000000..903b92de689
--- /dev/null
+++ b/app/views/ci/lints/_create.html.haml
@@ -0,0 +1,39 @@
+- if @status
+ %p
+ %b Status:
+ syntax is correct
+ %i.fa-ok.correct-syntax
+
+ %table.table.table-bordered
+ %thead
+ %tr
+ %th Parameter
+ %th Value
+ %tbody
+ - @stages.each do |stage|
+ - @builds.select { |build| build[:stage] == stage }.each do |build|
+ %tr
+ %td #{stage.capitalize} Job - #{build[:name]}
+ %td
+ %pre
+ = simple_format build[:script]
+
+ %br
+ %b Tag list:
+ = build[:tags]
+ %br
+ %b Refs only:
+ = build[:only] && build[:only].join(", ")
+ %br
+ %b Refs except:
+ = build[:except] && build[:except].join(", ")
+
+-else
+ %p
+ %b Status:
+ syntax is incorrect
+ %i.fa-remove.incorrect-syntax
+ %b Error:
+ = @error
+
+
diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml
new file mode 100644
index 00000000000..a96c0b11b6e
--- /dev/null
+++ b/app/views/ci/lints/create.js.haml
@@ -0,0 +1,2 @@
+:plain
+ $(".results").html("#{escape_javascript(render "create")}") \ No newline at end of file
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
new file mode 100644
index 00000000000..b0fd5dd8e58
--- /dev/null
+++ b/app/views/ci/lints/show.html.haml
@@ -0,0 +1,25 @@
+%h2 Check your .gitlab-ci.yml
+%hr
+
+= form_tag ci_lint_path, method: :post, remote: true do
+ .control-group
+ = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label'
+ .controls
+ = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+
+ .control-group.clearfix
+ .controls.pull-left.prepend-top-10
+ = submit_tag "Validate", class: 'btn btn-success submit-yml'
+
+
+%p.text-center.loading
+ %i.fa-refresh.fa-spin
+
+.results.prepend-top-20
+
+:coffeescript
+ $(".loading").hide()
+ $('form').bind 'ajax:beforeSend', ->
+ $(".loading").show()
+ $('form').bind 'ajax:complete', ->
+ $(".loading").hide()
diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml
new file mode 100644
index 00000000000..d818e8b6756
--- /dev/null
+++ b/app/views/ci/notify/build_fail_email.html.haml
@@ -0,0 +1,19 @@
+- content_for :header do
+ %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
+ GitLab CI (build failed)
+%h3
+ Project:
+ = link_to ci_project_url(@project) do
+ = @project.name
+
+%p
+ Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+%p
+ Author: #{@build.commit.git_author_name}
+%p
+ Branch: #{@build.commit.ref}
+%p
+ Message: #{@build.commit.git_commit_message}
+
+%p
+ Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)}
diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb
new file mode 100644
index 00000000000..1add215a1c8
--- /dev/null
+++ b/app/views/ci/notify/build_fail_email.text.erb
@@ -0,0 +1,9 @@
+Build failed for <%= @project.name %>
+
+Status: <%= @build.status %>
+Commit: <%= @build.commit.short_sha %>
+Author: <%= @build.commit.git_author_name %>
+Branch: <%= @build.commit.ref %>
+Message: <%= @build.commit.git_commit_message %>
+
+Url: <%= ci_project_build_url(@build.project, @build) %>
diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml
new file mode 100644
index 00000000000..a20dcaee24e
--- /dev/null
+++ b/app/views/ci/notify/build_success_email.html.haml
@@ -0,0 +1,20 @@
+- content_for :header do
+ %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
+ GitLab CI (build successful)
+
+%h3
+ Project:
+ = link_to ci_project_url(@project) do
+ = @project.name
+
+%p
+ Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+%p
+ Author: #{@build.commit.git_author_name}
+%p
+ Branch: #{@build.commit.ref}
+%p
+ Message: #{@build.commit.git_commit_message}
+
+%p
+ Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)}
diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb
new file mode 100644
index 00000000000..7ebd17e7270
--- /dev/null
+++ b/app/views/ci/notify/build_success_email.text.erb
@@ -0,0 +1,9 @@
+Build successful for <%= @project.name %>
+
+Status: <%= @build.status %>
+Commit: <%= @build.commit.short_sha %>
+Author: <%= @build.commit.git_author_name %>
+Branch: <%= @build.commit.ref %>
+Message: <%= @build.commit.git_commit_message %>
+
+Url: <%= ci_project_build_url(@build.project, @build) %>
diff --git a/app/views/ci/projects/_form.html.haml b/app/views/ci/projects/_form.html.haml
new file mode 100644
index 00000000000..d50e1a83b06
--- /dev/null
+++ b/app/views/ci/projects/_form.html.haml
@@ -0,0 +1,101 @@
+.bs-callout.help-callout
+ %p
+ If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path}
+ %p
+ Edit your
+ #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@project)}
+
+= nested_form_for [:ci, @project], html: { class: 'form-horizontal' } do |f|
+ - if @project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @project.errors.full_messages.each do |msg|
+ %li= msg
+
+ %fieldset
+ %legend Build settings
+ .form-group
+ = label_tag nil, class: 'control-label' do
+ Get code
+ .col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = label_tag do
+ = f.radio_button :allow_git_fetch, 'false'
+ %strong git clone
+ .light Slower but makes sure you have a clean dir before every build
+ .radio
+ = label_tag do
+ = f.radio_button :allow_git_fetch, 'true'
+ %strong git fetch
+ .light Faster
+ .form-group
+ = f.label :timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :timeout_in_minutes, class: 'form-control', min: '0'
+ .light per build in minutes
+
+
+ %fieldset
+ %legend Build Schedule
+ .form-group
+ = f.label :always_build, 'Schedule build', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.label :always_build do
+ = f.check_box :always_build
+ %span.light Repeat last build after X hours if no builds
+ .form-group
+ = f.label :polling_interval, "Build interval", class: 'control-label'
+ .col-sm-10
+ = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control'
+ .light In hours
+
+ %fieldset
+ %legend Project settings
+ .form-group
+ = f.label :default_ref, "Make tabs for the following branches", class: 'control-label'
+ .col-sm-10
+ = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable'
+ .light You will be able to filter builds by the following branches
+ .form-group
+ = f.label :public, 'Public mode', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.label :public do
+ = f.check_box :public
+ %span.light Anyone can see project and builds
+ .form-group
+ = f.label :coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%$
+
+
+
+ %fieldset
+ %legend Advanced settings
+ .form-group
+ = f.label :token, "CI token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89'
+
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save'
+ = link_to 'Cancel', projects_path, class: 'btn'
+ - unless @project.new_record?
+ = link_to 'Remove Project', ci_project_path(@project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right'
diff --git a/app/views/ci/projects/_gl_projects.html.haml b/app/views/ci/projects/_gl_projects.html.haml
new file mode 100644
index 00000000000..6ed19e13887
--- /dev/null
+++ b/app/views/ci/projects/_gl_projects.html.haml
@@ -0,0 +1,15 @@
+- @gl_projects.sort_by(&:name_with_namespace).each do |project|
+ %tr.light
+ %td
+ = project.name_with_namespace
+ %td
+ %small Not added to CI
+ %td
+ %td
+ - if Ci::Project.already_added?(project)
+ %strong.cgreen
+ Added
+ - else
+ = form_tag ci_projects_path do
+ = hidden_field_tag :project, project.to_h.to_json
+ = submit_tag 'Add project to CI', class: 'btn btn-default btn-sm'
diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml
new file mode 100644
index 00000000000..1888e1bde93
--- /dev/null
+++ b/app/views/ci/projects/_info.html.haml
@@ -0,0 +1,2 @@
+- if no_runners_for_project?(@project)
+ = render 'no_runners'
diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/ci/projects/_no_runners.html.haml
new file mode 100644
index 00000000000..c0a296fb17d
--- /dev/null
+++ b/app/views/ci/projects/_no_runners.html.haml
@@ -0,0 +1,8 @@
+.alert.alert-danger
+ %p
+ There are NO runners to build this project.
+ %br
+ You can add Specific runner for this project on Runners page
+
+ - if current_user.is_admin
+ or add Shared runner for whole application in admin are.
diff --git a/app/views/ci/projects/_project.html.haml b/app/views/ci/projects/_project.html.haml
new file mode 100644
index 00000000000..3e893410df8
--- /dev/null
+++ b/app/views/ci/projects/_project.html.haml
@@ -0,0 +1,22 @@
+- last_commit = project.last_commit
+%tr.alert{class: commit_status_alert_class(last_commit) }
+ %td
+ = link_to project do
+ %strong= project.name
+ %td
+ - if last_commit
+ #{last_commit.status} (#{commit_link(last_commit)})
+ - if project.last_commit_date
+ = time_ago_in_words project.last_commit_date
+ ago
+ - else
+ No builds yet
+ %td
+ - if project.public
+ %i.fa-globe
+ Public
+ - else
+ %i.fa-lock
+ Private
+ %td
+ = project.commits.count
diff --git a/app/views/ci/projects/_public.html.haml b/app/views/ci/projects/_public.html.haml
new file mode 100644
index 00000000000..c2157ab741a
--- /dev/null
+++ b/app/views/ci/projects/_public.html.haml
@@ -0,0 +1,21 @@
+= content_for :title do
+ %h3.project-title
+ Public projects
+
+.bs-callout
+ = link_to new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath)) do
+ %strong Login with GitLab
+ to see your private projects
+
+- if @projects.present?
+ .projects
+ %table.table
+ %tr
+ %th Name
+ %th Last commit
+ %th Access
+ %th Commits
+ = render @projects
+ = paginate @projects
+- else
+ %h4 No public projects yet
diff --git a/app/views/ci/projects/_search.html.haml b/app/views/ci/projects/_search.html.haml
new file mode 100644
index 00000000000..37fb804d8d0
--- /dev/null
+++ b/app/views/ci/projects/_search.html.haml
@@ -0,0 +1,18 @@
+.search
+ = form_tag "#", method: :get, class: 'navbar-form' do |f|
+ .form-group
+ .input-group
+ = search_field_tag "search", params[:search], placeholder: "Search", class: "search-input form-control"
+ .input-group-addon
+ %i.fa-search
+
+
+:coffeescript
+ $('.search .navbar-form').submit ->
+ NProgress.start()
+ query = $('.search .navbar-form .search-input').val()
+ $.get '#{gitlab_ci_projects_path}', { search: query }, (data) ->
+ $(".projects").html data.html
+ NProgress.done()
+ CiPager.init "#{gitlab_ci_projects_path}" + "?search=" + query, #{Ci::ProjectsController::PROJECTS_BATCH}, false
+ false
diff --git a/app/views/ci/projects/edit.html.haml b/app/views/ci/projects/edit.html.haml
new file mode 100644
index 00000000000..298007a6565
--- /dev/null
+++ b/app/views/ci/projects/edit.html.haml
@@ -0,0 +1,21 @@
+- if @project.generated_yaml_config
+ %p.alert.alert-danger
+ CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_project_path(@project)}
+ or
+ %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview
+ yaml file which is based on your old jobs.
+ Put this file to the root of your project and name it .gitlab-ci.yml
+
+= render 'form'
+
+- if @project.generated_yaml_config
+ #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"}
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} ×
+ %h4.modal-title Content of .gitlab-ci.yml
+ .modal-body
+ = text_area_tag :yaml, @project.generated_yaml_config, size: "70x25", class: "form-control"
+ .modal-footer
+ %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close
diff --git a/app/views/ci/projects/gitlab.html.haml b/app/views/ci/projects/gitlab.html.haml
new file mode 100644
index 00000000000..dbc0ea0880f
--- /dev/null
+++ b/app/views/ci/projects/gitlab.html.haml
@@ -0,0 +1,35 @@
+- if @offset == 0
+ .clearfix.light
+ .pull-left.fetch-status
+ Fetched from GitLab (#{link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink})
+ - if params[:search].present?
+ by keyword: "#{params[:search]}",
+ #{time_ago_in_words(current_user.sync_at)} ago.
+ = link_to gitlab_ci_projects_path(reset_cache: true, search: params[:search]), class: 'sync-now btn btn-sm btn-default reset-cache' do
+ %i.fa-refresh
+ Sync now
+ %br
+
+ .pull-right
+ #{@total_count} projects, #{@projects.size} of them added to CI
+ %br
+
+ %table.table.projects-table.content-list
+ %thead
+ %tr
+ %th Project Name
+ %th Last commit
+ %th Access
+ %th Commits
+
+ = render @projects
+
+ = render "gl_projects"
+
+ %p.text-center.hide.loading
+ %i.fa-refresh.fa-spin
+
+- else
+ = render @projects
+
+ = render "gl_projects"
diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml
new file mode 100644
index 00000000000..6243a28f9e2
--- /dev/null
+++ b/app/views/ci/projects/index.html.haml
@@ -0,0 +1,22 @@
+- if current_user
+ = content_for :title do
+ %h3.project-title
+ Dashboard
+ .pull-right
+ = render "search"
+
+ .projects
+ %p.fetch-status.light
+ %i.fa-refresh.fa-spin
+ Please wait while we fetch from GitLab (#{GitlabCi.config.gitlab_server.url})
+ :coffeescript
+ $.get '#{gitlab_ci_projects_path}', (data) ->
+ $(".projects").html data.html
+ $('.projects').on 'click', '.reset-cache', ->
+ $.get '#{gitlab_ci_projects_path}', { reset_cache: true }, (data) ->
+ $(".projects").html data.html
+ false
+ CiPager.init "#{gitlab_ci_projects_path}", #{Ci::ProjectsController::PROJECTS_BATCH}, false
+
+- else
+ = render 'public'
diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml
new file mode 100644
index 00000000000..27899591391
--- /dev/null
+++ b/app/views/ci/projects/show.html.haml
@@ -0,0 +1,59 @@
+= render 'ci/shared/guide' unless @project.setup_finished?
+
+- if current_user && current_user.can_manage_project?(@project.gitlab_id) && !@project.any_runners?
+ .alert.alert-danger
+ Builds for this project wont be served unless you configure runners on
+ = link_to "Runners page", ci_project_runners_path(@project)
+
+%ul.nav.nav-tabs.append-bottom-20
+ %li{class: ref_tab_class}
+ = link_to 'All commits', ci_project_path(@project)
+ - @project.tracked_refs.each do |ref|
+ %li{class: ref_tab_class(ref)}
+ = link_to ref, ci_project_path(@project, ref: ref)
+
+ - if @ref && !@project.tracked_refs.include?(@ref)
+ %li{class: 'active'}
+ = link_to @ref, ci_project_path(@project, ref: @ref)
+
+
+
+- if @ref
+ %p
+ Paste build status image for #{@ref} with next link
+ = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do
+ Status Badge
+ .badge-codes-block.bs-callout.bs-callout-info.hide
+ %p
+ Status badge for
+ %span.label.label-info #{@ref}
+ branch
+ %div
+ %label Markdown:
+ = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control'
+ %label Html:
+ = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control'
+
+
+
+
+%table.builds
+ %thead
+ %tr
+ %th Status
+ %th Commit
+ %th Message
+ %th Branch
+ %th Total duration
+ %th Finished at
+ - if @project.coverage_enabled?
+ %th Coverage
+
+ = render @commits
+
+= paginate @commits
+
+- if @commits.empty?
+ .bs-callout
+ %h4 No commits yet
+
diff --git a/app/views/ci/runners/_runner.html.haml b/app/views/ci/runners/_runner.html.haml
new file mode 100644
index 00000000000..7ead5736bb1
--- /dev/null
+++ b/app/views/ci/runners/_runner.html.haml
@@ -0,0 +1,35 @@
+%li.runner{id: dom_id(runner)}
+ %h4
+ = runner_status_icon(runner)
+ %span.monospace
+ - if @runners.include?(runner)
+ = link_to runner.short_sha, [:ci, @project, runner]
+ %small
+ =link_to edit_ci_project_runner_path(@project, runner) do
+ %i.fa.fa-edit.btn
+ - else
+ = runner.short_sha
+
+ .pull-right
+ - if @runners.include?(runner)
+ - if runner.belongs_to_one_project?
+ = link_to 'Remove runner', [:ci, @project, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - else
+ - runner_project = @project.runner_projects.find_by(runner_id: runner)
+ = link_to 'Disable for this project', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - elsif runner.specific?
+ = form_for [:ci, @project, @project.runner_projects.new] do |f|
+ = f.hidden_field :runner_id, value: runner.id
+ = f.submit 'Enable for this project', class: 'btn btn-sm'
+ .pull-right
+ %small.light
+ \##{runner.id}
+ - if runner.description.present?
+ %p.runner-description
+ = runner.description
+ - if runner.tag_list.present?
+ %p
+ - runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+
diff --git a/app/views/ci/runners/_shared_runners.html.haml b/app/views/ci/runners/_shared_runners.html.haml
new file mode 100644
index 00000000000..944b3fd930d
--- /dev/null
+++ b/app/views/ci/runners/_shared_runners.html.haml
@@ -0,0 +1,23 @@
+%h3 Shared runners
+
+.bs-callout.bs-callout-warning
+ GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
+ %hr
+ - if @project.shared_runners_enabled
+ = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-warning', method: :post do
+ Disable shared runners
+ - else
+ = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-success', method: :post do
+ Enable shared runners
+ &nbsp; for this project
+
+- if @shared_runners_count.zero?
+ This application has no shared runners yet.
+ Please use specific runners or ask administrator to create one
+- else
+ %h4.underlined-title Available shared runners - #{@shared_runners_count}
+ %ul.bordered-list.available-shared-runners
+ = render @shared_runners.first(10)
+ - if @shared_runners_count > 10
+ .light
+ and #{@shared_runners_count - 10} more...
diff --git a/app/views/ci/runners/_specific_runners.html.haml b/app/views/ci/runners/_specific_runners.html.haml
new file mode 100644
index 00000000000..0604e7a46c5
--- /dev/null
+++ b/app/views/ci/runners/_specific_runners.html.haml
@@ -0,0 +1,29 @@
+%h3 Specific runners
+
+.bs-callout.help-callout
+ %h4 How to setup a new project specific runner
+
+ %ol
+ %li
+ Install GitLab Runner software.
+ Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+ %li
+ Specify following URL during runner setup:
+ %code #{ci_root_url(only_path: false)}
+ %li
+ Use the following registration token during setup:
+ %code #{@project.token}
+ %li
+ Start runner!
+
+
+- if @runners.any?
+ %h4.underlined-title Runners activated for this project
+ %ul.bordered-list.activated-specific-runners
+ = render @runners
+
+- if @specific_runners.any?
+ %h4.underlined-title Available specific runners
+ %ul.bordered-list.available-specific-runners
+ = render @specific_runners
+ = paginate @specific_runners
diff --git a/app/views/ci/runners/edit.html.haml b/app/views/ci/runners/edit.html.haml
new file mode 100644
index 00000000000..81c8e58ae2b
--- /dev/null
+++ b/app/views/ci/runners/edit.html.haml
@@ -0,0 +1,27 @@
+%h4 Runner ##{@runner.id}
+%hr
+= form_for [:ci, @project, @runner], html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label :active, "Active", class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :active
+ %span.light Paused runners don't accept new builds
+ .form-group
+ = label_tag :token, class: 'control-label' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group
+ = label_tag :description, class: 'control-label' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group
+ = label_tag :tag_list, class: 'control-label' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, class: 'form-control'
+ .help-block You can setup jobs to only use runners with specific tags
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/ci/runners/index.html.haml b/app/views/ci/runners/index.html.haml
new file mode 100644
index 00000000000..529fb9c296d
--- /dev/null
+++ b/app/views/ci/runners/index.html.haml
@@ -0,0 +1,25 @@
+.light
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+
+ %p Each runner can be in one of the following states:
+ %div
+ %ul
+ %li
+ %span.label.label-success active
+ \- runner is active and can process any new build
+ %li
+ %span.label.label-danger paused
+ \- runner is paused and will not receive any new build
+
+%hr
+
+%p.lead To start serving your builds you can either add specific runners to your project or use shared runners
+.row
+ .col-sm-6
+ = render 'specific_runners'
+ .col-sm-6
+ = render 'shared_runners'
diff --git a/app/views/ci/runners/show.html.haml b/app/views/ci/runners/show.html.haml
new file mode 100644
index 00000000000..ffec495f85a
--- /dev/null
+++ b/app/views/ci/runners/show.html.haml
@@ -0,0 +1,64 @@
+= content_for :title do
+ %h3.project-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+%table.table
+ %thead
+ %tr
+ %th Property Name
+ %th Value
+ %tr
+ %td
+ Tags
+ %td
+ - @runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ %tr
+ %td
+ Name
+ %td
+ = @runner.name
+ %tr
+ %td
+ Version
+ %td
+ = @runner.version
+ %tr
+ %td
+ Revision
+ %td
+ = @runner.revision
+ %tr
+ %td
+ Platform
+ %td
+ = @runner.platform
+ %tr
+ %td
+ Architecture
+ %td
+ = @runner.architecture
+ %tr
+ %td
+ Description
+ %td
+ = @runner.description
+ %tr
+ %td
+ Last contact
+ %td
+ - if @runner.contacted_at
+ #{time_ago_in_words(@runner.contacted_at)} ago
+ - else
+ Never
+
+
+
diff --git a/app/views/ci/services/_form.html.haml b/app/views/ci/services/_form.html.haml
new file mode 100644
index 00000000000..9110aaa0528
--- /dev/null
+++ b/app/views/ci/services/_form.html.haml
@@ -0,0 +1,57 @@
+%h3.page-title
+ = @service.title
+ = boolean_to_icon @service.activated?
+
+%p= @service.description
+
+.back-link
+ = link_to ci_project_services_path(@project) do
+ &larr; to services
+
+%hr
+
+= form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
+ - if @service.errors.any?
+ .alert.alert-danger
+ %ul
+ - @service.errors.full_messages.each do |msg|
+ %li= msg
+
+ - if @service.help.present?
+ .bs-callout
+ = @service.help
+
+ .form-group
+ = f.label :active, "Active", class: "control-label"
+ .col-sm-10
+ = f.check_box :active
+
+ - @service.fields.each do |field|
+ - name = field[:name]
+ - label = field[:label] || name
+ - value = @service.send(name)
+ - type = field[:type]
+ - placeholder = field[:placeholder]
+ - choices = field[:choices]
+ - default_choice = field[:default_choice]
+ - help = field[:help]
+
+ .form-group
+ = f.label label, class: "control-label"
+ .col-sm-10
+ - if type == 'text'
+ = f.text_field name, class: "form-control", placeholder: placeholder
+ - elsif type == 'textarea'
+ = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
+ - elsif type == 'checkbox'
+ = f.check_box name
+ - elsif type == 'select'
+ = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
+ - if help
+ .light #{help}
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ &nbsp;
+ - if @service.valid? && @service.activated? && @service.can_test?
+ = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn'
diff --git a/app/views/ci/services/edit.html.haml b/app/views/ci/services/edit.html.haml
new file mode 100644
index 00000000000..bcc5832792f
--- /dev/null
+++ b/app/views/ci/services/edit.html.haml
@@ -0,0 +1 @@
+= render 'form'
diff --git a/app/views/ci/services/index.html.haml b/app/views/ci/services/index.html.haml
new file mode 100644
index 00000000000..37e5723b541
--- /dev/null
+++ b/app/views/ci/services/index.html.haml
@@ -0,0 +1,22 @@
+%h3.page-title Project services
+%p.light Project services allow you to integrate GitLab CI with other applications
+
+%table.table
+ %thead
+ %tr
+ %th
+ %th Service
+ %th Desription
+ %th Last edit
+ - @services.sort_by(&:title).each do |service|
+ %tr
+ %td
+ = boolean_to_icon service.activated?
+ %td
+ = link_to edit_ci_project_service_path(@project, service.to_param) do
+ %strong= service.title
+ %td
+ = service.description
+ %td.light
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml
new file mode 100644
index 00000000000..8a42f29b77c
--- /dev/null
+++ b/app/views/ci/shared/_guide.html.haml
@@ -0,0 +1,15 @@
+.bs-callout.help-callout
+ %h4 How to setup CI for this project
+
+ %ol
+ %li
+ Add at least one runner to the project.
+ Go to #{link_to 'Runners page', ci_project_runners_path(@project), target: :blank} for instructions.
+ %li
+ Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}.
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+ %li
+ Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank}
+ and press the "Test settings" button.
+ %li
+ Return to this page and refresh it, it should show a new build.
diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml
new file mode 100644
index 00000000000..f56c37d9b37
--- /dev/null
+++ b/app/views/ci/shared/_no_runners.html.haml
@@ -0,0 +1,7 @@
+.alert.alert-danger
+ %p
+ Now you need Runners to process your builds.
+ %span
+ Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+
+
diff --git a/app/views/ci/triggers/_trigger.html.haml b/app/views/ci/triggers/_trigger.html.haml
new file mode 100644
index 00000000000..addfbfcb0d4
--- /dev/null
+++ b/app/views/ci/triggers/_trigger.html.haml
@@ -0,0 +1,14 @@
+%tr
+ %td
+ .clearfix
+ %span.monospace= trigger.token
+
+ %td
+ - if trigger.last_trigger_request
+ #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago
+ - else
+ Never
+
+ %td
+ .pull-right
+ = link_to 'Revoke', ci_project_trigger_path(@project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped"
diff --git a/app/views/ci/triggers/index.html.haml b/app/views/ci/triggers/index.html.haml
new file mode 100644
index 00000000000..f04c116231d
--- /dev/null
+++ b/app/views/ci/triggers/index.html.haml
@@ -0,0 +1,67 @@
+%h3
+ Triggers
+
+%p.light
+ Triggers can be used to force a rebuild of a specific branch or tag with an API call.
+
+%hr.clearfix
+
+-if @triggers.any?
+ %table.table
+ %thead
+ %th Token
+ %th Last used
+ %th
+ = render @triggers
+- else
+ %h4 No triggers
+
+= form_for [:ci, @project, @trigger], html: { class: 'form-horizontal' } do |f|
+ .clearfix
+ = f.submit "Add Trigger", class: 'btn btn-success pull-right'
+
+%hr.clearfix
+
+-if @triggers.any?
+ %h3
+ Use CURL
+
+ %p.light
+ Copy the token above and set your branch or tag name. This is the reference that will be rebuild.
+
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ #{ci_build_trigger_url(@project.id, 'REF_NAME')}
+ %h3
+ Use .gitlab-ci.yml
+
+ %p.light
+ Copy the snippet to
+ %i .gitlab-ci.yml
+ of dependent project.
+ At the end of your build it will trigger this project to rebuilt.
+
+ %pre
+ :plain
+ trigger:
+ type: deploy
+ script:
+ - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}"
+ %h3
+ Pass build variables
+
+ %p.light
+ Add
+ %strong variables[VARIABLE]=VALUE
+ to API request.
+ The value of variable could then be used to distinguish triggered build from normal one.
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{ci_build_trigger_url(@project.id, 'REF_NAME')}
diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml
new file mode 100644
index 00000000000..308b217ea78
--- /dev/null
+++ b/app/views/ci/user_sessions/new.html.haml
@@ -0,0 +1,8 @@
+.login-block
+ %h2 Login using GitLab account
+ %p.light
+ Make sure you have account on GitLab server
+ = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink
+ %hr
+ = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' )
+
diff --git a/app/views/ci/user_sessions/show.html.haml b/app/views/ci/user_sessions/show.html.haml
new file mode 100644
index 00000000000..43f64a429b2
--- /dev/null
+++ b/app/views/ci/user_sessions/show.html.haml
@@ -0,0 +1,15 @@
+= image_tag user_avatar_url(current_user, 90), class: 'avatar avatar-inline avatar-tile s90', alt: ''
+%h3
+ Hi, #{@user.name}
+
+ - if @user.is_admin
+ %span.label.label-success Admin
+
+.profile-block
+ %p
+ %span.light Email:
+ %strong= @user.email
+
+ %p
+ %span.light GitLab profile:
+ %strong= link_to @user.username, GitlabCi.config.gitlab_server.url + '/u/' + @user.username, target: "_blank"
diff --git a/app/views/ci/variables/show.html.haml b/app/views/ci/variables/show.html.haml
new file mode 100644
index 00000000000..5cced18a09f
--- /dev/null
+++ b/app/views/ci/variables/show.html.haml
@@ -0,0 +1,37 @@
+%h3 Secret Variables
+%p.light
+ These variables will be set to environment by the runner and will be hidden in the build log.
+ %br
+ So you can use them for passwords, secret keys or whatever you want.
+
+%hr
+
+
+= nested_form_for @project, url: url_for(controller: 'ci/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
+ - if @project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @project.errors.full_messages.each do |msg|
+ %li= msg
+
+ = f.fields_for :variables do |variable_form|
+ .form-group
+ = variable_form.label :key, 'Key', class: 'control-label'
+ .col-sm-10
+ = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE"
+
+ .form-group
+ = variable_form.label :value, 'Value', class: 'control-label'
+ .col-sm-10
+ = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: ""
+
+ = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10'
+ %hr
+ %p
+ .clearfix
+ = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right'
+
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url
diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/ci/web_hooks/index.html.haml
new file mode 100644
index 00000000000..92c43cd1d9d
--- /dev/null
+++ b/app/views/ci/web_hooks/index.html.haml
@@ -0,0 +1,92 @@
+%h3
+ Web hooks
+
+%p.light
+ Web Hooks can be used for binding events when build completed.
+
+%hr.clearfix
+
+= form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f|
+ -if @web_hook.errors.any?
+ .alert.alert-danger
+ - @web_hook.errors.full_messages.each do |msg|
+ %p= msg
+ .form-group
+ = f.label :url, "URL", class: 'control-label'
+ .col-sm-10
+ = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
+ .form-actions
+ = f.submit "Add Web Hook", class: "btn btn-create"
+
+-if @web_hooks.any?
+ %h4 Activated web hooks (#{@web_hooks.count})
+ %table.table
+ - @web_hooks.each do |hook|
+ %tr
+ %td
+ .clearfix
+ %span.monospace= hook.url
+ %td
+ .pull-right
+ - if @project.commits.any?
+ = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+
+%h4 Web Hook data example
+
+:erb
+ <pre>
+ <code>
+ {
+ "build_id": 2,
+ "build_name":"rspec_linux"
+ "build_status": "failed",
+ "build_started_at": "2014-05-05T18:01:02.563Z",
+ "build_finished_at": "2014-05-05T18:01:07.611Z",
+ "project_id": 1,
+ "project_name": "Brightbox \/ Brightbox Cli",
+ "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
+ "ref": "master",
+ "sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "push_data": {
+ "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "after": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "ref": "refs\/heads\/master",
+ "user_id": 1,
+ "user_name": "Administrator",
+ "project_id": 5,
+ "repository": {
+ "name": "Brightbox Cli",
+ "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
+ "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
+ "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
+ },
+ "commits": [
+ {
+ "id": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "message": "Release v1.2.2",
+ "timestamp": "2014-04-22T16:46:42+03:00",
+ "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
+ "author": {
+ "name": "Paul Thornthwaite",
+ "email": "tokengeek@gmail.com"
+ }
+ },
+ {
+ "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
+ "timestamp": "2014-04-11T18:17:26+03:00",
+ "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "author": {
+ "name": "Paul Thornthwaite",
+ "email": "tokengeek@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 2,
+ "ci_yaml_file":"rspec_linux:\r\n script: ls\r\n"
+ }
+ }
+ </code>
+ </pre>
diff --git a/app/views/layouts/ci/_head.html.haml b/app/views/layouts/ci/_head.html.haml
new file mode 100644
index 00000000000..871752c9812
--- /dev/null
+++ b/app/views/layouts/ci/_head.html.haml
@@ -0,0 +1,11 @@
+%head
+ %meta{charset: "utf-8"}
+ %meta{content: "GitLab Continuous Integration", name: "description"}
+ %title GitLab CI
+ = stylesheet_link_tag "ci/application", :media => "all"
+ = javascript_include_tag "ci/application"
+ = csrf_meta_tags
+ = favicon_link_tag 'ci/favicon.ico'
+ :erb
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml
new file mode 100644
index 00000000000..bce3ce77031
--- /dev/null
+++ b/app/views/layouts/ci/_info.html.haml
@@ -0,0 +1,9 @@
+.container
+ - if alert || notice
+ - if alert
+ .alert.alert-danger= alert
+ - if notice
+ .alert.alert-info= notice
+
+ - if current_user && current_user.is_admin && Ci::Runner.count.zero?
+ = render 'ci/shared/no_runners'
diff --git a/app/views/layouts/ci/_nav.html.haml b/app/views/layouts/ci/_nav.html.haml
new file mode 100644
index 00000000000..4e944d4d0d6
--- /dev/null
+++ b/app/views/layouts/ci/_nav.html.haml
@@ -0,0 +1,32 @@
+.navbar.navbar-static-top.navbar-ci
+ .container
+ .navbar-header
+ %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"}
+ %span.sr-only Toggle navigation
+ %i.fa-reorder
+
+ = link_to 'GitLab CI', ci_root_path, class: "navbar-brand"
+
+ .collapse.navbar-collapse
+ %ul.nav.navbar-nav
+ - if current_user && current_user.is_admin
+ %li
+ = link_to ci_admin_projects_path do
+ Admin
+ %li
+ = link_to 'Help', ci_help_path
+
+ %ul.nav.navbar-nav.pull-right
+ - if current_user
+ %li
+ = link_to ci_user_sessions_path do
+ .profile-holder
+ = image_tag user_avatar_url(current_user, 64), class: 'avatar s32', alt: ''
+ %span= current_user.name
+ %li
+ = link_to ci_user_sessions_path, class: "logout", method: :delete do
+ %i.fa-signout
+ Logout
+ - else
+ %li
+ = link_to "Login with GitLab", auth_ci_user_sessions_path, no_turbolink.merge(class: 'btn btn-success btn-login')
diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml
new file mode 100644
index 00000000000..792a5f1e4dd
--- /dev/null
+++ b/app/views/layouts/ci/_nav_admin.html.haml
@@ -0,0 +1,28 @@
+%ul.nav.nav-pills.nav-stacked.admin-menu
+ = nav_link path: 'projects' do
+ = link_to ci_admin_projects_path do
+ %i.fa-list-alt
+ Projects
+ = nav_link path: 'events' do
+ = link_to ci_admin_events_path do
+ %i.fa-book
+ Events
+ = nav_link path: 'runners#index' do
+ = link_to ci_admin_runners_path do
+ %i.fa-cog
+ Runners
+ %small.pull-right
+ = Ci::Runner.count(:all)
+ = nav_link path: 'builds' do
+ = link_to ci_admin_builds_path do
+ %i.fa-link
+ Builds
+ %small.pull-right
+ = Ci::Build.count(:all)
+ %li
+ %hr
+ = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
+ = link_to ci_admin_application_settings_path do
+ %i.fa-cogs
+ %span
+ Settings
diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml
new file mode 100644
index 00000000000..24ee1609d25
--- /dev/null
+++ b/app/views/layouts/ci/_nav_project.html.haml
@@ -0,0 +1,40 @@
+%ul.nav.nav-pills.nav-stacked.project-menu
+ = nav_link path: 'projects#show' do
+ = link_to ci_project_path(@project) do
+ %i.fa-list-alt
+ Commits
+ %small.pull-right= @project.commits.count
+ = nav_link path: 'charts#show' do
+ = link_to ci_project_charts_path(@project) do
+ %i.fa-bar-chart
+ Charts
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to ci_project_runners_path(@project) do
+ %i.fa-cog
+ Runners
+ = nav_link path: 'variables#index' do
+ = link_to ci_project_variables_path(@project) do
+ %i.fa-code
+ Variables
+ = nav_link path: 'web_hooks#index' do
+ = link_to ci_project_web_hooks_path(@project) do
+ %i.fa-link
+ Web Hooks
+ = nav_link path: 'triggers#index' do
+ = link_to ci_project_triggers_path(@project) do
+ %i.fa-retweet
+ Triggers
+ = nav_link path: 'services#index' do
+ = link_to ci_project_services_path(@project) do
+ %i.fa-share
+ Services
+ = nav_link path: 'events#index' do
+ = link_to ci_project_events_path(@project) do
+ %i.fa-book
+ Events
+ %li
+ %hr
+ = nav_link path: 'projects#edit' do
+ = link_to edit_ci_project_path(@project) do
+ %i.fa-cogs
+ Settings
diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml
new file mode 100644
index 00000000000..71b767cc4f1
--- /dev/null
+++ b/app/views/layouts/ci/admin.html.haml
@@ -0,0 +1,17 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/ci/head'
+ %body{ :'data-page' => body_data_page }
+ = render 'layouts/ci/nav'
+ = render 'layouts/ci/info'
+ - if content_for?(:title)
+ .container.container-title
+ = yield(:title)
+ %hr
+
+ .container
+ .row
+ .col-md-2.append-bottom-20
+ = render 'layouts/ci/nav_admin'
+ .col-md-10
+ = yield
diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml
new file mode 100644
index 00000000000..7306d378e44
--- /dev/null
+++ b/app/views/layouts/ci/application.html.haml
@@ -0,0 +1,13 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/ci/head'
+ %body{ :'data-page' => body_data_page }
+ = render 'layouts/ci/nav'
+ = render 'layouts/ci/info'
+ - if content_for?(:title)
+ .container.container-title
+ = yield(:title)
+ %hr
+
+ .container.container-body
+ = yield
diff --git a/app/views/layouts/ci/empty.html.haml b/app/views/layouts/ci/empty.html.haml
new file mode 100644
index 00000000000..a36ebee7ef3
--- /dev/null
+++ b/app/views/layouts/ci/empty.html.haml
@@ -0,0 +1,13 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/ci/head'
+ %body{ :'data-page' => body_data_page }
+ = render 'layouts/ci/info'
+ - if content_for?(:title)
+ .container.container-title
+ = yield(:title)
+ %hr
+
+ .container.container-body
+ = yield
+
diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml
new file mode 100644
index 00000000000..270b206df5e
--- /dev/null
+++ b/app/views/layouts/ci/notify.html.haml
@@ -0,0 +1,19 @@
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
+ %title
+ GitLab CI
+
+ %body
+ = yield :header
+
+ %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"}
+ %tr
+ %td{align: "left", style: "margin: 0; padding: 10px;"}
+ = yield
+ %br
+ %tr
+ %td{align: "left", style: "margin: 0; padding: 10px;"}
+ %p{style: "font-size:small;color:#777"}
+ - if @project
+ You're receiving this notification because you are the one who triggered a build on the #{@project.name} project.
diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml
new file mode 100644
index 00000000000..d0c0861669d
--- /dev/null
+++ b/app/views/layouts/ci/project.html.haml
@@ -0,0 +1,26 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/ci/head'
+ %body{ :'data-page' => body_data_page }
+ = render 'layouts/ci/nav'
+ = render 'layouts/ci/info'
+ .container
+ %h3.project-title
+ = @project.name
+ - if @project.public
+ %small
+ %i.fa-globe
+ Public
+
+ .pull-right
+ = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' )
+ %hr
+ .container
+ - if current_user && current_user.can_manage_project?(@project.gitlab_id)
+ .row
+ .col-md-2.append-bottom-20
+ = render 'layouts/ci/nav_project'
+ .col-md-10
+ = yield
+ - else
+ = yield
diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb
new file mode 100644
index 00000000000..ebb43570e2a
--- /dev/null
+++ b/app/workers/ci/hip_chat_notifier_worker.rb
@@ -0,0 +1,19 @@
+module Ci
+ class HipChatNotifierWorker
+ include Sidekiq::Worker
+
+ def perform(message, options={})
+ room = options.delete('room')
+ token = options.delete('token')
+ server = options.delete('server')
+ name = options.delete('service_name')
+ client_opts = {
+ api_version: 'v2',
+ server_url: server
+ }
+
+ client = HipChat::Client.new(token, client_opts)
+ client[room].send(name, message, options.symbolize_keys)
+ end
+ end
+end
diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb
new file mode 100644
index 00000000000..3bbb9b4bec7
--- /dev/null
+++ b/app/workers/ci/slack_notifier_worker.rb
@@ -0,0 +1,10 @@
+module Ci
+ class SlackNotifierWorker
+ include Sidekiq::Worker
+
+ def perform(webhook_url, message, options={})
+ notifier = Slack::Notifier.new(webhook_url)
+ notifier.ping(message, options)
+ end
+ end
+end
diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb
new file mode 100644
index 00000000000..0bb83845572
--- /dev/null
+++ b/app/workers/ci/web_hook_worker.rb
@@ -0,0 +1,9 @@
+module Ci
+ class WebHookWorker
+ include Sidekiq::Worker
+
+ def perform(hook_id, data)
+ Ci::WebHook.find(hook_id).execute data
+ end
+ end
+end