summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock46
-rw-r--r--app/assets/javascripts/ci_lint_editor.js.es618
-rw-r--r--app/assets/javascripts/dispatcher.js.es617
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es62
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es6 (renamed from app/assets/javascripts/lib/utils/common_utils.js)15
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es6 (renamed from app/assets/javascripts/member_expiration_date.js)17
-rw-r--r--app/assets/javascripts/vue_pagination/index.js.es6148
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es641
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es699
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es663
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es6131
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es676
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stages.js.es621
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js.es634
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es659
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es673
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js.es618
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/pages/lint.scss10
-rw-r--r--app/assets/stylesheets/pages/members.scss6
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss10
-rw-r--r--app/controllers/projects/group_links_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb30
-rw-r--r--app/controllers/projects/project_members_controller.rb52
-rw-r--r--app/controllers/projects/settings/members_controller.rb55
-rw-r--r--app/finders/members_finder.rb13
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb12
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/key.rb4
-rw-r--r--app/models/label.rb1
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/user.rb4
-rw-r--r--app/serializers/build_action_entity.rb14
-rw-r--r--app/serializers/build_artifact_entity.rb14
-rw-r--r--app/serializers/commit_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb83
-rw-r--r--app/serializers/pipeline_serializer.rb40
-rw-r--r--app/serializers/request_aware_entity.rb9
-rw-r--r--app/serializers/stage_entity.rb38
-rw-r--r--app/serializers/status_entity.rb8
-rw-r--r--app/services/notification_service.rb5
-rw-r--r--app/services/projects/participants_service.rb2
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb4
-rw-r--r--app/views/ci/lints/show.html.haml31
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml9
-rw-r--r--app/views/profiles/keys/_key.html.haml3
-rw-r--r--app/views/profiles/keys/_key_details.html.haml3
-rw-r--r--app/views/projects/branches/index.html.haml3
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml6
-rw-r--r--app/views/projects/group_links/_index.html.haml (renamed from app/views/projects/group_links/index.html.haml)4
-rw-r--r--app/views/projects/pipelines/index.html.haml41
-rw-r--r--app/views/projects/project_members/_index.html.haml22
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml38
-rw-r--r--app/views/projects/project_members/_team.html.haml8
-rw-r--r--app/views/projects/project_members/import.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml29
-rw-r--r--app/views/projects/settings/members/show.html.haml6
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml1
-rw-r--r--app/views/shared/milestones/_issuables.html.haml1
-rw-r--r--app/workers/use_key_worker.rb13
-rw-r--r--changelogs/unreleased/19086-double-newline.yml4
-rw-r--r--changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml4
-rw-r--r--changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml4
-rw-r--r--changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml4
-rw-r--r--changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml5
-rw-r--r--changelogs/unreleased/26014-fix-update-doc.yml4
-rw-r--r--changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml4
-rw-r--r--changelogs/unreleased/26129-add-link-to-branches-page.yml4
-rw-r--r--changelogs/unreleased/26238-buttons-not-accessible.yml4
-rw-r--r--changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml4
-rw-r--r--changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml5
-rw-r--r--changelogs/unreleased/didemacet-ci-lint-page.yml4
-rw-r--r--changelogs/unreleased/feature-log-ldap-to-application-log.yml4
-rw-r--r--changelogs/unreleased/fix-broken-url-on-group-avatar.yml4
-rw-r--r--changelogs/unreleased/get_last_used_date_of_ssh_key.yml4
-rw-r--r--changelogs/unreleased/remove-project-authorizations-id-column.yml4
-rw-r--r--changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml4
-rw-r--r--changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml4
-rw-r--r--changelogs/unreleased/support-google-cloud-storage-backups.yml4
-rw-r--r--changelogs/unreleased/update-gitlab-markup-gem.yml4
-rw-r--r--changelogs/unreleased/validate-title-length.yml4
-rw-r--r--config/application.rb2
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20161221152132_add_last_used_at_to_key.rb9
-rw-r--r--db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb12
-rw-r--r--db/schema.rb7
-rw-r--r--doc/administration/auth/ldap.md9
-rw-r--r--doc/administration/reply_by_email.md4
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/raketasks/backup_restore.md10
-rw-r--r--doc/update/8.14-to-8.15.md87
-rw-r--r--doc/update/8.15-to-8.16.md237
-rw-r--r--doc/update/patch_versions.md39
-rw-r--r--doc/user/project/cycle_analytics.md15
-rw-r--r--doc/workflow/notifications.md2
-rw-r--r--features/steps/project/team_management.rb6
-rw-r--r--lib/api/api.rb6
-rw-r--r--lib/api/helpers.rb39
-rw-r--r--lib/api/helpers/pagination.rb45
-rw-r--r--lib/api/internal.rb8
-rw-r--r--lib/backup/manager.rb44
-rw-r--r--lib/ci/ansi2html.rb2
-rw-r--r--lib/ci/api/api.rb10
-rw-r--r--lib/email_template_interceptor.rb4
-rw-r--r--lib/gitlab/ldap/access.rb26
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb8
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb6
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb22
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb14
-rw-r--r--spec/controllers/projects/settings/members_controller_spec.rb14
-rw-r--r--spec/db/production/settings.rb5
-rw-r--r--spec/factories/ci/pipelines.rb8
-rw-r--r--spec/factories/ci/runners.rb4
-rw-r--r--spec/features/ci_lint_spec.rb9
-rw-r--r--spec/features/projects/group_links_spec.rb4
-rw-r--r--spec/features/projects/members/anonymous_user_sees_members_spec.rb4
-rw-r--r--spec/features/projects/members/group_links_spec.rb4
-rw-r--r--spec/features/projects/members/group_members_spec.rb8
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb8
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb441
-rw-r--r--spec/features/security/project/internal_access_spec.rb4
-rw-r--r--spec/features/security/project/private_access_spec.rb4
-rw-r--r--spec/features/security/project/public_access_spec.rb4
-rw-r--r--spec/initializers/secret_token_spec.rb7
-rw-r--r--spec/javascripts/vue_pagination/pagination_spec.js.es6168
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb94
-rw-r--r--spec/lib/ci/ansi2html_spec.rb8
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb114
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb63
-rw-r--r--spec/models/ci/pipeline_spec.rb42
-rw-r--r--spec/models/environment_spec.rb17
-rw-r--r--spec/models/key_spec.rb9
-rw-r--r--spec/models/label_spec.rb2
-rw-r--r--spec/serializers/build_action_entity_spec.rb21
-rw-r--r--spec/serializers/build_artifact_entity_spec.rb22
-rw-r--r--spec/serializers/commit_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_entity_spec.rb138
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb101
-rw-r--r--spec/serializers/request_aware_entity_spec.rb22
-rw-r--r--spec/serializers/stage_entity_spec.rb51
-rw-r--r--spec/serializers/status_entity_spec.rb23
-rw-r--r--spec/services/projects/participants_service_spec.rb32
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb17
-rw-r--r--spec/support/stub_env.rb7
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/views/shared/milestones/_issuables.html.haml.rb32
-rw-r--r--spec/workers/use_key_worker_spec.rb23
156 files changed, 3297 insertions, 575 deletions
diff --git a/Gemfile b/Gemfile
index 29695c08e45..af5566595ed 100644
--- a/Gemfile
+++ b/Gemfile
@@ -84,10 +84,14 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40'
+gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
+# for Google storage
+gem 'google-api-client', '~> 0.8.6'
+
# for aws storage
gem 'unf', '~> 0.1.4'
@@ -97,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
-gem 'gitlab-markup', '~> 1.5.0'
+gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index b042e4b1b09..cb71a468aa1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -58,6 +58,10 @@ GEM
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
+ autoparse (0.3.3)
+ addressable (>= 2.3.1)
+ extlib (>= 0.9.15)
+ multi_json (>= 1.0.0)
autoprefixer-rails (6.2.3)
execjs
json
@@ -179,6 +183,7 @@ GEM
excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
+ extlib (0.9.16)
factory_girl (4.7.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.7.0)
@@ -208,6 +213,10 @@ GEM
builder
excon (~> 0.49)
formatador (~> 0.2)
+ fog-google (0.5.0)
+ fog-core
+ fog-json
+ fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -254,7 +263,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab-markup (1.5.0)
+ gitlab-markup (1.5.1)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
@@ -279,6 +288,25 @@ GEM
json
multi_json
request_store (>= 1.0)
+ google-api-client (0.8.7)
+ activesupport (>= 3.2, < 5.0)
+ addressable (~> 2.3)
+ autoparse (~> 0.3)
+ extlib (~> 0.9)
+ faraday (~> 0.9)
+ googleauth (~> 0.3)
+ launchy (~> 2.4)
+ multi_json (~> 1.10)
+ retriable (~> 1.4)
+ signet (~> 0.6)
+ googleauth (0.5.1)
+ faraday (~> 0.9)
+ jwt (~> 1.4)
+ logging (~> 2.0)
+ memoist (~> 0.12)
+ multi_json (~> 1.11)
+ os (~> 0.9)
+ signet (~> 0.7)
grape (0.18.0)
activesupport
builder
@@ -381,11 +409,16 @@ GEM
listen (3.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
+ little-plugger (1.1.4)
+ logging (2.1.0)
+ little-plugger (~> 1.1)
+ multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.9.0)
+ memoist (0.15.0)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
@@ -473,6 +506,7 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
+ os (0.9.6)
paranoia (2.2.0)
activerecord (>= 4.0, < 5.1)
parser (2.3.1.4)
@@ -584,6 +618,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
+ retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
@@ -675,6 +710,11 @@ GEM
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
+ signet (0.7.3)
+ addressable (~> 2.3)
+ faraday (~> 0.9)
+ jwt (~> 1.5)
+ multi_json (~> 1.10)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
@@ -841,6 +881,7 @@ DEPENDENCIES
flay (~> 2.6.1)
fog-aws (~> 0.9)
fog-core (~> 1.40)
+ fog-google (~> 0.5)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
@@ -851,11 +892,12 @@ DEPENDENCIES
gemojione (~> 3.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab-markup (~> 1.5.0)
+ gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
+ google-api-client (~> 0.8.6)
grape (~> 0.18.0)
grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2)
diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6
new file mode 100644
index 00000000000..56ffaa765a8
--- /dev/null
+++ b/app/assets/javascripts/ci_lint_editor.js.es6
@@ -0,0 +1,18 @@
+(() => {
+ window.gl = window.gl || {};
+
+ class CILintEditor {
+ constructor() {
+ this.editor = window.ace.edit('ci-editor');
+ this.textarea = document.querySelector('#content');
+
+ this.editor.getSession().setMode('ace/mode/yaml');
+ this.editor.on('input', () => {
+ const content = this.editor.getSession().getValue();
+ this.textarea.value = content;
+ });
+ }
+ }
+
+ gl.CILintEditor = CILintEditor;
+})();
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 1c1b6cd2dad..54f13e328bd 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -184,11 +184,6 @@
new TreeView();
}
break;
- case 'projects:pipelines:index':
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- });
- break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
@@ -215,7 +210,9 @@
new gl.Members();
new UsersSelect();
break;
- case 'projects:project_members:index':
+ case 'projects:members:show':
+ new gl.MemberExpirationDate('.js-access-expiration-date-groups');
+ new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
new UsersSelect();
@@ -261,10 +258,6 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
- case 'projects:group_links:index':
- new gl.MemberExpirationDate();
- new GroupsSelect();
- break;
case 'search:show':
new Search();
break;
@@ -275,6 +268,10 @@
case 'projects:variables:index':
new gl.ProjectVariables();
break;
+ case 'ci:lints:create':
+ case 'ci:lints:show':
+ new gl.CILintEditor();
+ break;
}
switch (path.first()) {
case 'admin':
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 6b7fb9215d1..8b6fafb6104 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -216,7 +216,7 @@
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</th>
<th class="environments-commit">Commit</th>
- <th class="environments-date"></th>
+ <th class="environments-date">Created</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 31a71379af3..b8d637a9827 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -139,6 +139,21 @@
}, 200);
};
+ /**
+ this will take in the `name` of the param you want to parse in the url
+ if the name does not exist this function will return `null`
+ otherwise it will return the value of the param key provided
+ */
+ w.gl.utils.getParameterByName = (name) => {
+ const url = window.location.href;
+ name = name.replace(/[[\]]/g, '\\$&');
+ const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
+ const results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+ };
+
})(window);
}).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js.es6
index 7741cd29793..bf6c0ec2798 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -1,30 +1,29 @@
-/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
-(function() {
+(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
- gl.MemberExpirationDate = function() {
+ window.gl = window.gl || {};
+ gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
-
- var inputs = $('.js-access-expiration-date');
+ const inputs = $(selector);
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
- onSelect: function () {
+ onSelect: function onSelect() {
$(this).trigger('change');
toggleClearInput.call(this);
- }
+ },
});
- inputs.next('.js-clear-input').on('click', function(event) {
+ inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
- var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ const input = $(this).closest('.clearable-input').find(selector);
input.datepicker('setDate', null)
.trigger('change');
toggleClearInput.call(input);
diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6
new file mode 100644
index 00000000000..605824fa939
--- /dev/null
+++ b/app/assets/javascripts/vue_pagination/index.js.es6
@@ -0,0 +1,148 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign, no-plusplus */
+
+((gl) => {
+ const PAGINATION_UI_BUTTON_LIMIT = 4;
+ const UI_LIMIT = 6;
+ const SPREAD = '...';
+ const PREV = 'Prev';
+ const NEXT = 'Next';
+ const FIRST = '<< First';
+ const LAST = 'Last >>';
+
+ gl.VueGlPagination = Vue.extend({
+ props: {
+
+ /**
+ This function will take the information given by the pagination component
+ And make a new Turbolinks call
+
+ Here is an example `change` method:
+
+ change(pagenum, apiScope) {
+ Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
+ },
+ */
+
+ change: {
+ type: Function,
+ required: true,
+ },
+
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ changePage(e) {
+ let apiScope = gl.utils.getParameterByName('scope');
+
+ if (!apiScope) apiScope = 'all';
+
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages, apiScope);
+ break;
+ case NEXT:
+ this.change(nextPage, apiScope);
+ break;
+ case PREV:
+ this.change(previousPage, apiScope);
+ break;
+ case FIRST:
+ this.change(1, apiScope);
+ break;
+ default:
+ this.change(+text, apiScope);
+ break;
+ }
+ },
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
+
+ if (page > 1) items.push({ title: FIRST });
+
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
+
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+
+ for (let i = start; i <= end; i++) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
+
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
+
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
+
+ if (total - page >= 1) items.push({ title: LAST, last: true });
+
+ return items;
+ },
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
new file mode 100644
index 00000000000..9dfbedd73ab
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6
@@ -0,0 +1,41 @@
+/* global Vue, VueResource, gl */
+/*= require vue_common_component/commit */
+/*= require vue-resource
+/*= require boards/vue_resource_interceptor */
+/*= require ./status.js.es6 */
+/*= require ./store.js.es6 */
+/*= require ./pipeline_url.js.es6 */
+/*= require ./stage.js.es6 */
+/*= require ./stages.js.es6 */
+/*= require ./pipeline_actions.js.es6 */
+/*= require ./time_ago.js.es6 */
+/*= require ./pipelines.js.es6 */
+
+(() => {
+ const project = document.querySelector('.pipelines');
+ const entry = document.querySelector('.vue-pipelines-index');
+ const svgs = document.querySelector('.pipeline-svgs');
+
+ Vue.use(VueResource);
+
+ if (!entry) return null;
+ return new Vue({
+ el: entry,
+ data: {
+ scope: project.dataset.url,
+ store: new gl.PipelineStore(),
+ svgs: svgs.dataset,
+ },
+ components: {
+ 'vue-pipelines': gl.VuePipelines,
+ },
+ template: `
+ <vue-pipelines
+ :scope='scope'
+ :store='store'
+ :svgs='svgs'
+ >
+ </vue-pipelines>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
new file mode 100644
index 00000000000..ad5cb30cc42
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -0,0 +1,99 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelineActions = Vue.extend({
+ props: ['pipeline', 'svgs'],
+ computed: {
+ actions() {
+ return this.pipeline.details.manual_actions.length > 0;
+ },
+ artifacts() {
+ return this.pipeline.details.artifacts.length > 0;
+ },
+ },
+ methods: {
+ download(name) {
+ return `Download ${name} artifacts`;
+ },
+ },
+ template: `
+ <td class="pipeline-actions hidden-xs">
+ <div class="controls pull-right">
+ <div class="btn-group inline">
+ <div class="btn-group">
+ <a
+ v-if='actions'
+ class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
+ data-toggle="dropdown"
+ title="Manual build"
+ alt="Manual Build"
+ >
+ <span v-html='svgs.iconPlay'></span>
+ <i class="fa fa-caret-down"></i>
+ </a>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for='action in pipeline.details.manual_actions'>
+ <a
+ rel="nofollow"
+ data-method="post"
+ :href='action.path'
+ title="Manual build"
+ >
+ <span v-html='svgs.iconPlay'></span>
+ <span title="Manual build">{{action.name}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div class="btn-group">
+ <a
+ v-if='artifacts'
+ class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ data-toggle="dropdown"
+ type="button"
+ >
+ <i class="fa fa-download"></i>
+ <i class="fa fa-caret-down"></i>
+ </a>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for='artifact in pipeline.details.artifacts'>
+ <a
+ rel="nofollow"
+ :href='artifact.path'
+ >
+ <i class="fa fa-download"></i>
+ <span>{{download(artifact.name)}}</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="cancel-retry-btns inline">
+ <a
+ v-if='pipeline.flags.retryable'
+ class="btn has-tooltip"
+ title="Retry"
+ rel="nofollow"
+ data-method="post"
+ :href='pipeline.retry_path'
+ >
+ <i class="fa fa-repeat"></i>
+ </a>
+ <a
+ v-if='pipeline.flags.cancelable'
+ class="btn btn-remove has-tooltip"
+ title="Cancel"
+ rel="nofollow"
+ data-method="post"
+ :href='pipeline.cancel_path'
+ data-original-title="Cancel"
+ >
+ <i class="fa fa-remove"></i>
+ </a>
+ </div>
+ </div>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
new file mode 100644
index 00000000000..ae5649f0519
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6
@@ -0,0 +1,63 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelineUrl = Vue.extend({
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a :href='pipeline.path'>
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ v-if='user'
+ :href='pipeline.user.web_url'
+ >
+ <img
+ v-if='user'
+ class="avatar has-tooltip s20 "
+ :title='pipeline.user.name'
+ data-container="body"
+ :src='pipeline.user.avatar_url'
+ >
+ </a>
+ <span
+ v-if='!user'
+ class="api monospace"
+ >
+ API
+ </span>
+ <span
+ v-if='pipeline.flags.latest'
+ class="label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch"
+ >
+ latest
+ </span>
+ <span
+ v-if='pipeline.flags.yaml_errors'
+ class="label label-danger has-tooltip"
+ :title='pipeline.yaml_errors'
+ :data-original-title='pipeline.yaml_errors'
+ >
+ yaml invalid
+ </span>
+ <span
+ v-if='pipeline.flags.stuck'
+ class="label label-warning"
+ >
+ stuck
+ </span>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
new file mode 100644
index 00000000000..73627e9ba50
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -0,0 +1,131 @@
+/* global Vue, Turbolinks, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VuePipelines = Vue.extend({
+ components: {
+ runningPipeline: gl.VueRunningPipeline,
+ pipelineActions: gl.VuePipelineActions,
+ stages: gl.VueStages,
+ commit: gl.CommitComponent,
+ pipelineUrl: gl.VuePipelineUrl,
+ pipelineHead: gl.VuePipelineHead,
+ glPagination: gl.VueGlPagination,
+ statusScope: gl.VueStatusScope,
+ timeAgo: gl.VueTimeAgo,
+ },
+ data() {
+ return {
+ pipelines: [],
+ timeLoopInterval: '',
+ intervalId: '',
+ apiScope: 'all',
+ pageInfo: {},
+ pagenum: 1,
+ count: { all: 0, running_or_pending: 0 },
+ pageRequest: false,
+ };
+ },
+ props: ['scope', 'store', 'svgs'],
+ created() {
+ const pagenum = gl.utils.getParameterByName('p');
+ const scope = gl.utils.getParameterByName('scope');
+ if (pagenum) this.pagenum = pagenum;
+ if (scope) this.apiScope = scope;
+ this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
+ },
+ methods: {
+ change(pagenum, apiScope) {
+ Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
+ },
+ author(pipeline) {
+ if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
+ if (pipeline.commit.author) return pipeline.commit.author;
+ return {
+ avatar_url: pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${pipeline.commit.author_email}`,
+ username: pipeline.commit.author_name,
+ };
+ },
+ ref(pipeline) {
+ const { ref } = pipeline;
+ return { name: ref.name, tag: ref.tag, ref_url: ref.path };
+ },
+ commitTitle(pipeline) {
+ return pipeline.commit ? pipeline.commit.title : '';
+ },
+ commitSha(pipeline) {
+ return pipeline.commit ? pipeline.commit.short_id : '';
+ },
+ commitUrl(pipeline) {
+ return pipeline.commit ? pipeline.commit.commit_path : '';
+ },
+ match(string) {
+ return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
+ },
+ },
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ <div class="table-holder" v-if='pipelines.length'>
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th>Status</th>
+ <th>Pipeline</th>
+ <th>Commit</th>
+ <th>Stages</th>
+ <th></th>
+ <th class="hidden-xs"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="commit" v-for='pipeline in pipelines'>
+ <status-scope
+ :pipeline='pipeline'
+ :match='match'
+ :svgs='svgs'
+ >
+ </status-scope>
+ <pipeline-url :pipeline='pipeline'></pipeline-url>
+ <td>
+ <commit
+ :commit-icon-svg='svgs.commitIconSvg'
+ :author='author(pipeline)'
+ :tag="pipeline.ref.tag"
+ :title='commitTitle(pipeline)'
+ :commit-ref='ref(pipeline)'
+ :short-sha='commitSha(pipeline)'
+ :commit-url='commitUrl(pipeline)'
+ >
+ </commit>
+ </td>
+ <stages
+ :pipeline='pipeline'
+ :svgs='svgs'
+ :match='match'
+ >
+ </stages>
+ <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
+ <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="pipelines realtime-loading" v-if='pageRequest'>
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ <gl-pagination
+ v-if='pageInfo.total > pageInfo.perPage'
+ :pagenum='pagenum'
+ :change='change'
+ :count='count.all'
+ :pageInfo='pageInfo'
+ >
+ </gl-pagination>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
new file mode 100644
index 00000000000..74a79dcedae
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -0,0 +1,76 @@
+/* global Vue, Flash, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStage = Vue.extend({
+ data() {
+ return {
+ request: false,
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ };
+ },
+ props: ['stage', 'svgs', 'match'],
+ methods: {
+ fetchBuilds() {
+ if (this.request) return this.clearBuilds();
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.request = true;
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ this.request = false;
+ return flash;
+ });
+ },
+ clearBuilds() {
+ this.builds = '';
+ this.request = false;
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.request ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.request) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ svg() {
+ const icon = this.stage.status.icon;
+ const stageIcon = icon.replace(/icon/i, 'stage_icon');
+ return this.svgs[this.match(stageIcon)];
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click='fetchBuilds'
+ @blur='fetchBuilds'
+ :class="triggerButtonClass"
+ :title='stage.title'
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button">
+ <span v-html="svg"></span>
+ <i class="fa fa-caret-down "></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up"></div>
+ <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div>
+ </ul>
+ </div>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
new file mode 100644
index 00000000000..cb176b3f0c6
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
@@ -0,0 +1,21 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStages = Vue.extend({
+ components: {
+ 'vue-stage': gl.VueStage,
+ },
+ props: ['pipeline', 'svgs', 'match'],
+ template: `
+ <td class="stage-cell">
+ <div
+ class="stage-container dropdown js-mini-pipeline-graph"
+ v-for='stage in pipeline.details.stages'
+ >
+ <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
+ </div>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6
new file mode 100644
index 00000000000..05175082704
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6
@@ -0,0 +1,34 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueStatusScope = Vue.extend({
+ props: [
+ 'pipeline', 'svgs', 'match',
+ ],
+ computed: {
+ cssClasses() {
+ const cssObject = { 'ci-status': true };
+ cssObject[`ci-${this.pipeline.details.status.group}`] = true;
+ return cssObject;
+ },
+ svg() {
+ return this.svgs[this.match(this.pipeline.details.status.icon)];
+ },
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class='cssClasses'
+ :href='detailsPath'
+ v-html='svg + pipeline.details.status.text'
+ >
+ </a>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
new file mode 100644
index 00000000000..6b34839b030
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -0,0 +1,59 @@
+/* global gl, Flash */
+/* eslint-disable no-param-reassign, no-underscore-dangle */
+/*= require vue_realtime_listener/index.js */
+
+((gl) => {
+ const pageValues = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+
+ gl.PipelineStore = class {
+ fetchDataLoop(Vue, pageNum, url, apiScope) {
+ const updatePipelineNums = (count) => {
+ const { all } = count;
+ const running = count.running_or_pending;
+ document.querySelector('.js-totalbuilds-count').innerHTML = all;
+ document.querySelector('.js-running-count').innerHTML = running;
+ };
+
+ const goFetch = () =>
+ this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
+ .then((response) => {
+ const pageInfo = pageValues(response.headers);
+ this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
+
+ const res = JSON.parse(response.body);
+ this.count = Object.assign({}, this.count, res.count);
+ this.pipelines = Object.assign([], this.pipelines, res.pipelines);
+
+ updatePipelineNums(this.count);
+ this.pageRequest = false;
+ }, () => {
+ this.pageRequest = false;
+ return new Flash('Something went wrong on our end.');
+ });
+
+ goFetch();
+
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(() => {
+ this.$children
+ .filter(e => e.$options._componentTag === 'time-ago')
+ .forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
new file mode 100644
index 00000000000..655110feba1
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -0,0 +1,73 @@
+/* global Vue, gl */
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueTimeAgo = Vue.extend({
+ data() {
+ return {
+ currentTime: new Date(),
+ };
+ },
+ props: ['pipeline', 'svgs'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td>
+ <p class="duration" v-if='duration'>
+ <span v-html='svgs.iconTimer'></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'
+ >
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+ });
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6
new file mode 100644
index 00000000000..23cac1466d2
--- /dev/null
+++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6
@@ -0,0 +1,18 @@
+/* eslint-disable no-param-reassign */
+
+((gl) => {
+ gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
+ const removeAll = () => {
+ removeIntervals();
+ window.removeEventListener('beforeunload', removeIntervals);
+ window.removeEventListener('focus', startIntervals);
+ window.removeEventListener('blur', removeIntervals);
+ document.removeEventListener('page:fetch', removeAll);
+ };
+
+ window.addEventListener('beforeunload', removeIntervals);
+ window.addEventListener('focus', startIntervals);
+ window.addEventListener('blur', removeIntervals);
+ document.addEventListener('page:fetch', removeAll);
+ };
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 771dfaec46e..1c6698ad0c6 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -163,6 +163,10 @@ ul.content-list {
&:last-child {
margin-right: 0;
+
+ @media(max-width: $screen-xs-max) {
+ margin: 0 auto;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
index a7c80dce424..68b6c5ecbd4 100644
--- a/app/assets/stylesheets/pages/lint.scss
+++ b/app/assets/stylesheets/pages/lint.scss
@@ -9,3 +9,13 @@
color: $lint-correct-color;
}
}
+
+.ci-linter {
+ .ci-editor {
+ height: 400px;
+ }
+
+ .ci-template pre {
+ white-space: pre-wrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 36ee5d17211..be7193bae04 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -25,7 +25,7 @@
}
.form-horizontal {
- margin-top: 5px;
+ margin-top: 20px;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
@@ -98,6 +98,10 @@
padding-right: 35px;
@media (min-width: $screen-sm-min) {
+ width: 250px;
+ }
+
+ @media (min-width: $screen-md-min) {
width: 350px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ed53ad94021..8861315d776 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,4 +1,9 @@
.pipelines {
+ .realtime-loading {
+ font-size: 40px;
+ text-align: center;
+ }
+
.stage {
max-width: 90px;
width: 90px;
@@ -24,6 +29,10 @@
min-width: 1200px;
table-layout: fixed;
+ .label {
+ margin-bottom: 3px;
+ }
+
.pipeline-id {
color: $black;
}
@@ -177,6 +186,7 @@
.stage-cell {
font-size: 0;
+ > .stage-container > div > button > span > svg,
> .stage-container > button > svg {
height: 22px;
width: 22px;
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 9eaf26a0dbf..66b7bdbd988 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
before_action :authorize_admin_project_member!, only: [:update]
def index
- @group_links = project.project_group_links.all
-
- @skip_groups = @group_links.pluck(:group_id)
- @skip_groups << project.namespace_id unless project.personal?
+ redirect_to namespace_project_settings_members_path
end
def create
@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.'
end
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ redirect_to namespace_project_settings_members_path(project.namespace, project)
end
def update
@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to namespace_project_group_links_path(project.namespace, project)
+ redirect_to namespace_project_settings_members_path(project.namespace, project)
end
format.js { head :ok }
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index cc347922c6a..84451257b98 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
- @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
- @pipelines = @pipelines.includes(project: :namespace)
+ @pipelines = PipelinesFinder
+ .new(project)
+ .execute(scope: @scope)
+ .page(params[:page])
+ .per(30)
- @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
- @pipelines_count = PipelinesFinder.new(project).execute.count
+ @running_or_pending_count = PipelinesFinder
+ .new(project).execute(scope: 'running').count
+
+ @pipelines_count = PipelinesFinder
+ .new(project).execute.count
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ pipelines: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .represent(@pipelines),
+ count: {
+ all: @pipelines_count,
+ running_or_pending: @running_or_pending_count
+ }
+ }
+ end
+ end
end
def new
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3aec6f18e27..6e158e685e9 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
- @sort = params[:sort].presence || sort_value_name
- @group_links = @project.project_group_links
-
- @project_members = @project.project_members
- @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
-
- group = @project.group
-
- if group
- # We need `.where.not(user_id: nil)` here otherwise when a group has an
- # invitee, it would make the following query return 0 rows since a NULL
- # user_id would be present in the subquery
- # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
- # FIXME: This whole logic should be moved to a finder!
- non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
- group_members = group.group_members.where.not(user_id: non_null_user_ids)
- group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
- end
-
- if params[:search].present?
- user_ids = @project.users.search(params[:search]).select(:id)
- @project_members = @project_members.where(user_id: user_ids)
-
- if group_members
- user_ids = group.users.search(params[:search]).select(:id)
- group_members = group_members.where(user_id: user_ids)
- end
-
- @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
- end
-
- wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
- wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
-
- @project_members = Member.
- where(wheres.join(' OR ')).
- sort(@sort).
- page(params[:page])
-
- @requesters = AccessRequestsFinder.new(@project).execute(current_user)
-
- @project_member = @project.project_members.new
+ sort = params[:sort].presence || sort_value_name
+ redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
def create
status = Members::CreateService.new(@project, current_user, params).execute
- redirect_url = namespace_project_project_members_path(@project.namespace, @project)
+ redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
@@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to namespace_project_project_members_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end
format.js { head :ok }
end
end
def resend_invite
- redirect_path = namespace_project_project_members_path(@project.namespace, @project)
+ redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
@project_member = @project.project_members.find(params[:id])
@@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
- redirect_to(namespace_project_project_members_path(project.namespace, project),
+ redirect_to(namespace_project_settings_members_path(project.namespace, project),
notice: notice)
end
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
new file mode 100644
index 00000000000..5735e281f66
--- /dev/null
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -0,0 +1,55 @@
+module Projects
+ module Settings
+ class MembersController < Projects::ApplicationController
+ include SortingHelper
+
+ def show
+ @sort = params[:sort].presence || sort_value_name
+ @group_links = @project.project_group_links
+
+ @project_members = @project.project_members
+ @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+
+ group = @project.group
+
+ # group links
+ @group_links = @project.project_group_links.all
+
+ @skip_groups = @group_links.pluck(:group_id)
+ @skip_groups << @project.namespace_id unless @project.personal?
+
+ if group
+ # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # invitee, it would make the following query return 0 rows since a NULL
+ # user_id would be present in the subquery
+ # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
+ group_members = MembersFinder.new(@project_members, group).execute(current_user)
+ end
+
+ if params[:search].present?
+ user_ids = @project.users.search(params[:search]).select(:id)
+ @project_members = @project_members.where(user_id: user_ids)
+
+ if group_members
+ user_ids = group.users.search(params[:search]).select(:id)
+ group_members = group_members.where(user_id: user_ids)
+ end
+
+ @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
+ end
+
+ wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
+
+ @project_members = Member.
+ where(wheres.join(' OR ')).
+ sort(@sort).
+ page(params[:page])
+
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
+
+ @project_member = @project.project_members.new
+ end
+ end
+ end
+end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
new file mode 100644
index 00000000000..702944404f5
--- /dev/null
+++ b/app/finders/members_finder.rb
@@ -0,0 +1,13 @@
+class MembersFinder < Projects::ApplicationController
+ def initialize(project_members, project_group)
+ @project_members = project_members
+ @project_group = project_group
+ end
+
+ def execute(current_user)
+ non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
+ group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
+ group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
+ group_members
+ end
+end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 99db73c9ee0..5742fec4458 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -206,4 +206,9 @@ module GitlabRoutingHelper
file_namespace_project_build_artifacts_path(*args)
end
end
+
+ # Settings
+ def project_settings_members_path(project, *args)
+ namespace_project_settings_members_path(project.namespace, project, *args)
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 6645f13346d..6654f6997ce 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -75,7 +75,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index abbbddaa4f6..2a97e8bae4a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -142,7 +142,7 @@ module Ci
end
def artifacts
- builds.latest.with_artifacts_not_expired
+ builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
def project_id
@@ -191,7 +191,11 @@ module Ci
end
def manual_actions
- builds.latest.manual_actions
+ builds.latest.manual_actions.includes(project: [:namespace])
+ end
+
+ def stuck?
+ builds.pending.any?(&:stuck?)
end
def retryable?
@@ -283,6 +287,10 @@ module Ci
end
end
+ def has_yaml_errors?
+ yaml_errors.present?
+ end
+
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 5cde94b3509..652abf18a8a 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base
end
def update_merge_request_metrics?
- self.name == "production"
+ (environment_type || name) == "production"
end
def first_deployment_for(commit)
diff --git a/app/models/key.rb b/app/models/key.rb
index 6f377f0e8ae..8be29c697f1 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -49,6 +49,10 @@ class Key < ActiveRecord::Base
"key-#{id}"
end
+ def update_last_used_at
+ UseKeyWorker.perform_async(self.id)
+ end
+
def add_to_shell
GitlabShellWorker.perform_async(
:add_key,
diff --git a/app/models/label.rb b/app/models/label.rb
index 5c01c15e5af..5b6b9a7a736 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base
# Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] }
+ validates :title, length: { maximum: 255 }
default_scope { order(title: :asc) }
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 43fc218de2b..58f6214bea7 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
]
+ EXCLUDED_WATCHER_EVENTS = [
+ :success_pipeline
+ ]
+
store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events
diff --git a/app/models/project.rb b/app/models/project.rb
index ec40def6fb1..94a6f3ba799 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -130,7 +130,7 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
- has_many :project_authorizations, dependent: :destroy
+ has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
diff --git a/app/models/user.rb b/app/models/user.rb
index 66a768d54bb..06dd98a3188 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -73,7 +73,7 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
- has_many :project_authorizations, dependent: :destroy
+ has_many :project_authorizations
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
@@ -444,7 +444,7 @@ class User < ActiveRecord::Base
end
def remove_project_authorizations(project_ids)
- project_authorizations.where(id: project_ids).delete_all
+ project_authorizations.where(project_id: project_ids).delete_all
end
def set_authorized_projects_column
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
new file mode 100644
index 00000000000..3e72892d584
--- /dev/null
+++ b/app/serializers/build_action_entity.rb
@@ -0,0 +1,14 @@
+class BuildActionEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |build|
+ build.name.humanize
+ end
+
+ expose :path do |build|
+ play_namespace_project_build_path(
+ build.project.namespace,
+ build.project,
+ build)
+ end
+end
diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb
new file mode 100644
index 00000000000..8b643d8e783
--- /dev/null
+++ b/app/serializers/build_artifact_entity.rb
@@ -0,0 +1,14 @@
+class BuildArtifactEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name do |build|
+ build.name
+ end
+
+ expose :path do |build|
+ download_namespace_project_build_artifacts_path(
+ build.project.namespace,
+ build.project,
+ build)
+ end
+end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index acc20f6dc52..49f4db36295 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -3,6 +3,10 @@ class CommitEntity < API::Entities::RepoCommit
expose :author, using: UserEntity
+ expose :author_gravatar_url do |commit|
+ GravatarService.new.execute(commit.author_email)
+ end
+
expose :commit_url do |commit|
namespace_project_tree_url(
request.project.namespace,
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
new file mode 100644
index 00000000000..d04a4990cb0
--- /dev/null
+++ b/app/serializers/pipeline_entity.rb
@@ -0,0 +1,83 @@
+class PipelineEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :user, using: UserEntity
+
+ expose :path do |pipeline|
+ namespace_project_pipeline_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ pipeline)
+ end
+
+ expose :details do
+ expose :status do |pipeline, options|
+ StatusEntity.represent(
+ pipeline.detailed_status(request.user),
+ options)
+ end
+
+ expose :duration
+ expose :finished_at
+ expose :stages, using: StageEntity
+ expose :artifacts, using: BuildArtifactEntity
+ expose :manual_actions, using: BuildActionEntity
+ end
+
+ expose :flags do
+ expose :latest?, as: :latest
+ expose :triggered?, as: :triggered
+ expose :stuck?, as: :stuck
+ expose :has_yaml_errors?, as: :yaml_errors
+ expose :can_retry?, as: :retryable
+ expose :can_cancel?, as: :cancelable
+ end
+
+ expose :ref do
+ expose :name do |pipeline|
+ pipeline.ref
+ end
+
+ expose :path do |pipeline|
+ namespace_project_tree_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ id: pipeline.ref)
+ end
+
+ expose :tag?, as: :tag
+ expose :branch?, as: :branch
+ end
+
+ expose :commit, using: CommitEntity
+ expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? }
+
+ expose :retry_path, if: proc { can_retry? } do |pipeline|
+ retry_namespace_project_pipeline_path(pipeline.project.namespace,
+ pipeline.project,
+ pipeline.id)
+ end
+
+ expose :cancel_path, if: proc { can_cancel? } do |pipeline|
+ cancel_namespace_project_pipeline_path(pipeline.project.namespace,
+ pipeline.project,
+ pipeline.id)
+ end
+
+ expose :created_at, :updated_at
+
+ private
+
+ alias_method :pipeline, :object
+
+ def can_retry?
+ pipeline.retryable? &&
+ can?(request.user, :update_pipeline, pipeline)
+ end
+
+ def can_cancel?
+ pipeline.cancelable? &&
+ can?(request.user, :update_pipeline, pipeline)
+ end
+end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
new file mode 100644
index 00000000000..cfa86cc2553
--- /dev/null
+++ b/app/serializers/pipeline_serializer.rb
@@ -0,0 +1,40 @@
+class PipelineSerializer < BaseSerializer
+ entity PipelineEntity
+ class InvalidResourceError < StandardError; end
+ include API::Helpers::Pagination
+ Struct.new('Pagination', :request, :response)
+
+ def represent(resource, opts = {})
+ if paginated?
+ raise InvalidResourceError unless resource.respond_to?(:page)
+
+ super(paginate(resource.includes(project: :namespace)), opts)
+ else
+ super(resource, opts)
+ end
+ end
+
+ def paginated?
+ defined?(@pagination)
+ end
+
+ def with_pagination(request, response)
+ tap { @pagination = Struct::Pagination.new(request, response) }
+ end
+
+ private
+
+ # Methods needed by `API::Helpers::Pagination`
+ #
+ def params
+ @pagination.request.query_parameters
+ end
+
+ def request
+ @pagination.request
+ end
+
+ def header(header, value)
+ @pagination.response.headers[header] = value
+ end
+end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
index e159d750cb7..3039014aaaa 100644
--- a/app/serializers/request_aware_entity.rb
+++ b/app/serializers/request_aware_entity.rb
@@ -2,14 +2,11 @@ module RequestAwareEntity
extend ActiveSupport::Concern
included do
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
+ include Gitlab::Allowable
end
def request
- @options.fetch(:request)
- end
-
- def can?(object, action, subject)
- Ability.allowed?(object, action, subject)
+ options.fetch(:request)
end
end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
new file mode 100644
index 00000000000..7a047bdc712
--- /dev/null
+++ b/app/serializers/stage_entity.rb
@@ -0,0 +1,38 @@
+class StageEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :name
+
+ expose :title do |stage|
+ "#{stage.name}: #{detailed_status.label}"
+ end
+
+ expose :detailed_status,
+ as: :status,
+ with: StatusEntity
+
+ expose :path do |stage|
+ namespace_project_pipeline_path(
+ stage.pipeline.project.namespace,
+ stage.pipeline.project,
+ stage.pipeline,
+ anchor: stage.name)
+ end
+
+ expose :dropdown_path do |stage|
+ stage_namespace_project_pipeline_path(
+ stage.pipeline.project.namespace,
+ stage.pipeline.project,
+ stage.pipeline,
+ stage: stage.name,
+ format: :json)
+ end
+
+ private
+
+ alias_method :stage, :object
+
+ def detailed_status
+ stage.detailed_status(request.user)
+ end
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
new file mode 100644
index 00000000000..47066bebfb1
--- /dev/null
+++ b/app/serializers/status_entity.rb
@@ -0,0 +1,8 @@
+class StatusEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :icon, :text, :label, :group
+
+ expose :has_details?, as: :has_details
+ expose :details_path
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 9a7af5730d2..c3b61e68eab 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -591,7 +591,10 @@ class NotificationService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
- recipients = add_project_watchers(recipients, project)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients, project)
+ end
recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 6040391fd94..96c363c8d1a 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,7 +36,7 @@ module Projects
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url }
+ { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url }
end
end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 8559908e0c3..21ec1bd9e65 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -35,7 +35,7 @@ module Users
# rows not in the new list or with a different access level should be
# removed.
if !fresh[project_id] || fresh[project_id] != row.access_level
- array << row.id
+ array << row.project_id
end
end
@@ -100,7 +100,7 @@ module Users
end
def current_authorizations
- user.project_authorizations.select(:id, :project_id, :access_level)
+ user.project_authorizations.select(:project_id, :access_level)
end
def fresh_authorizations
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 889086c62b1..95eb9a57152 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,20 +1,25 @@
- page_title "CI Lint"
- page_description "Validate your GitLab CI configuration file"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
%h2 Check your .gitlab-ci.yml
-%hr
-.row
- = form_tag ci_lint_path, method: :post do
- .form-group
- = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
+.ci-linter
+ .row
+ = form_tag ci_lint_path, method: :post do
+ .form-group
+ .col-sm-12
+ .file-holder
+ .file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor #{@content}
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
- = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
- .col-sm-12
- .pull-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ .pull-left.prepend-top-10
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
-.row.prepend-top-20
- .col-sm-12
- .results
- = render partial: 'create' if defined?(@status)
+ .row.prepend-top-20
+ .col-sm-12
+ .results.ci-template
+ = render partial: 'create' if defined?(@status)
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index d19eaa6add9..38d63fd9acc 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -21,5 +21,5 @@
= render 'shared/group_tips'
.form-actions
- = f.submit 'Create group', class: "btn btn-create", tabindex: 3
+ = f.submit 'Create group', class: "btn btn-create"
= link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 613b8b7d301..0fb2bb460cb 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,14 +1,9 @@
- if project_nav_tab? :team
- = nav_link(controller: [:project_members, :teams]) do
- = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
+ = nav_link(controller: [:members, :teams]) do
+ = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if can_edit
- - if @project.allowed_to_share_with_group?
- = nav_link(controller: :group_links) do
- = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
- %span
- Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 3276db6692c..d2a60ac2867 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -6,6 +6,9 @@
= key.title
.description
= key.fingerprint
+ .last-used-at
+ last used:
+ = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index dd7615400dc..d44603c638c 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -11,6 +11,9 @@
%li
%span.light Created on:
%strong= @key.created_at.to_s(:medium)
+ %li
+ %span.light Last used on:
+ %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8
%p
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index ecd812312c0..5f8f56150f9 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -5,7 +5,8 @@
%div{ class: container_class }
.top-area.adjust
.nav-text
- Protected branches can be managed in project settings
+ Protected branches can be managed in
+ = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project)
.nav-controls
= form_tag(filter_branches_path, method: :get) do
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index aaf1b428178..6ce586cc8f6 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -78,9 +78,9 @@
.btn-group.inline
- if actions.any?
.btn-group
- %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
- = icon('caret-down')
+ = icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
@@ -89,7 +89,7 @@
%span= build.name.humanize
- if artifacts.present?
.btn-group
- %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/_index.html.haml
index 1b0dbbb8111..99d0df2ac34 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/_index.html.haml
@@ -20,10 +20,10 @@
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
%i.clear-icon.js-clear-input
.help-block
- On this date, all users in the group will automatically lose access to this project.
+ On this date, all members in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 4bb3d4d35fb..abea6932567 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -35,21 +35,34 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
-
- .content-list.pipelines
+ .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- if @pipelines.blank?
%div
.nothing-here-block No pipelines to show
- else
- .table-holder
- %table.table.ci-table.js-pipeline-table
- %thead
- %th.pipeline-status Status
- %th.pipeline-info Pipeline
- %th.pipeline-commit Commit
- %th.pipeline-stages Stages
- %th.pipeline-date
- %th.pipeline-actions.hidden-xs
- = render @pipelines, commit_sha: true, stage: true, allow_retry: true
-
- = paginate @pipelines, theme: 'gitlab'
+ .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+ } }
+
+ .vue-pipelines-index
+
+= page_specific_javascript_tag('vue_pagination/index.js')
+= page_specific_javascript_tag('vue_pipelines_index/index.js')
diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml
new file mode 100644
index 00000000000..ab0771b5751
--- /dev/null
+++ b/app/views/projects/project_members/_index.html.haml
@@ -0,0 +1,22 @@
+.row.prepend-top-default
+ .col-lg-3.settings-sidebar
+ %h4.prepend-top-0
+ Members
+ - if can?(current_user, :admin_project_member, @project)
+ %p
+ Add a new member to
+ %strong= @project.name
+ .col-lg-9
+ .light.prepend-top-default
+ - if can?(current_user, :admin_project_member, @project)
+ = render "projects/project_members/new_project_member"
+
+ = render 'shared/members/requests', membership_source: @project, requesters: @requesters
+ .append-bottom-default.clearfix
+ %h5.member.existing-title
+ Existing members and groups
+ - if @group_links.any?
+ = render 'projects/project_members/groups', group_links: @group_links
+
+ = render 'projects/project_members/team', members: @project_members
+ = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 79dcd7a6ee9..2b1c23f7dda 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,22 +1,18 @@
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
- .row
- .col-md-4.col-lg-6
- = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
- .help-block.append-bottom-10
- Search for users by name, username, or email, or invite new ones using their email address.
-
- .col-md-3.col-lg-2
- = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
- .help-block.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
- about role permissions
-
- .col-md-3.col-lg-2
- .clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
- %i.clear-icon.js-clear-input
- .help-block.append-bottom-10
- On this date, the user(s) will automatically lose access to this project.
-
- .col-md-2
- = f.submit "Add to project", class: "btn btn-create btn-block"
+ .form-group
+ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
+ .help-block.append-bottom-10
+ Search for members by name, username, or email, or invite new ones using their email address.
+ .form-group
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
+ .help-block.append-bottom-10
+ = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
+ about role permissions
+ .form-group
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block.append-bottom-10
+ On this date, the member(s) will automatically lose access to this project.
+ = f.submit "Add to project", class: "btn btn-create"
+ = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index c1e894d8f40..5292e73be7a 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,7 +1,13 @@
.panel.panel-default
.panel-heading
- Users with access to
+ Members with access to
%strong #{@project.name}
%span.badge= @project_members.total_count
+ = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ = render 'shared/members/sort_dropdown'
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index eef97107d77..42ce4f8001b 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -12,5 +12,4 @@
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
- = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
-
+ = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
deleted file mode 100644
index 4f1cec20f85..00000000000
--- a/app/views/projects/project_members/index.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- page_title "Members"
-
-.project-members-page.prepend-top-default
- %h4.project-members-title.clearfix
- Members
- = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
- - if can?(current_user, :admin_project_member, @project)
- .project-members-new.append-bottom-default
- %p.clearfix
- Add new user to
- %strong= @project.name
- = render "new_project_member"
-
- = render 'shared/members/requests', membership_source: @project, requesters: @requesters
-
- .append-bottom-default.clearfix
- %h5.member.existing-title
- Existing users and groups
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- = render 'shared/members/sort_dropdown'
- - if @group_links.any?
- = render 'groups', group_links: @group_links
-
- = render 'team', members: @project_members
- = paginate @project_members, theme: "gitlab"
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
new file mode 100644
index 00000000000..d81ed7bb609
--- /dev/null
+++ b/app/views/projects/settings/members/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "Members"
+
+= render "projects/project_members/index"
+- if can?(current_user, :admin_project, @project)
+ - if @project.allowed_to_share_with_group?
+ = render "projects/group_links/index"
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 000532b1c9a..ee043910548 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,4 +1,4 @@
-%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button
+%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index a46ba3b0605..81b5bc1de30 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -37,7 +37,6 @@
%i.clear-icon.js-clear-input
- if can_admin_member
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
- remote: true,
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 15ff5b8a27e..c8fd45c4319 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -9,6 +9,7 @@
- if show_counter
.right
= issuables.size
+ .pull-right= number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb
new file mode 100644
index 00000000000..c9d382cc5d6
--- /dev/null
+++ b/app/workers/use_key_worker.rb
@@ -0,0 +1,13 @@
+class UseKeyWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(key_id)
+ key = Key.find(key_id)
+ key.touch(:last_used_at)
+ rescue ActiveRecord::RecordNotFound
+ Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job")
+
+ false
+ end
+end
diff --git a/changelogs/unreleased/19086-double-newline.yml b/changelogs/unreleased/19086-double-newline.yml
new file mode 100644
index 00000000000..dd9b58920fb
--- /dev/null
+++ b/changelogs/unreleased/19086-double-newline.yml
@@ -0,0 +1,4 @@
+---
+title: Fix double spaced CI log
+merge_request: 8349
+author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml
new file mode 100644
index 00000000000..83cf3670ec0
--- /dev/null
+++ b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml
@@ -0,0 +1,4 @@
+---
+title: Treat environments matching `production/*` as Production
+merge_request: 8500
+author:
diff --git a/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml b/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml
new file mode 100644
index 00000000000..0c9853de3b6
--- /dev/null
+++ b/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml
@@ -0,0 +1,4 @@
+---
+title: Added number_with_delimiter to counter on milestone panels
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml b/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml
new file mode 100644
index 00000000000..13d3476fe39
--- /dev/null
+++ b/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml
@@ -0,0 +1,4 @@
+---
+title: Adds label to Environments "Date Created"
+merge_request: 8376
+author: Saad Shahd
diff --git a/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml
new file mode 100644
index 00000000000..206be8fe3cb
--- /dev/null
+++ b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Combined the settings options project members and groups into a single one
+ called members
+merge_request:
+author:
diff --git a/changelogs/unreleased/26014-fix-update-doc.yml b/changelogs/unreleased/26014-fix-update-doc.yml
new file mode 100644
index 00000000000..419c032cb0f
--- /dev/null
+++ b/changelogs/unreleased/26014-fix-update-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Re-order update steps in the 8.14 -> 8.15 upgrade guide
+merge_request:
+author:
diff --git a/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml b/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml
new file mode 100644
index 00000000000..85440eb86f9
--- /dev/null
+++ b/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml
@@ -0,0 +1,4 @@
+---
+title: Don't instrument 405 Grape calls
+merge_request: 8445
+author:
diff --git a/changelogs/unreleased/26129-add-link-to-branches-page.yml b/changelogs/unreleased/26129-add-link-to-branches-page.yml
new file mode 100644
index 00000000000..aceb92dbb9c
--- /dev/null
+++ b/changelogs/unreleased/26129-add-link-to-branches-page.yml
@@ -0,0 +1,4 @@
+---
+title: Convert project setting text into protected branch path link
+merge_request: 8377
+author: Ken Ding
diff --git a/changelogs/unreleased/26238-buttons-not-accessible.yml b/changelogs/unreleased/26238-buttons-not-accessible.yml
new file mode 100644
index 00000000000..34d38d45709
--- /dev/null
+++ b/changelogs/unreleased/26238-buttons-not-accessible.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes buttons not being accessible via the keyboard when creating new group
+merge_request: 8469
+author:
diff --git a/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml b/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml
new file mode 100644
index 00000000000..b4aef8fe3da
--- /dev/null
+++ b/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml
@@ -0,0 +1,4 @@
+---
+title: Make play button on Pipelines page accessible via keyboard
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml b/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml
new file mode 100644
index 00000000000..83f6233dd88
--- /dev/null
+++ b/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml
@@ -0,0 +1,5 @@
+---
+title: Made download artifacts button accessible via keyboard by changing it from
+ an anchor tag to an actual button
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/didemacet-ci-lint-page.yml b/changelogs/unreleased/didemacet-ci-lint-page.yml
new file mode 100644
index 00000000000..07386321c9d
--- /dev/null
+++ b/changelogs/unreleased/didemacet-ci-lint-page.yml
@@ -0,0 +1,4 @@
+---
+title: Change CI template linter textarea with Ace Editor
+merge_request: 8452
+author: Didem Acet
diff --git a/changelogs/unreleased/feature-log-ldap-to-application-log.yml b/changelogs/unreleased/feature-log-ldap-to-application-log.yml
new file mode 100644
index 00000000000..4cfbc23edb7
--- /dev/null
+++ b/changelogs/unreleased/feature-log-ldap-to-application-log.yml
@@ -0,0 +1,4 @@
+---
+title: Log LDAP blocking/unblocking events to application log
+merge_request: 8042
+author: Markus Koller
diff --git a/changelogs/unreleased/fix-broken-url-on-group-avatar.yml b/changelogs/unreleased/fix-broken-url-on-group-avatar.yml
new file mode 100644
index 00000000000..7ce22b4826e
--- /dev/null
+++ b/changelogs/unreleased/fix-broken-url-on-group-avatar.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken url on group avatar
+merge_request: 8464
+author: hogewest
diff --git a/changelogs/unreleased/get_last_used_date_of_ssh_key.yml b/changelogs/unreleased/get_last_used_date_of_ssh_key.yml
new file mode 100644
index 00000000000..b753949922c
--- /dev/null
+++ b/changelogs/unreleased/get_last_used_date_of_ssh_key.yml
@@ -0,0 +1,4 @@
+---
+title: Record and show last used date of SSH Keys
+merge_request: 8113
+author: Vincent Wong
diff --git a/changelogs/unreleased/remove-project-authorizations-id-column.yml b/changelogs/unreleased/remove-project-authorizations-id-column.yml
new file mode 100644
index 00000000000..24c86f0fb1b
--- /dev/null
+++ b/changelogs/unreleased/remove-project-authorizations-id-column.yml
@@ -0,0 +1,4 @@
+---
+title: Remove the project_authorizations.id column
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml b/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml
new file mode 100644
index 00000000000..47d484e5c84
--- /dev/null
+++ b/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml
@@ -0,0 +1,4 @@
+---
+title: Make successful pipeline emails off for watchers
+merge_request: 8176
+author:
diff --git a/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml
new file mode 100644
index 00000000000..8ec3cfdbb08
--- /dev/null
+++ b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml
@@ -0,0 +1,4 @@
+---
+title: Restore backup correctly when "BACKUP" environment variable is passed
+merge_request: 8477
+author:
diff --git a/changelogs/unreleased/support-google-cloud-storage-backups.yml b/changelogs/unreleased/support-google-cloud-storage-backups.yml
new file mode 100644
index 00000000000..cec279a5c73
--- /dev/null
+++ b/changelogs/unreleased/support-google-cloud-storage-backups.yml
@@ -0,0 +1,4 @@
+---
+title: Re-add Google Cloud Storage as a backup strategy
+merge_request:
+author:
diff --git a/changelogs/unreleased/update-gitlab-markup-gem.yml b/changelogs/unreleased/update-gitlab-markup-gem.yml
new file mode 100644
index 00000000000..96cdfd051f0
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-markup-gem.yml
@@ -0,0 +1,4 @@
+---
+title: Update the gitlab-markup gem to the version 1.5.1
+merge_request: 8509
+author:
diff --git a/changelogs/unreleased/validate-title-length.yml b/changelogs/unreleased/validate-title-length.yml
new file mode 100644
index 00000000000..7abf1c4d05a
--- /dev/null
+++ b/changelogs/unreleased/validate-title-length.yml
@@ -0,0 +1,4 @@
+---
+title: "Validate label's title length"
+merge_request: 5767
+author: Tomáš Kukrál
diff --git a/config/application.rb b/config/application.rb
index d36c6d5c92e..1de7fb7bdb8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -109,6 +109,8 @@ module Gitlab
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
+ config.assets.precompile << "vue_pipelines_index/index.js"
+ config.assets.precompile << "vue_pagination/index.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 4d20acbef7a..26e2dc9e6e7 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -307,6 +307,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ namespace :settings do
+ resource :members, only: [:show]
+ end
+
# Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes
draw :wiki
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index c22964179d9..022b0e80917 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -29,6 +29,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
+ - [use_key, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
diff --git a/db/migrate/20161221152132_add_last_used_at_to_key.rb b/db/migrate/20161221152132_add_last_used_at_to_key.rb
new file mode 100644
index 00000000000..fb2b15817de
--- /dev/null
+++ b/db/migrate/20161221152132_add_last_used_at_to_key.rb
@@ -0,0 +1,9 @@
+class AddLastUsedAtToKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :keys, :last_used_at, :datetime
+ end
+end
diff --git a/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb b/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb
new file mode 100644
index 00000000000..7c788160022
--- /dev/null
+++ b/db/post_migrate/20170106172224_remove_project_authorizations_id_column.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveProjectAuthorizationsIdColumn < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :project_authorizations, :id, :primary_key
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 923ece86edb..f3bf7ced393 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161227192806) do
+ActiveRecord::Schema.define(version: 20170106172224) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -528,6 +528,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do
t.string "fingerprint"
t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false
+ t.datetime "last_used_at"
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
@@ -861,7 +862,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
- create_table "project_authorizations", force: :cascade do |t|
+ create_table "project_authorizations", id: false, force: :cascade do |t|
t.integer "user_id"
t.integer "project_id"
t.integer "access_level"
@@ -1306,4 +1307,4 @@ ActiveRecord::Schema.define(version: 20161227192806) do
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
-end \ No newline at end of file
+end
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index b8b63df091e..04723365277 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -298,8 +298,11 @@ LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
-### Login with valid credentials rejected
+### Troubleshooting
-If there is an unexpected error while authenticating the user with the LDAP
-backend, the login is rejected and details about the error are logged to
+If a user account is blocked or unblocked due to the LDAP configuration, a
+message will be logged to `application.log`.
+
+If there is an unexpected error during an LDAP lookup (configuration error,
+timeout), the login is rejected and a message will be logged to
`production.log`.
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 14cd7a03826..00494e7e9d6 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -71,8 +71,8 @@ If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
-[these instructions](./postfix.md).
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
+[Postfix setup documentation](reply_by_email_postfix_setup.md).
### Omnibus package installations
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2740b2982b9..9dba03b1924 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-16-stable gitlab
-**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-16-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -400,16 +400,10 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The
following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse`
which is the recommended location.
- cd /home/git/gitlab
-
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
### Initialize Database and Activate Advanced Features
- # Go to GitLab installation folder
-
- cd /home/git/gitlab
-
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f42bb6a81a2..51f7454610d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -9,6 +9,9 @@ This archive will be saved in `backup_path`, which is specified in the
The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
identifies the time at which each backup was created.
+> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`)
+> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`)
+
You can only restore a backup to exactly the same version of GitLab on which it
was created. The best way to migrate your repositories from one server to
another is through backup restore.
@@ -88,7 +91,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
-for AWS, OpenStack Swift and Rackspace as well. A local driver is
+for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages:
@@ -223,7 +226,8 @@ For installations from source:
## Backup archive permissions
-The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default.
+The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
+will have owner/group git:git and 0600 permissions by default.
This is meant to avoid other system users reading GitLab's data.
If you need the backup archives to have different permissions you can use the 'archive_permissions' setting.
@@ -335,7 +339,7 @@ First make sure your backup tar file is in the backup directory described in the
`/var/opt/gitlab/backups`.
```shell
-sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/
+sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/
```
Stop the processes that are connected to the database. Leave the rest of GitLab
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 8d4bfd913bd..b1e3b116548 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -11,12 +11,15 @@ guide links by version.
### 1. Stop server
- sudo service gitlab stop
+```bash
+sudo service gitlab stop
+```
### 2. Backup
```bash
cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
@@ -49,6 +52,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
@@ -56,6 +61,8 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut
For GitLab Community Edition:
```bash
+cd /home/git/gitlab
+
sudo -u git -H git checkout 8-15-stable
```
@@ -64,28 +71,12 @@ OR
For GitLab Enterprise Edition:
```bash
-sudo -u git -H git checkout 8-15-stable-ee
-```
-
-### 5. Update gitlab-shell
-
-```bash
-cd /home/git/gitlab-shell
-sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.1.1
-```
-
-### 6. Update gitlab-workhorse
-
-Install and compile gitlab-workhorse. This requires
-[Go 1.5](https://golang.org/dl) which should already be on your system from
-GitLab 8.1.
+cd /home/git/gitlab
-```bash
-sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+sudo -u git -H git checkout 8-15-stable-ee
```
-### 7. Install libs, migrations, etc.
+### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -106,6 +97,27 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.1.1
+```
+
### 8. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -113,6 +125,8 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
+cd /home/git/gitlab
+
git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example
```
@@ -122,6 +136,8 @@ Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`.
```sh
+cd /home/git/gitlab
+
sudo -u git -H git config --global repack.writeBitmaps true
```
@@ -130,6 +146,8 @@ sudo -u git -H git config --global repack.writeBitmaps true
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
+cd /home/git/gitlab
+
# For HTTPS configurations
git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl
@@ -162,26 +180,42 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
- sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
For Ubuntu 16.04.1 LTS:
- sudo systemctl daemon-reload
+```bash
+sudo systemctl daemon-reload
+```
### 9. Start application
- sudo service gitlab start
- sudo service nginx restart
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
### 10. Check application status
Check if GitLab and its environment are configured correctly:
- sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
To make sure you didn't miss anything run a more thorough check:
- sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
If all items are green, then congratulations, the upgrade is complete!
@@ -196,6 +230,7 @@ database migration (the backup is already migrated to the previous version).
```bash
cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
new file mode 100644
index 00000000000..3d68fe201a7
--- /dev/null
+++ b/doc/update/8.15-to-8.16.md
@@ -0,0 +1,237 @@
+# From 8.15 to 8.16
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-16-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-16-stable-ee
+```
+
+### 5. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.1.1
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-15-stable:config/gitlab.yml.example origin/8-16-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/8-15-stable:lib/support/nginx/gitlab-ssl origin/8-16-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-15-stable:lib/support/nginx/gitlab origin/8-16-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 9. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.15)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.14 to 8.15](8.14-to-8.15.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 685972cfb41..54d523b59fd 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -14,6 +14,7 @@ user on the database version)
```bash
cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
@@ -32,28 +33,13 @@ current version with `cat VERSION`).
```bash
cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
-### 3. Update gitlab-shell to the corresponding version
-
-```bash
-cd /home/git/gitlab-shell
-sudo -u git -H git fetch
-sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
-```
-
-### 4. Update gitlab-workhorse to the corresponding version
-
-```bash
-cd /home/git/gitlab
-
-sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
-```
-
-### 5. Install libs, migrations, etc.
+### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -74,6 +60,23 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
+### 4. Update gitlab-workhorse to the corresponding version
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 5. Update gitlab-shell to the corresponding version
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
+```
+
### 6. Start application
```bash
@@ -86,6 +89,8 @@ sudo service nginx restart
Check if GitLab and its environment are configured correctly:
```bash
+cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 86fe52ef4ff..62afd8cf247 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -50,7 +50,7 @@ exception of the staging and production stages, where only data deployed to
production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
-[environment], then you will not have any data for those stages.
+or `production/*` [environment], then you will not have any data for those stages.
Below you can see in more detail what the various stages of Cycle Analytics mean.
@@ -61,7 +61,7 @@ Below you can see in more detail what the various stages of Cycle Analytics mean
| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
-| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. |
| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
---
@@ -79,10 +79,13 @@ Here's a little explanation of how this works behind the scenes:
etc.
To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
-So, if a merge request doesn't close an issue or an issue is not labeled with a
-label present in the Issue Board or assigned a milestone or a project has no
-`production` environment (for staging and production stages), the Cycle Analytics
-dashboard won't present any data at all.
+So, the Cycle Analytics dashboard won't present any data:
+- For merge requests that do not close an issue.
+- For issues not labeled with a label present in the Issue Board.
+- For issues not assigned a milestone.
+- For staging and production stages, if the project has no `production` or `production/*`
+ environment.
+
## Example workflow
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index c936e8833c6..4c52974e103 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher
-- Watchers: users with notification level "Watch"
+- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 22d971fadfb..c89f587f14d 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -113,8 +113,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
project.team << [user, :reporter]
end
- step 'I click link "Import team from another project"' do
- click_link "Import"
+ step 'I click link "Import team from another project"' do
+ page.within '.users-project-form' do
+ click_link "Import"
+ end
end
When 'I submit "Website" project for import team' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 9d5adffd8f4..6cf6b501021 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -14,7 +14,11 @@ module API
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
- # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
+ # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
+ rescue_from Grape::Exceptions::MethodNotAllowed do |e|
+ error! e.message, e.status, e.headers
+ end
+
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index ee9247ee240..20b5bc1502a 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,6 +1,7 @@
module API
module Helpers
include Gitlab::Utils
+ include Helpers::Pagination
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
@@ -85,12 +86,6 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
- def paginate(relation)
- relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
- add_pagination_headers(data)
- end
- end
-
def authenticate!
unauthorized! unless current_user
end
@@ -361,38 +356,6 @@ module API
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end
- def add_pagination_headers(paginated_data)
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', paginated_data.total_pages.to_s
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
- end
-
- def pagination_links(paginated_data)
- request_url = request.url.split('?').first
- request_params = params.clone
- request_params[:per_page] = paginated_data.limit_value
-
- links = []
-
- request_params[:page] = paginated_data.current_page - 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
-
- request_params[:page] = paginated_data.current_page + 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
-
- request_params[:page] = 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
-
- request_params[:page] = paginated_data.total_pages
- links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
-
- links.join(', ')
- end
-
def secret_token
Gitlab::Shell.secret_token
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
new file mode 100644
index 00000000000..2199eea7e5f
--- /dev/null
+++ b/lib/api/helpers/pagination.rb
@@ -0,0 +1,45 @@
+module API
+ module Helpers
+ module Pagination
+ def paginate(relation)
+ relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+ end
+
+ def pagination_links(paginated_data)
+ request_url = request.url.split('?').first
+ request_params = params.clone
+ request_params[:per_page] = paginated_data.limit_value
+
+ links = []
+
+ request_params[:page] = paginated_data.current_page - 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
+
+ request_params[:page] = paginated_data.current_page + 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
+
+ request_params[:page] = 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+
+ request_params[:page] = paginated_data.total_pages
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+
+ links.join(', ')
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index db2d18f935d..d235977fbd8 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -28,6 +28,8 @@ module API
protocol = params[:protocol]
+ actor.update_last_used_at if actor.is_a?(Key)
+
access =
if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
@@ -61,6 +63,8 @@ module API
status 200
key = Key.find(params[:key_id])
+ key.update_last_used_at
+
token_handler = Gitlab::LfsToken.new(key)
{
@@ -103,7 +107,9 @@ module API
key = Key.find_by(id: params[:key_id])
- unless key
+ if key
+ key.update_last_used_at
+ else
return { 'success' => false, 'message' => 'Could not find the given key' }
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 7e6537e3d9e..cefbfdce3bb 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -2,6 +2,7 @@ module Backup
class Manager
ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
FOLDERS_TO_BACKUP = %w[repositories db]
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'
def pack
# Make sure there is a connection
@@ -14,7 +15,7 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"]
- tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar'
+ tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}"
Dir.chdir(Gitlab.config.backup.path) do
File.open("#{Gitlab.config.backup.path}/backup_information.yml",
@@ -82,7 +83,7 @@ module Backup
removed = 0
Dir.chdir(Gitlab.config.backup.path) do
- Dir.glob('*_gitlab_backup.tar').each do |file|
+ Dir.glob("*#{FILE_NAME_SUFFIX}").each do |file|
next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/
timestamp = $1.to_i
@@ -108,41 +109,50 @@ module Backup
Dir.chdir(Gitlab.config.backup.path)
# check for existing backups in the backup dir
- file_list = Dir.glob("*_gitlab_backup.tar")
- puts "no backups found" if file_list.count == 0
+ file_list = Dir.glob("*#{FILE_NAME_SUFFIX}")
+
+ if file_list.count == 0
+ $progress.puts "No backups found in #{Gitlab.config.backup.path}"
+ $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
+ exit 1
+ end
if file_list.count > 1 && ENV["BACKUP"].nil?
- puts "Found more than one backup, please specify which one you want to restore:"
- puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
+ $progress.puts 'Found more than one backup, please specify which one you want to restore:'
+ $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
exit 1
end
- tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first
+ if ENV['BACKUP'].present?
+ tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ tar_file = file_list.first
+ end
unless File.exist?(tar_file)
- puts "The specified backup doesn't exist!"
+ $progress.puts "The backup file #{tar_file} does not exist!"
exit 1
end
- $progress.print "Unpacking backup ... "
+ $progress.print 'Unpacking backup ... '
unless Kernel.system(*%W(tar -xf #{tar_file}))
- puts "unpacking backup failed".color(:red)
+ $progress.puts 'unpacking backup failed'.color(:red)
exit 1
else
- $progress.puts "done".color(:green)
+ $progress.puts 'done'.color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION
- puts "GitLab version mismatch:".color(:red)
- puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
- puts " Please switch to the following version and try again:".color(:red)
- puts " version: #{settings[:gitlab_version]}".color(:red)
- puts
- puts "Hint: git checkout v#{settings[:gitlab_version]}"
+ $progress.puts 'GitLab version mismatch:'.color(:red)
+ $progress.puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
+ $progress.puts ' Please switch to the following version and try again:'.color(:red)
+ $progress.puts " version: #{settings[:gitlab_version]}".color(:red)
+ $progress.puts
+ $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
end
end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 229050151d3..c10d3616f31 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -105,7 +105,7 @@ module Ci
break
elsif s.scan(/</)
@out << '&lt;'
- elsif s.scan(/\n/)
+ elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index a6b9beecded..24bb3649a76 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
+ # Retain 405 error rather than a 500 error for Grape 0.15.0+.
+ # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
+ rescue_from Grape::Exceptions::MethodNotAllowed do |e|
+ error! e.message, e.status, e.headers
+ end
+
+ rescue_from Grape::Exceptions::Base do |e|
+ error! e.message, e.status, e.headers
+ end
+
rescue_from :all do |exception|
handle_api_exception(exception)
end
diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb
index fb04a7824b8..63f9f8d7a5a 100644
--- a/lib/email_template_interceptor.rb
+++ b/lib/email_template_interceptor.rb
@@ -5,8 +5,8 @@ class EmailTemplateInterceptor
def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled.
unless current_application_settings.html_emails_enabled
- message.part.delete_if do |part|
- part.content_type.try(:start_with?, 'text/html')
+ message.parts.delete_if do |part|
+ part.content_type.start_with?('text/html')
end
end
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 7e06bd2b0fb..7ed01bf56ca 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -34,21 +34,21 @@ module Gitlab
def allowed?
if ldap_user
unless ldap_config.active_directory
- user.activate if user.ldap_blocked?
+ unblock_user(user, 'is available again') if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- user.ldap_block
+ block_user(user, 'is disabled in Active Directory')
false
else
- user.activate if user.ldap_blocked?
+ unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
- user.ldap_block
+ block_user(user, 'does not exist anymore')
false
end
end
@@ -64,6 +64,24 @@ module Gitlab
def ldap_user
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end
+
+ def block_user(user, reason)
+ user.ldap_block
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " +
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+
+ def unblock_user(user, reason)
+ user.activate
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " +
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
end
end
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 91fb0bb317a..d01d47a6a7a 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -70,8 +70,12 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
- path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
- trans.action = "Grape##{endpoint.route.request_method} #{path}"
+
+ # endpoint.route is nil in the case of a 405 response
+ if endpoint.route
+ path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
+ trans.action = "Grape##{endpoint.route.request_method} #{path}"
+ end
end
private
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index b9d9117c928..17dc101b7ee 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -31,7 +31,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
- namespace_project_group_links_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
end
end
@@ -62,7 +62,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
- namespace_project_group_links_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
end
end
@@ -76,7 +76,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
- namespace_project_group_links_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
expect(flash[:alert]).to eq('Please select a group.')
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 5fe7e6407cc..1ed2ee3ab4a 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -5,13 +5,33 @@ describe Projects::PipelinesController do
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
before do
sign_in(user)
end
+ describe 'GET index.json' do
+ before do
+ create_list(:ci_empty_pipeline, 2, project: project)
+
+ get :index, namespace_id: project.namespace.path,
+ project_id: project.path,
+ format: :json
+ end
+
+ it 'returns JSON with serialized pipelines' do
+ expect(response).to have_http_status(:ok)
+
+ expect(json_response).to include('pipelines')
+ expect(json_response['pipelines'].count).to eq 2
+ expect(json_response['count']['all']).to eq 2
+ expect(json_response['count']['running_or_pending']).to eq 2
+ end
+ end
+
describe 'GET stages.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
context 'when accessing existing stage' do
before do
create(:ci_build, pipeline: pipeline, stage: 'build')
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index b52137fbe7e..442f81187dc 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do
let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
- it 'renders index with 200 status code' do
+ it 'should have the settings/members address with a 302 status code' do
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
- expect(response).to render_template(:index)
+ expect(response).to have_http_status(302)
+ expect(response.location).to include namespace_project_settings_members_path(project.namespace, project)
end
end
@@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
it 'adds no user to members' do
@@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'No users or groups specified.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
end
end
@@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to redirect_to(
- namespace_project_project_members_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
expect(project.members).not_to include member
end
@@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do
expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to(
- namespace_project_project_members_path(project.namespace, project)
+ namespace_project_settings_members_path(project.namespace, project)
)
end
end
diff --git a/spec/controllers/projects/settings/members_controller_spec.rb b/spec/controllers/projects/settings/members_controller_spec.rb
new file mode 100644
index 00000000000..076d6cd9c6e
--- /dev/null
+++ b/spec/controllers/projects/settings/members_controller_spec.rb
@@ -0,0 +1,14 @@
+require('spec_helper')
+
+describe Projects::Settings::MembersController do
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
index a7c5283df94..007b35bbb77 100644
--- a/spec/db/production/settings.rb
+++ b/spec/db/production/settings.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
require 'rainbow/ext/string'
describe 'seed production settings', lib: true do
+ include StubENV
+
context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
before do
- allow(ENV).to receive(:[]).and_call_original
- allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789')
+ stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789')
end
it 'writes the token to the database' do
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 1735791f644..77404f46c92 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -31,6 +31,14 @@ FactoryGirl.define do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
end
+
+ # Populates pipeline with errors
+ #
+ pipeline.config_processor if evaluator.config
+ end
+
+ trait :invalid do
+ config(rspec: nil)
end
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index e3b73e29987..ed4acca23f1 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -8,6 +8,10 @@ FactoryGirl.define do
is_shared false
active true
+ trait :online do
+ contacted_at Time.now
+ end
+
trait :shared do
is_shared true
end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index 81077f4b005..3ebc432206a 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'CI Lint' do
+describe 'CI Lint', js: true do
before do
login_as :user
end
@@ -8,7 +8,10 @@ describe 'CI Lint' do
describe 'YAML parsing' do
before do
visit ci_lint_path
- fill_in 'content', with: yaml_content
+ # Ace editor updates a hidden textarea and it happens asynchronously
+ # `sleep 0.1` is actually needed here because of this
+ execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");")
+ sleep 0.1
click_on 'Validate'
end
@@ -40,7 +43,7 @@ describe 'CI Lint' do
let(:yaml_content) { 'my yaml content' }
it 'loads previous YAML content after validation' do
- expect(page).to have_field('content', with: 'my yaml content', type: 'textarea')
+ expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea')
end
end
end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index 1a71a03fbd9..8b302a6aa23 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do
context 'setting an expiration date for a group link' do
before do
- visit namespace_project_group_links_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
select2 group.id, from: '#link_group_id'
- fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+ fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
click_on 'Share'
end
diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
index c5e3d143d91..d82cf53c690 100644
--- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb
+++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb
@@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do
end
scenario "anonymous user visits the project's members page and sees the list of members" do
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
expect(current_path).to eq(
- namespace_project_project_members_path(project.namespace, project))
+ namespace_project_settings_members_path(project.namespace, project))
expect(page).to have_content(user.name)
end
end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
index 94995f7cf95..cffb935ad5a 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/group_links_spec.rb
@@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
@group_link = create(:project_group_link, project: project, group: group)
login_as(user)
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
it 'updates group access level' do
@@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
wait_for_ajax
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
expect(first('.group_member')).to have_content('Guest')
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 7d0065ee2c4..3385e5972ff 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -19,7 +19,7 @@ feature 'Projects members', feature: true do
context 'with a group invitee' do
before do
group_invitee
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
@@ -33,7 +33,7 @@ feature 'Projects members', feature: true do
before do
group_invitee
project_invitee
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project invitee, the project developer, and the group owner' do
@@ -54,7 +54,7 @@ feature 'Projects members', feature: true do
context 'with a group requester' do
before do
group.request_access(group_requester)
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
@@ -68,7 +68,7 @@ feature 'Projects members', feature: true do
before do
group.request_access(group_requester)
project.request_access(project_requester)
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project requester, the project developer, and the group owner' do
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index b7273021c95..f136d9ce0fa 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -14,15 +14,15 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master)
end
- scenario 'expiration date is displayed in the members list' do
+ scenario 'expiration date is displayed in the members list', js: true do
travel_to Time.zone.parse('2016-08-06 08:00') do
- visit namespace_project_project_members_path(project.namespace, project)
-
+ visit namespace_project_settings_members_path(project.namespace, project)
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: '2016-08-10'
- click_on 'Add to project'
end
+ find('.users-project-form').click
+ click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 97c42bd7f01..0b4dcaa39c6 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do
open_project_settings_menu
click_link 'Members'
- visit namespace_project_project_members_path(project.namespace, project)
+ visit namespace_project_settings_members_path(project.namespace, project)
page.within('.content') do
expect(page).not_to have_content(user.name)
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index cef50f6f237..3ba996e2e10 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -1,267 +1,364 @@
require 'spec_helper'
describe 'Pipelines', :feature, :js do
- include GitlabRoutingHelper
- include WaitForAjax
+ include WaitForVueResource
let(:project) { create(:empty_project) }
- let(:user) { create(:user) }
- before do
- login_as(user)
- project.team << [user, :developer]
- end
-
- describe 'GET /:project/pipelines' do
- let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') }
-
- [:all, :running, :branches].each do |scope|
- context "displaying #{scope}" do
- let(:project) { create(:project) }
-
- before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
-
- it { expect(page).to have_content(pipeline.short_sha) }
- end
- end
-
- context 'anonymous access' do
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ context 'when user is logged in' do
+ let(:user) { create(:user) }
- it { expect(page).to have_http_status(:success) }
+ before do
+ login_as(user)
+ project.team << [user, :developer]
end
- context 'cancelable pipeline' do
- let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
-
- before do
- build.run
- visit namespace_project_pipelines_path(project.namespace, project)
+ describe 'GET /:project/pipelines' do
+ let(:project) { create(:project) }
+
+ let!(:pipeline) do
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ ref: 'master',
+ status: 'running',
+ sha: project.commit.id,
+ )
end
- it { expect(page).to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-running') }
+ [:all, :running, :branches].each do |scope|
+ context "when displaying #{scope}" do
+ before do
+ visit_project_pipelines(scope: scope)
+ end
- context 'when canceling' do
- before { click_link('Cancel') }
-
- it { expect(page).not_to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-canceled') }
+ it 'contains pipeline commit short SHA' do
+ expect(page).to have_content(pipeline.short_sha)
+ end
+ end
end
- end
- context 'retryable pipelines' do
- let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+ context 'when pipeline is cancelable' do
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline,
+ stage: 'test',
+ commands: 'test')
+ end
- before do
- build.drop
- visit namespace_project_pipelines_path(project.namespace, project)
- end
+ before do
+ build.run
+ visit_project_pipelines
+ end
- it { expect(page).to have_link('Retry') }
- it { expect(page).to have_selector('.ci-failed') }
+ it 'indicates that pipeline can be canceled' do
+ expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.ci-running')
+ end
- context 'when retrying' do
- before { click_link('Retry') }
+ context 'when canceling' do
+ before { click_link('Cancel') }
- it { expect(page).not_to have_link('Retry') }
- it { expect(page).to have_selector('.ci-running') }
+ it 'indicated that pipelines was canceled' do
+ expect(page).not_to have_link('Cancel')
+ expect(page).to have_selector('.ci-canceled')
+ end
+ end
end
- end
- context 'with manual actions' do
- let!(:manual) do
- create(:ci_build, :manual, pipeline: pipeline,
- name: 'manual build',
- stage: 'test',
- commands: 'test')
- end
+ context 'when pipeline is retryable' do
+ let!(:build) do
+ create(:ci_build, pipeline: pipeline,
+ stage: 'test',
+ commands: 'test')
+ end
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
- end
+ before do
+ build.drop
+ visit_project_pipelines
+ end
- it 'has link to the manual action' do
- find('.js-pipeline-dropdown-manual-actions').click
+ it 'indicates that pipeline can be retried' do
+ expect(page).to have_link('Retry')
+ expect(page).to have_selector('.ci-failed')
+ end
- expect(page).to have_link('Manual build')
- end
+ context 'when retrying' do
+ before { click_link('Retry') }
- context 'when manual action was played' do
- before do
- find('.js-pipeline-dropdown-manual-actions').click
- click_link('Manual build')
+ it 'shows running pipeline that is not retryable' do
+ expect(page).not_to have_link('Retry')
+ expect(page).to have_selector('.ci-running')
+ end
end
+ end
- it 'enqueues manual action job' do
- expect(manual.reload).to be_pending
+ context 'when pipeline has configuration errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, :invalid, project: project)
end
- end
- end
- context 'for generic statuses' do
- context 'when running' do
- let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
+ before { visit_project_pipelines }
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
+ it 'contains badge that indicates errors' do
+ expect(page).to have_content 'yaml invalid'
end
- it 'is cancelable' do
- expect(page).to have_link('Cancel')
+ it 'contains badge with tooltip which contains error' do
+ expect(pipeline).to have_yaml_errors
+ expect(page).to have_selector(
+ %Q{span[data-original-title="#{pipeline.yaml_errors}"]})
end
+ end
- it 'has pipeline running' do
- expect(page).to have_selector('.ci-running')
+ context 'with manual actions' do
+ let!(:manual) do
+ create(:ci_build, :manual,
+ pipeline: pipeline,
+ name: 'manual build',
+ stage: 'test',
+ commands: 'test')
end
- context 'when canceling' do
- before { click_link('Cancel') }
+ before { visit_project_pipelines }
- it { expect(page).not_to have_link('Cancel') }
- it { expect(page).to have_selector('.ci-canceled') }
+ it 'has a dropdown with play button' do
+ expect(page).to have_selector('.dropdown-toggle.btn.btn-default .icon-play')
end
- end
- context 'when failed' do
- let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') }
+ it 'has link to the manual action' do
+ find('.js-pipeline-dropdown-manual-actions').click
- before do
- status.drop
- visit namespace_project_pipelines_path(project.namespace, project)
+ expect(page).to have_link('Manual build')
end
- it 'is not retryable' do
- expect(page).not_to have_link('Retry')
- end
+ context 'when manual action was played' do
+ before do
+ find('.js-pipeline-dropdown-manual-actions').click
+ click_link('Manual build')
+ end
- it 'has failed pipeline' do
- expect(page).to have_selector('.ci-failed')
+ it 'enqueues manual action job' do
+ expect(manual.reload).to be_pending
+ end
end
end
- end
-
- context 'downloadable pipelines' do
- context 'with artifacts' do
- let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') }
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ context 'for generic statuses' do
+ context 'when running' do
+ let!(:running) do
+ create(:generic_commit_status,
+ status: 'running',
+ pipeline: pipeline,
+ stage: 'test')
+ end
+
+ before { visit_project_pipelines }
+
+ it 'is cancelable' do
+ expect(page).to have_link('Cancel')
+ end
+
+ it 'has pipeline running' do
+ expect(page).to have_selector('.ci-running')
+ end
+
+ context 'when canceling' do
+ before { click_link('Cancel') }
+
+ it 'indicates that pipeline was canceled' do
+ expect(page).not_to have_link('Cancel')
+ expect(page).to have_selector('.ci-canceled')
+ end
+ end
+ end
- it { expect(page).to have_selector('.build-artifacts') }
- it do
- find('.js-pipeline-dropdown-download').click
- expect(page).to have_link(with_artifacts.name)
+ context 'when failed' do
+ let!(:status) do
+ create(:generic_commit_status, :pending,
+ pipeline: pipeline,
+ stage: 'test')
+ end
+
+ before do
+ status.drop
+ visit_project_pipelines
+ end
+
+ it 'is not retryable' do
+ expect(page).not_to have_link('Retry')
+ end
+
+ it 'has failed pipeline' do
+ expect(page).to have_selector('.ci-failed')
+ end
end
end
- context 'with artifacts expired' do
- let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+ context 'downloadable pipelines' do
+ context 'with artifacts' do
+ let!(:with_artifacts) do
+ create(:ci_build, :artifacts, :success,
+ pipeline: pipeline,
+ name: 'rspec tests',
+ stage: 'test')
+ end
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ before { visit_project_pipelines }
- it { expect(page).not_to have_selector('.build-artifacts') }
- end
+ it 'has artifats' do
+ expect(page).to have_selector('.build-artifacts')
+ end
- context 'without artifacts' do
- let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+ it 'has artifacts download dropdown' do
+ find('.js-pipeline-dropdown-download').click
- before { visit namespace_project_pipelines_path(project.namespace, project) }
+ expect(page).to have_link(with_artifacts.name)
+ end
+ end
- it { expect(page).not_to have_selector('.build-artifacts') }
- end
- end
+ context 'with artifacts expired' do
+ let!(:with_artifacts_expired) do
+ create(:ci_build, :artifacts_expired, :success,
+ pipeline: pipeline,
+ name: 'rspec',
+ stage: 'test')
+ end
- context 'mini pipleine graph' do
- let!(:build) do
- create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build')
- end
+ before { visit_project_pipelines }
- before do
- visit namespace_project_pipelines_path(project.namespace, project)
- end
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+
+ context 'without artifacts' do
+ let!(:without_artifacts) do
+ create(:ci_build, :success,
+ pipeline: pipeline,
+ name: 'rspec',
+ stage: 'test')
+ end
- it 'should render a mini pipeline graph' do
- endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name)
+ before { visit_project_pipelines }
- expect(page).to have_selector('.js-mini-pipeline-graph')
- expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']")
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
end
- context 'when clicking a graph stage' do
- it 'should open a dropdown' do
- find('.js-builds-dropdown-button').trigger('click')
+ context 'mini pipeline graph' do
+ let!(:build) do
+ create(:ci_build, :pending, pipeline: pipeline,
+ stage: 'build',
+ name: 'build')
+ end
- wait_for_ajax
+ before { visit_project_pipelines }
- expect(page).to have_link build.name
+ it 'should render a mini pipeline graph' do
+ expect(page).to have_selector('.js-mini-pipeline-graph')
+ expect(page).to have_selector('.js-builds-dropdown-button')
end
- it 'should be possible to retry the failed build' do
- find('.js-builds-dropdown-button').trigger('click')
+ context 'when clicking a stage badge' do
+ it 'should open a dropdown' do
+ find('.js-builds-dropdown-button').trigger('click')
+
+ expect(page).to have_link build.name
+ end
- wait_for_ajax
+ it 'should be possible to cancel pending build' do
+ find('.js-builds-dropdown-button').trigger('click')
+ find('a.js-ci-action-icon').trigger('click')
- find('a.js-ci-action-icon').trigger('click')
- expect(page).not_to have_content('Cancel running')
+ expect(page).to have_content('canceled')
+ expect(build.reload).to be_canceled
+ end
end
end
end
- end
- describe 'POST /:project/pipelines' do
- let(:project) { create(:project) }
+ describe 'POST /:project/pipelines' do
+ let(:project) { create(:project) }
- before { visit new_namespace_project_pipeline_path(project.namespace, project) }
+ before do
+ visit new_namespace_project_pipeline_path(project.namespace, project)
+ end
+
+ context 'for valid commit' do
+ before { fill_in('pipeline[ref]', with: 'master') }
+
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
- context 'for valid commit' do
- before { fill_in('pipeline[ref]', with: 'master') }
+ it 'creates a new pipeline' do
+ expect { click_on 'Create pipeline' }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
+ end
- context 'with gitlab-ci.yml' do
- before { stub_ci_pipeline_to_return_yaml_file }
+ context 'without gitlab-ci.yml' do
+ before { click_on 'Create pipeline' }
- it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) }
+ it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ end
end
- context 'without gitlab-ci.yml' do
- before { click_on 'Create pipeline' }
+ context 'for invalid commit' do
+ before do
+ fill_in('pipeline[ref]', with: 'invalid-reference')
+ click_on 'Create pipeline'
+ end
- it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ it { expect(page).to have_content('Reference not found') }
end
end
- context 'for invalid commit' do
+ describe 'Create pipelines' do
+ let(:project) { create(:project) }
+
before do
- fill_in('pipeline[ref]', with: 'invalid-reference')
- click_on 'Create pipeline'
+ visit new_namespace_project_pipeline_path(project.namespace, project)
+ end
+
+ describe 'new pipeline page' do
+ it 'has field to add a new pipeline' do
+ expect(page).to have_field('pipeline[ref]')
+ expect(page).to have_content('Create for')
+ end
end
- it { expect(page).to have_content('Reference not found') }
+ describe 'find pipelines' do
+ it 'shows filtered pipelines', js: true do
+ fill_in('pipeline[ref]', with: 'fix')
+ find('input#ref').native.send_keys(:keydown)
+
+ within('.ui-autocomplete') do
+ expect(page).to have_selector('li', text: 'fix')
+ end
+ end
+ end
end
end
- describe 'Create pipelines', feature: true do
- let(:project) { create(:project) }
-
+ context 'when user is not logged in' do
before do
- visit new_namespace_project_pipeline_path(project.namespace, project)
+ visit namespace_project_pipelines_path(project.namespace, project)
end
- describe 'new pipeline page' do
- it 'has field to add a new pipeline' do
- expect(page).to have_field('pipeline[ref]')
- expect(page).to have_content('Create for')
- end
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it { expect(page).to have_content 'No pipelines to show' }
+ it { expect(page).to have_http_status(:success) }
end
- describe 'find pipelines' do
- it 'shows filtered pipelines', js: true do
- fill_in('pipeline[ref]', with: 'fix')
- find('input#ref').native.send_keys(:keydown)
+ context 'when project is private' do
+ let(:project) { create(:project, :private) }
- within('.ui-autocomplete') do
- expect(page).to have_selector('li', text: 'fix')
- end
- end
+ it { expect(page).to have_content 'You need to sign in' }
end
end
+
+ def visit_project_pipelines(**query)
+ visit namespace_project_pipelines_path(project.namespace, project, query)
+ wait_for_vue_resource
+ end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 1897c8119d2..ecebabefff8 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/project_members" do
- subject { namespace_project_project_members_path(project.namespace, project) }
+ describe "GET /:project_path/settings/members" do
+ subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index f52e23f9433..9bc59a7c4f9 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/project_members" do
- subject { namespace_project_project_members_path(project.namespace, project) }
+ describe "GET /:project_path/settings/members" do
+ subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index bed9e92fcb6..a8d43b3d581 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) }
end
- describe "GET /:project_path/project_members" do
- subject { namespace_project_project_members_path(project.namespace, project) }
+ describe "GET /:project_path/settings/members" do
+ subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 837b0de9a4c..ad7f032d1e5 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
require_relative '../../config/initializers/secret_token'
describe 'create_tokens', lib: true do
+ include StubENV
+
let(:secrets) { ActiveSupport::OrderedOptions.new }
before do
- allow(ENV).to receive(:[]).and_call_original
allow(File).to receive(:write)
allow(File).to receive(:delete)
allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets)
@@ -17,7 +18,7 @@ describe 'create_tokens', lib: true do
context 'setting secret_key_base and otp_key_base' do
context 'when none of the secrets exist' do
before do
- allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil)
+ stub_env('SECRET_KEY_BASE', nil)
allow(File).to receive(:exist?).with('.secret').and_return(false)
allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false)
allow(self).to receive(:warn_missing_secret)
@@ -69,7 +70,7 @@ describe 'create_tokens', lib: true do
context 'when secret_key_base exists in the environment and secrets.yml' do
before do
- allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key')
+ stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
end
diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6
new file mode 100644
index 00000000000..1a7f2bb5fb8
--- /dev/null
+++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6
@@ -0,0 +1,168 @@
+//= require vue
+//= require lib/utils/common_utils
+//= require vue_pagination/index
+/* global fixture, gl */
+
+describe('Pagination component', () => {
+ let component;
+
+ const changeChanges = {
+ one: '',
+ two: '',
+ };
+
+ const change = (one, two) => {
+ changeChanges.one = one;
+ changeChanges.two = two;
+ };
+
+ it('should render and start at page 1', () => {
+ fixture.set('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 2,
+ previousPage: '',
+ },
+ change,
+ },
+ });
+
+ expect(component.$el.classList).toContain('gl-pagination');
+
+ component.changePage({ target: { innerText: '1' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the previous page', () => {
+ fixture.set('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 3,
+ previousPage: 1,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: 'Prev' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the next page', () => {
+ fixture.set('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: 'Next' } });
+
+ expect(changeChanges.one).toEqual(5);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the last page', () => {
+ fixture.set('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: 'Last >>' } });
+
+ expect(changeChanges.one).toEqual(10);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should go to the first page', () => {
+ fixture.set('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 5,
+ previousPage: 3,
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: '<< First' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+
+ it('should do nothing', () => {
+ fixture.set('<div class="test-pagination-container"></div>');
+
+ component = new window.gl.VueGlPagination({
+ el: document.querySelector('.test-pagination-container'),
+ propsData: {
+ pageInfo: {
+ totalPages: 10,
+ nextPage: 2,
+ previousPage: '',
+ },
+ change,
+ },
+ });
+
+ component.changePage({ target: { innerText: '...' } });
+
+ expect(changeChanges.one).toEqual(1);
+ expect(changeChanges.two).toEqual('all');
+ });
+});
+
+describe('paramHelper', () => {
+ it('can parse url parameters correctly', () => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+
+ const scope = gl.utils.getParameterByName('scope');
+ const p = gl.utils.getParameterByName('p');
+
+ expect(scope).toEqual('all');
+ expect(p).toEqual('2');
+ });
+
+ it('returns null if param not in url', () => {
+ window.history.pushState({}, null, '?p=2');
+
+ const scope = gl.utils.getParameterByName('scope');
+ const p = gl.utils.getParameterByName('p');
+
+ expect(scope).toEqual(null);
+ expect(p).toEqual('2');
+ });
+});
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
new file mode 100644
index 00000000000..267318faed4
--- /dev/null
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe API::Helpers::Pagination do
+ let(:resource) { Project.all }
+
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ describe '#paginate' do
+ let(:value) { spy('return value') }
+
+ before do
+ allow(value).to receive(:to_query).and_return(value)
+
+ allow(subject).to receive(:header).and_return(value)
+ allow(subject).to receive(:params).and_return(value)
+ allow(subject).to receive(:request).and_return(value)
+ end
+
+ describe 'required instance methods' do
+ let(:return_spy) { spy }
+
+ it 'requires some instance methods' do
+ expect_message(:header)
+ expect_message(:params)
+ expect_message(:request)
+
+ subject.paginate(resource)
+ end
+ end
+
+ context 'when resource can be paginated' do
+ before do
+ create_list(:empty_project, 3)
+ end
+
+ describe 'first page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ page: 1, per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 2
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '3')
+ expect_header('X-Total-Pages', '2')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '2')
+ expect_header('X-Prev-Page', '')
+ expect_header('Link', any_args)
+
+ subject.paginate(resource)
+ end
+ end
+
+ describe 'second page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ page: 2, per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 1
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '3')
+ expect_header('X-Total-Pages', '2')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '2')
+ expect_header('X-Next-Page', '')
+ expect_header('X-Prev-Page', '1')
+ expect_header('Link', any_args)
+
+ subject.paginate(resource)
+ end
+ end
+ end
+
+ def expect_header(name, value)
+ expect(subject).to receive(:header).with(name, value)
+ end
+
+ def expect_message(method)
+ expect(subject).to receive(method)
+ .at_least(:once).and_return(value)
+ end
+ end
+end
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
index 898f1e84ab0..0762fd7e56a 100644
--- a/spec/lib/ci/ansi2html_spec.rb
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do
expect(subject.convert("<")[:html]).to eq('&lt;')
end
+ it "replaces newlines with line break tags" do
+ expect(subject.convert("\n")[:html]).to eq('<br>')
+ end
+
+ it "groups carriage returns with newlines" do
+ expect(subject.convert("\r\n")[:html]).to eq('<br>')
+ end
+
describe "incremental update" do
shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) }
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index 1b749d1bd39..f84782ab440 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -1,9 +1,27 @@
require 'spec_helper'
describe Backup::Manager, lib: true do
- describe '#remove_old' do
- let(:progress) { StringIO.new }
+ include StubENV
+
+ let(:progress) { StringIO.new }
+
+ before do
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ @old_progress = $progress # rubocop:disable Style/GlobalVars
+ $progress = progress # rubocop:disable Style/GlobalVars
+ end
+
+ after do
+ $progress = @old_progress # rubocop:disable Style/GlobalVars
+ end
+ describe '#remove_old' do
let(:files) do
[
'1451606400_2016_01_01_gitlab_backup.tar',
@@ -20,20 +38,6 @@ describe Backup::Manager, lib: true do
allow(Dir).to receive(:glob).and_return(files)
allow(FileUtils).to receive(:rm)
allow(Time).to receive(:now).and_return(Time.utc(2016))
-
- allow(progress).to receive(:puts)
- allow(progress).to receive(:print)
-
- allow_any_instance_of(String).to receive(:color) do |string, _color|
- string
- end
-
- @old_progress = $progress # rubocop:disable Style/GlobalVars
- $progress = progress # rubocop:disable Style/GlobalVars
- end
-
- after do
- $progress = @old_progress # rubocop:disable Style/GlobalVars
end
context 'when keep_time is zero' do
@@ -124,4 +128,82 @@ describe Backup::Manager, lib: true do
end
end
end
+
+ describe '#unpack' do
+ before do
+ allow(Dir).to receive(:chdir)
+ end
+
+ context 'when there are no backup files in the directory' do
+ before do
+ allow(Dir).to receive(:glob).and_return([])
+ end
+
+ it 'fails the operation and prints an error' do
+ expect { subject.unpack }.to raise_error SystemExit
+ expect(progress).to have_received(:puts)
+ .with(a_string_matching('No backups found'))
+ end
+ end
+
+ context 'when there are two backup files in the directory and BACKUP variable is not set' do
+ before do
+ allow(Dir).to receive(:glob).and_return(
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar',
+ '1451520000_2015_12_31_gitlab_backup.tar',
+ ]
+ )
+ end
+
+ it 'fails the operation and prints an error' do
+ expect { subject.unpack }.to raise_error SystemExit
+ expect(progress).to have_received(:puts)
+ .with(a_string_matching('Found more than one backup'))
+ end
+ end
+
+ context 'when BACKUP variable is set to a non-existing file' do
+ before do
+ allow(Dir).to receive(:glob).and_return(
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar'
+ ]
+ )
+ allow(File).to receive(:exist?).and_return(false)
+
+ stub_env('BACKUP', 'wrong')
+ end
+
+ it 'fails the operation and prints an error' do
+ expect { subject.unpack }.to raise_error SystemExit
+ expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar')
+ expect(progress).to have_received(:puts)
+ .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist'))
+ end
+ end
+
+ context 'when BACKUP variable is set to a correct file' do
+ before do
+ allow(Dir).to receive(:glob).and_return(
+ [
+ '1451606400_2016_01_01_gitlab_backup.tar'
+ ]
+ )
+ allow(File).to receive(:exist?).and_return(true)
+ allow(Kernel).to receive(:system).and_return(true)
+ allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION)
+
+ stub_env('BACKUP', '1451606400_2016_01_01')
+ end
+
+ it 'unpacks the file' do
+ subject.unpack
+
+ expect(Kernel).to have_received(:system)
+ .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar")
+ expect(progress).to have_received(:puts).with(a_string_matching('done'))
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index ac26c831fd0..d88a141b458 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -248,6 +248,7 @@ DeployKey:
- fingerprint
- public
- can_push
+- last_used_at
Service:
- id
- type
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index 534bcbf39fe..011c33e63a1 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'should block user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'does not exist anymore')
+
access.allowed?
- expect(user).to be_blocked
- expect(user).to be_ldap_blocked
end
end
@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
+
access.allowed?
- expect(user).to be_blocked
- expect(user).to be_ldap_blocked
end
end
@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'does not unblock user in GitLab' do
+ expect(access).not_to receive(:unblock_user)
+
access.allowed?
+
expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks user in GitLab' do
+ expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
+
access.allowed?
- expect(user).not_to be_blocked
end
end
end
@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'does not exist anymore')
+
access.allowed?
- expect(user).to be_blocked
- expect(user).to be_ldap_blocked
end
end
@@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks the user if it exists' do
+ expect(access).to receive(:unblock_user).with(user, 'is available again')
+
access.allowed?
- expect(user).not_to be_blocked
end
end
end
end
end
+
+ describe '#block_user' do
+ before do
+ user.activate
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ access.block_user user, 'reason'
+ end
+
+ it 'blocks the user' do
+ expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
+ end
+
+ it 'logs the reason' do
+ expect(Gitlab::AppLogger).to have_received(:info).with(
+ "LDAP account \"123456\" reason, " +
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
+
+ describe '#unblock_user' do
+ before do
+ user.ldap_block
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ access.unblock_user user, 'reason'
+ end
+
+ it 'activates the user' do
+ expect(user).not_to be_blocked
+ expect(user).not_to be_ldap_blocked
+ end
+
+ it 'logs the reason' do
+ Gitlab::AppLogger.info(
+ "LDAP account \"123456\" reason, " +
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index cebaa157ef3..d1aee27057a 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -888,6 +888,48 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#stuck?' do
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ context 'when pipeline is stuck' do
+ it 'is stuck' do
+ expect(pipeline).to be_stuck
+ end
+ end
+
+ context 'when pipeline is not stuck' do
+ before { create(:ci_runner, :shared, :online) }
+
+ it 'is not stuck' do
+ expect(pipeline).not_to be_stuck
+ end
+ end
+ end
+
+ describe '#has_yaml_errors?' do
+ context 'when pipeline has errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: nil })
+ end
+
+ it 'contains yaml errors' do
+ expect(pipeline).to have_yaml_errors
+ end
+ end
+
+ context 'when pipeline does not have errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: { script: 'rake test' } })
+ end
+
+ it 'does not containyaml errors' do
+ expect(pipeline).not_to have_yaml_errors
+ end
+ end
+ end
+
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 93eb402e060..96efe1696c3 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -63,6 +63,23 @@ describe Environment, models: true do
end
end
+ describe '#update_merge_request_metrics?' do
+ { 'production' => true,
+ 'production/eu' => true,
+ 'production/www.gitlab.com' => true,
+ 'productioneu' => false,
+ 'Production' => false,
+ 'Production/eu' => false,
+ 'test-production' => false
+ }.each do |name, expected_value|
+ it "returns #{expected_value} for #{name}" do
+ env = create(:environment, name: name)
+
+ expect(env.update_merge_request_metrics?).to eq(expected_value)
+ end
+ end
+ end
+
describe '#first_deployment_for' do
let(:project) { create(:project) }
let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7758b7ffa97..5eaddd822be 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -28,6 +28,15 @@ describe Key, models: true do
expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})")
end
end
+
+ describe "#update_last_used_at" do
+ it "enqueues a UseKeyWorker job" do
+ key = create(:key)
+
+ expect(UseKeyWorker).to receive(:perform_async).with(key.id)
+ key.update_last_used_at
+ end
+ end
end
context "validation of uniqueness (based on fingerprint uniqueness)" do
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 0c163659a71..a9139f7d4ab 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -31,12 +31,14 @@ describe Label, models: true do
it 'validates title' do
is_expected.not_to allow_value('G,ITLAB').for(:title)
is_expected.not_to allow_value('').for(:title)
+ is_expected.not_to allow_value('s' * 256).for(:title)
is_expected.to allow_value('GITLAB').for(:title)
is_expected.to allow_value('gitlab').for(:title)
is_expected.to allow_value('G?ITLAB').for(:title)
is_expected.to allow_value('G&ITLAB').for(:title)
is_expected.to allow_value("customer's request").for(:title)
+ is_expected.to allow_value('s' * 255).for(:title)
end
end
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
new file mode 100644
index 00000000000..383704572b1
--- /dev/null
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe BuildActionEntity do
+ let(:build) { create(:ci_build, name: 'test_build') }
+
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains humanized build name' do
+ expect(subject[:name]).to eq 'Test build'
+ end
+
+ it 'contains path to the action play' do
+ expect(subject[:path]).to include "builds/#{build.id}/play"
+ end
+ end
+end
diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb
new file mode 100644
index 00000000000..2fc60aa9de6
--- /dev/null
+++ b/spec/serializers/build_artifact_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe BuildArtifactEntity do
+ let(:build) { create(:ci_build, name: 'test:build') }
+
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains build name' do
+ expect(subject[:name]).to eq 'test:build'
+ end
+
+ it 'contains path to the artifacts' do
+ expect(subject[:path])
+ .to include "builds/#{build.id}/artifacts/download"
+ end
+ end
+end
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index 15f11ac3df9..a8662e81d20 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -45,4 +45,8 @@ describe CommitEntity do
subject
end
+
+ it 'exposes gravatar url that belongs to author' do
+ expect(subject.fetch(:author_gravatar_url)).to match /gravatar/
+ end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
new file mode 100644
index 00000000000..b19464c7117
--- /dev/null
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe PipelineEntity do
+ let(:user) { create(:user) }
+ let(:request) { double('request') }
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ end
+
+ let(:entity) do
+ described_class.represent(pipeline, request: request)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when pipeline is empty' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'contains required fields' do
+ expect(subject).to include :id, :user, :path
+ expect(subject).to include :ref, :commit
+ expect(subject).to include :updated_at, :created_at
+ end
+
+ it 'contains details' do
+ expect(subject).to include :details
+ expect(subject[:details])
+ .to include :duration, :finished_at
+ expect(subject[:details])
+ .to include :stages, :artifacts, :manual_actions
+ expect(subject[:details][:status]).to include :icon, :text, :label
+ end
+
+ it 'contains flags' do
+ expect(subject).to include :flags
+ expect(subject[:flags])
+ .to include :latest, :triggered, :stuck,
+ :yaml_errors, :retryable, :cancelable
+ end
+ end
+
+ context 'when pipeline is retryable' do
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, status: :success, project: project)
+ end
+
+ before do
+ create(:ci_build, :failed, pipeline: pipeline)
+ end
+
+ context 'user has ability to retry pipeline' do
+ before { project.team << [user, :developer] }
+
+ it 'retryable flag is true' do
+ expect(subject[:flags][:retryable]).to eq true
+ end
+
+ it 'contains retry path' do
+ expect(subject[:retry_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to retry pipeline' do
+ it 'retryable flag is false' do
+ expect(subject[:flags][:retryable]).to eq false
+ end
+
+ it 'does not contain retry path' do
+ expect(subject).not_to have_key(:retry_path)
+ end
+ end
+ end
+
+ context 'when pipeline is cancelable' do
+ let(:project) { create(:empty_project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, status: :running, project: project)
+ end
+
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ context 'user has ability to cancel pipeline' do
+ before { project.team << [user, :developer] }
+
+ it 'cancelable flag is true' do
+ expect(subject[:flags][:cancelable]).to eq true
+ end
+
+ it 'contains cancel path' do
+ expect(subject[:cancel_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to cancel pipeline' do
+ it 'cancelable flag is false' do
+ expect(subject[:flags][:cancelable]).to eq false
+ end
+
+ it 'does not contain cancel path' do
+ expect(subject).not_to have_key(:cancel_path)
+ end
+ end
+ end
+
+ context 'when pipeline has YAML errors' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: { invalid: :value } })
+ end
+
+ it 'contains flag that indicates there are errors' do
+ expect(subject[:flags][:yaml_errors]).to be true
+ end
+
+ it 'contains information about error' do
+ expect(subject[:yaml_errors]).to be_present
+ end
+ end
+
+ context 'when pipeline does not have YAML errors' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'contains flag that indicates there are no errors' do
+ expect(subject[:flags][:yaml_errors]).to be false
+ end
+
+ it 'does not contain field that normally holds an error' do
+ expect(subject).not_to have_key(:yaml_errors)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
new file mode 100644
index 00000000000..3a32cb394dd
--- /dev/null
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe PipelineSerializer do
+ let(:user) { create(:user) }
+
+ let(:serializer) do
+ described_class.new(user: user)
+ end
+
+ let(:entity) do
+ serializer.represent(resource)
+ end
+
+ subject { entity.as_json }
+
+ describe '#represent' do
+ context 'when used without pagination' do
+ it 'created a not paginated serializer' do
+ expect(serializer).not_to be_paginated
+ end
+
+ context 'when a single object is being serialized' do
+ let(:resource) { create(:ci_empty_pipeline) }
+
+ it 'serializers the pipeline object' do
+ expect(subject[:id]).to eq resource.id
+ end
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:resource) { create_list(:ci_pipeline, 2) }
+
+ it 'serializers the array of pipelines' do
+ expect(subject).not_to be_empty
+ end
+ end
+ end
+
+ context 'when used with pagination' do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:pagination) { {} }
+
+ before do
+ allow(request)
+ .to receive(:query_parameters)
+ .and_return(pagination)
+ end
+
+ let(:serializer) do
+ described_class.new(user: user)
+ .with_pagination(request, response)
+ end
+
+ it 'created a paginated serializer' do
+ expect(serializer).to be_paginated
+ end
+
+ context 'when resource does is not paginatable' do
+ context 'when a single pipeline object is being serialized' do
+ let(:resource) { create(:ci_empty_pipeline) }
+ let(:pagination) { { page: 1, per_page: 1 } }
+
+ it 'raises error' do
+ expect { subject }
+ .to raise_error(PipelineSerializer::InvalidResourceError)
+ end
+ end
+ end
+
+ context 'when resource is paginatable relation' do
+ let(:resource) { Ci::Pipeline.all }
+ let(:pagination) { { page: 1, per_page: 2 } }
+
+ context 'when a single pipeline object is present in relation' do
+ before { create(:ci_empty_pipeline) }
+
+ it 'serializes pipeline relation' do
+ expect(subject.first).to have_key :id
+ end
+ end
+
+ context 'when a multiple pipeline objects are being serialized' do
+ before { create_list(:ci_empty_pipeline, 3) }
+
+ it 'serializes appropriate number of objects' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(response).to receive(:[]=).with('X-Total', '3')
+ expect(response).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(response).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/request_aware_entity_spec.rb b/spec/serializers/request_aware_entity_spec.rb
new file mode 100644
index 00000000000..aa666b961dc
--- /dev/null
+++ b/spec/serializers/request_aware_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe RequestAwareEntity do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ it 'includes URL helpers' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+
+ it 'includes method for checking abilities' do
+ expect(subject).to respond_to(:can?)
+ end
+
+ it 'fetches request from options' do
+ expect(subject).to receive(:options)
+ .and_return({ request: 'some value' })
+
+ expect(subject.request).to eq 'some value'
+ end
+end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
new file mode 100644
index 00000000000..4ab40d08432
--- /dev/null
+++ b/spec/serializers/stage_entity_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe StageEntity do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:request) { double('request') }
+ let(:user) { create(:user) }
+
+ let(:entity) do
+ described_class.new(stage, request: request)
+ end
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ create(:ci_build, :success, pipeline: pipeline)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains relevant fields' do
+ expect(subject).to include :name, :status, :path
+ end
+
+ it 'contains detailed status' do
+ expect(subject[:status]).to include :text, :label, :group, :icon
+ expect(subject[:status][:label]).to eq 'passed'
+ end
+
+ it 'contains valid name' do
+ expect(subject[:name]).to eq 'test'
+ end
+
+ it 'contains path to the stage' do
+ expect(subject[:path])
+ .to include "pipelines/#{pipeline.id}##{stage.name}"
+ end
+
+ it 'contains path to the stage dropdown' do
+ expect(subject[:dropdown_path])
+ .to include "pipelines/#{pipeline.id}/stage.json?stage=test"
+ end
+
+ it 'contains stage title' do
+ expect(subject[:title]).to eq 'test: passed'
+ end
+ end
+end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
new file mode 100644
index 00000000000..89428b4216e
--- /dev/null
+++ b/spec/serializers/status_entity_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe StatusEntity do
+ let(:entity) { described_class.new(status) }
+
+ let(:status) do
+ Gitlab::Ci::Status::Success.new(double('object'), double('user'))
+ end
+
+ before do
+ allow(status).to receive(:has_details?).and_return(true)
+ allow(status).to receive(:details_path).and_return('some/path')
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains status details' do
+ expect(subject).to include :text, :icon, :label, :group
+ expect(subject).to include :has_details, :details_path
+ end
+ end
+end
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
new file mode 100644
index 00000000000..063b3bd76eb
--- /dev/null
+++ b/spec/services/projects/participants_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Projects::ParticipantsService, services: true do
+ describe '#groups' do
+ describe 'avatar_url' do
+ let(:project) { create(:empty_project, :public) }
+ let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) }
+ let(:user) { create(:user) }
+ let(:base_url) { Settings.send(:build_base_gitlab_url) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ it 'should return an url for the avatar' do
+ participants = described_class.new(project, user)
+ groups = participants.groups
+
+ expect(groups.size).to eq 1
+ expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png"
+ end
+
+ it 'should return an url for the avatar with relative url' do
+ stub_config_setting(relative_url_root: '/gitlab')
+ stub_config_setting(url: Settings.send(:build_gitlab_url))
+
+ participants = described_class.new(project, user)
+ groups = participants.groups
+
+ expect(groups.size).to eq 1
+ expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png"
+ end
+ end
+ end
+end
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 1f6919151de..9fbb61565e3 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -20,7 +20,7 @@ describe Users::RefreshAuthorizedProjectsService do
to_remove = create_authorization(project2, user)
expect(service).to receive(:update_with_lease).
- with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
+ with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
service.execute
end
@@ -29,7 +29,7 @@ describe Users::RefreshAuthorizedProjectsService do
to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
expect(service).to receive(:update_with_lease).
- with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
+ with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
service.execute
end
@@ -90,7 +90,7 @@ describe Users::RefreshAuthorizedProjectsService do
it 'removes authorizations that should be removed' do
authorization = create_authorization(project, user)
- service.update_authorizations([authorization.id])
+ service.update_authorizations([authorization.project_id])
expect(user.project_authorizations).to be_empty
end
@@ -147,7 +147,12 @@ describe Users::RefreshAuthorizedProjectsService do
end
it 'sets the values to the project authorization rows' do
- expect(hash.values).to eq([ProjectAuthorization.first])
+ expect(hash.values.length).to eq(1)
+
+ value = hash.values[0]
+
+ expect(value.project_id).to eq(project.id)
+ expect(value.access_level).to eq(Gitlab::Access::MASTER)
end
end
@@ -167,10 +172,6 @@ describe Users::RefreshAuthorizedProjectsService do
expect(service.current_authorizations.length).to eq(1)
end
- it 'includes the row ID for every row' do
- expect(row.id).to be_a_kind_of(Numeric)
- end
-
it 'includes the project ID for every row' do
expect(row.project_id).to eq(project.id)
end
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
new file mode 100644
index 00000000000..18597b5c71f
--- /dev/null
+++ b/spec/support/stub_env.rb
@@ -0,0 +1,7 @@
+module StubENV
+ def stub_env(key, value)
+ allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed
+ @env_already_stubbed ||= true
+ allow(ENV).to receive(:[]).with(key).and_return(value)
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index a9fea5f1e81..bc751d20ce1 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -41,7 +41,7 @@ describe 'gitlab:app namespace rake task' do
context 'gitlab version' do
before do
- allow(Dir).to receive(:glob).and_return([])
+ allow(Dir).to receive(:glob).and_return(['1_gitlab_backup.tar'])
allow(Dir).to receive(:chdir)
allow(File).to receive(:exist?).and_return(true)
allow(Kernel).to receive(:system).and_return(true)
diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb
new file mode 100644
index 00000000000..4769d569548
--- /dev/null
+++ b/spec/views/shared/milestones/_issuables.html.haml.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'shared/milestones/_issuables.html.haml' do
+ let(:issuables_size) { 100 }
+
+ before do
+ allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil,
+ show_full_project_name: nil, dom_class: '',
+ issuables: double(size: issuables_size).as_null_object)
+
+ stub_template 'shared/milestones/_issuable.html.haml' => ''
+ end
+
+ it 'should show the issuables count if show_counter is true' do
+ render 'shared/milestones/issuables', show_counter: true
+ expect(rendered).to have_content('100')
+ end
+
+ it 'should not show the issuables count if show_counter is false' do
+ render 'shared/milestones/issuables', show_counter: false
+ expect(rendered).not_to have_content('100')
+ end
+
+ describe 'a high issuables count' do
+ let(:issuables_size) { 1000 }
+
+ it 'should show a delimited number if show_counter is true' do
+ render 'shared/milestones/issuables', show_counter: true
+ expect(rendered).to have_content('1,000')
+ end
+ end
+end
diff --git a/spec/workers/use_key_worker_spec.rb b/spec/workers/use_key_worker_spec.rb
new file mode 100644
index 00000000000..e50c788b82a
--- /dev/null
+++ b/spec/workers/use_key_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe UseKeyWorker do
+ describe "#perform" do
+ it "updates the key's last_used_at attribute to the current time when it exists" do
+ worker = described_class.new
+ key = create(:key)
+ current_time = Time.zone.now
+
+ Timecop.freeze(current_time) do
+ expect { worker.perform(key.id) }
+ .to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time)
+ end
+ end
+
+ it "returns false and skips the job when the key doesn't exist" do
+ worker = described_class.new
+ key = create(:key)
+
+ expect(worker.perform(key.id + 1)).to eq false
+ end
+ end
+end