summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-12-14 15:18:46 +0000
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-12-14 15:18:46 +0000
commit4bdfb0391a578847fde5589eb44ac6737c9e4b64 (patch)
tree4af12a6ef4f7ead85c2d80a48687f42c492564fa
parent161d7c02d4ff49d6d91b982faee61b5de00d2631 (diff)
parentb70b962892b2428557d95a9639fbcead4fe546fd (diff)
downloadgitlab-ce-4bdfb0391a578847fde5589eb44ac6737c9e4b64.tar.gz
Merge branch 'master' into 'dz-remove-namespaces-path-uniq'
Merge Request - https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8013 # Conflicts: # db/schema.rb
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/commit/file.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js2
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js4
-rw-r--r--app/assets/javascripts/diff.js.es67
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es621
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js11
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es674
-rw-r--r--app/assets/javascripts/pipelines.js.es611
-rw-r--r--app/assets/javascripts/smart_interval.js.es669
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/environments.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss3
-rw-r--r--app/assets/stylesheets/pages/notifications.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss17
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/snippets.scss20
-rw-r--r--app/controllers/projects/releases_controller.rb9
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/sessions_controller.rb6
-rw-r--r--app/finders/snippets_finder.rb47
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb20
-rw-r--r--app/helpers/environment_helper.rb6
-rw-r--r--app/helpers/gitlab_routing_helper.rb5
-rw-r--r--app/helpers/snippets_helper.rb11
-rw-r--r--app/models/ci/build.rb15
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/stage.rb6
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/concerns/routable.rb7
-rw-r--r--app/models/project_services/buildkite_service.rb3
-rw-r--r--app/models/project_services/drone_ci_service.rb3
-rw-r--r--app/models/project_services/emails_on_push_service.rb18
-rw-r--r--app/models/project_services/hipchat_service.rb6
-rw-r--r--app/models/project_services/irker_service.rb3
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/user.rb4
-rw-r--r--app/policies/group_member_policy.rb6
-rw-r--r--app/policies/group_policy.rb6
-rw-r--r--app/policies/personal_snippet_policy.rb5
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/views/admin/dashboard/_head.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml4
-rw-r--r--app/views/admin/projects/index.html.haml12
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/ci/status/_badge.html.haml10
-rw-r--r--app/views/dashboard/_snippets_head.html.haml20
-rw-r--r--app/views/dashboard/snippets/index.html.haml42
-rw-r--r--app/views/explore/snippets/index.html.haml10
-rw-r--r--app/views/groups/issues.html.haml6
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml3
-rw-r--r--app/views/projects/ci/builds/_build.html.haml5
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml4
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml5
-rw-r--r--app/views/projects/commit/_change.html.haml9
-rw-r--r--app/views/projects/commit/_commit_box.html.haml9
-rw-r--r--app/views/projects/commit/_pipeline.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml5
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml14
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/stage/_graph.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml9
-rw-r--r--app/views/repository_check_mailer/notify.html.haml2
-rw-r--r--app/views/repository_check_mailer/notify.text.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rwxr-xr-xapp/views/shared/icons/_icon_status_manual.svg1
-rw-r--r--app/views/shared/notifications/_button.html.haml3
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml3
-rw-r--r--app/views/shared/snippets/_header.html.haml12
-rw-r--r--app/views/shared/snippets/_snippet.html.haml31
-rw-r--r--app/views/snippets/_actions.html.haml14
-rw-r--r--app/views/snippets/_snippets.html.haml3
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml31
-rw-r--r--changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml4
-rw-r--r--changelogs/unreleased/20052-actions-table-vscroll.yml4
-rw-r--r--changelogs/unreleased/24807-stop-ddosing-ourselves.yml4
-rw-r--r--changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml4
-rw-r--r--changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml4
-rw-r--r--changelogs/unreleased/25136-last-deployment-link.yml4
-rw-r--r--changelogs/unreleased/25294-remove-signed-out-msg.yml4
-rw-r--r--changelogs/unreleased/25482-fix-api-sudo.yml4
-rw-r--r--changelogs/unreleased/25483-broken-tabs.yml4
-rw-r--r--changelogs/unreleased/allow-more-filenames.yml4
-rw-r--r--changelogs/unreleased/api-cherry-pick.yml4
-rw-r--r--changelogs/unreleased/api-simple-group-project.yml4
-rw-r--r--changelogs/unreleased/awards_handler.yml4
-rw-r--r--changelogs/unreleased/chomp-git-status-message.yml5
-rw-r--r--changelogs/unreleased/features-api-snippets.yml4
-rw-r--r--changelogs/unreleased/issue_13270.yml4
-rw-r--r--changelogs/unreleased/issue_25030.yml4
-rw-r--r--changelogs/unreleased/process-commit-worker-migration-encoding.yml4
-rw-r--r--changelogs/unreleased/unescape-relative-path.yml4
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/admin.rb23
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb20
-rw-r--r--db/migrate/20161212142807_add_lower_path_index_to_routes.rb22
-rw-r--r--db/schema.rb4
-rw-r--r--doc/README.md2
-rw-r--r--doc/api/commits.md39
-rw-r--r--doc/api/groups.md17
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/services.md112
-rw-r--r--doc/api/snippets.md232
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/update/8.13-to-8.14.md2
-rw-r--r--features/admin/settings.feature19
-rw-r--r--features/steps/project/snippets.rb2
-rw-r--r--features/steps/shared/paths.rb6
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/branches.rb16
-rw-r--r--lib/api/commits.rb36
-rw-r--r--lib/api/entities.rb13
-rw-r--r--lib/api/groups.rb16
-rw-r--r--lib/api/helpers.rb140
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/api/services.rb626
-rw-r--r--lib/api/snippets.rb137
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb2
-rw-r--r--lib/banzai/filter/relative_link_filter.rb14
-rw-r--r--lib/gitlab/allowable.rb7
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb37
-rw-r--r--lib/gitlab/ci/status/build/common.rb19
-rw-r--r--lib/gitlab/ci/status/build/factory.rb18
-rw-r--r--lib/gitlab/ci/status/build/play.rb53
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb37
-rw-r--r--lib/gitlab/ci/status/build/stop.rb49
-rw-r--r--lib/gitlab/ci/status/core.rb27
-rw-r--r--lib/gitlab/ci/status/extended.rb8
-rw-r--r--lib/gitlab/ci/status/factory.rb30
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb8
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb8
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb4
-rw-r--r--lib/gitlab/ci/status/stage/common.rb10
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb6
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/routing.rb6
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake4
-rw-r--r--package.json2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb55
-rw-r--r--spec/factories/ci/builds.rb13
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb6
-rw-r--r--spec/features/admin/admin_settings_spec.rb (renamed from features/steps/admin/settings.rb)59
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb67
-rw-r--r--spec/features/projects/builds_spec.rb37
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb44
-rw-r--r--spec/features/security/admin_access_spec.rb2
-rw-r--r--spec/finders/snippets_finder_spec.rb84
-rw-r--r--spec/javascripts/awards_handler_spec.js10
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml52
-rw-r--r--spec/javascripts/fixtures/pipeline_graph.html.haml15
-rw-r--r--spec/javascripts/pipelines_spec.js.es625
-rw-r--r--spec/javascripts/project_title_spec.js16
-rw-r--r--spec/javascripts/smart_interval_spec.js.es635
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb2
-rw-r--r--spec/lib/gitlab/allowable_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/status/build/common_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb141
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb82
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb84
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/extended_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/common_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/stage/common_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb8
-rw-r--r--spec/lib/gitlab/routing_spec.rb23
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb27
-rw-r--r--spec/models/build_spec.rb91
-rw-r--r--spec/models/ci/pipeline_spec.rb20
-rw-r--r--spec/models/ci/stage_spec.rb4
-rw-r--r--spec/models/commit_status_spec.rb9
-rw-r--r--spec/models/concerns/routable_spec.rb14
-rw-r--r--spec/models/generic_commit_status_spec.rb16
-rw-r--r--spec/models/group_spec.rb7
-rw-r--r--spec/models/user_spec.rb11
-rw-r--r--spec/requests/api/branches_spec.rb31
-rw-r--r--spec/requests/api/commits_spec.rb70
-rw-r--r--spec/requests/api/groups_spec.rb22
-rw-r--r--spec/requests/api/helpers_spec.rb (renamed from spec/requests/api/api_helpers_spec.rb)85
-rw-r--r--spec/requests/api/merge_requests_spec.rb16
-rw-r--r--spec/requests/api/services_spec.rb5
-rw-r--r--spec/requests/api/snippets_spec.rb157
-rw-r--r--spec/requests/api/users_spec.rb27
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/support/login_helpers.rb3
-rw-r--r--spec/support/services_shared_context.rb6
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
217 files changed, 3737 insertions, 893 deletions
diff --git a/Gemfile b/Gemfile
index f27d6363e3d..2cc7764e6b8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
-gem 'sidekiq', '~> 4.2'
+gem 'sidekiq', '~> 4.2.7'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index c464ff70587..3de1a7cbf26 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -126,7 +126,7 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.2)
- connection_pool (2.2.0)
+ connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
@@ -648,10 +648,10 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (4.2.1)
+ sidekiq (4.2.7)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
- rack-protection (~> 1.5)
+ rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
@@ -928,7 +928,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 4.2)
+ sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 3f29826fa9b..600bac8834a 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -3,7 +3,7 @@
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
- new ImageFile(file);
+ new gl.ImageFile(file);
}
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 4c2ae595319..fd8910e916f 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
(function() {
- this.ImageFile = (function() {
+ gl.ImageFile = (function() {
var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 1cc34e490c2..efa228a75d9 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -6,7 +6,7 @@
var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied!');
+ showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
@@ -31,7 +31,7 @@
var originalTitle = $target.data('original-title');
$target
- .attr('title', 'Copied!')
+ .attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
index ecf9d1de81c..9cf33e62958 100644
--- a/app/assets/javascripts/diff.js.es6
+++ b/app/assets/javascripts/diff.js.es6
@@ -5,8 +5,11 @@
class Diff {
constructor() {
- $('.files .diff-file').singleFileDiff();
- $('.files .diff-file').filesCommentButton();
+ const $diffFile = $('.files .diff-file');
+ $diffFile.singleFileDiff();
+ $diffFile.filesCommentButton();
+
+ $diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 6f9d6283071..2f3da745119 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -52,6 +52,10 @@
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
},
beforeInsert: function(value) {
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ var withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
+ }
if (!GitLab.GfmAutoComplete.dataLoaded) {
return this.at;
} else {
@@ -117,6 +121,7 @@
insertTpl: ':${name}:',
data: ['loading'],
startWithSpace: false,
+ skipSpecialCharacterTest: true,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
@@ -141,6 +146,7 @@
data: ['loading'],
startWithSpace: false,
alwaysHighlightFirst: true,
+ skipSpecialCharacterTest: true,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
@@ -219,12 +225,13 @@
}
};
})(this),
- insertTpl: '${atwho-at}"${title}"',
+ insertTpl: '${atwho-at}${title}',
data: ['loading'],
startWithSpace: false,
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
@@ -284,18 +291,11 @@
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(merges) {
- var sanitizeLabelTitle;
- sanitizeLabelTitle = function(title) {
- if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
- return "\"" + (sanitize(title)) + "\"";
- } else {
- return sanitize(title);
- }
- };
return $.map(merges, function(m) {
return {
- title: sanitizeLabelTitle(m.title),
+ title: sanitize(m.title),
color: m.color,
search: "" + m.title
};
@@ -308,6 +308,7 @@
at: '/',
alias: 'commands',
searchKey: 'search',
+ skipSpecialCharacterTest: true,
displayTpl: function(value) {
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 051ff98c774..1982f4af939 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -2,7 +2,7 @@
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
- var by_author, by_email, data, entry, i, len, total;
+ var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {};
by_author = {};
by_email = {};
@@ -11,7 +11,8 @@
if (total[entry.date] == null) {
this.add_date(entry.date, total);
}
- data = by_author[entry.author_name] || by_email[entry.author_email];
+ normalized_email = entry.author_email.toLowerCase();
+ data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
@@ -32,12 +33,14 @@
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
- var data;
+ var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
+ normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
- return by_email[author.author_email] = data;
+ by_email[normalized_email] = data;
+ return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index d9495e50388..7022aa1263b 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -40,19 +40,26 @@
$('#modal_merge_info').modal({
show: false
});
- this.firstCICheck = true;
- this.readyForCICheck = false;
- this.readyForCIEnvironmentCheck = false;
- this.cancel = false;
- clearInterval(this.fetchBuildStatusInterval);
- clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners();
this.addEventListeners();
this.getCIStatus(false);
- this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon();
- this.pollCIStatus();
- this.pollCIEnvironmentsStatus();
+
+ this.ciStatusInterval = new global.SmartInterval({
+ callback: this.getCIStatus.bind(this, true),
+ startingInterval: 10000,
+ maxInterval: 30000,
+ hiddenInterval: 120000,
+ incrementByFactorOf: 5000,
+ });
+ this.ciEnvironmentStatusInterval = new global.SmartInterval({
+ callback: this.getCIEnvironmentsStatus.bind(this),
+ startingInterval: 30000,
+ maxInterval: 120000,
+ hiddenInterval: 240000,
+ incrementByFactorOf: 15000,
+ immediateExecution: true,
+ });
notifyPermissions();
}
@@ -60,10 +67,6 @@
return $(document).off('page:change.merge_request');
};
- MergeRequestWidget.prototype.cancelPolling = function() {
- return this.cancel = true;
- };
-
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
@@ -72,9 +75,6 @@
var page;
page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) {
- clearInterval(_this.fetchBuildStatusInterval);
- clearInterval(_this.fetchBuildEnvironmentStatusInterval);
- _this.cancelPolling();
return _this.clearEventListeners();
}
};
@@ -114,6 +114,11 @@
});
};
+ MergeRequestWidget.prototype.cancelPolling = function () {
+ this.ciStatusInterval.cancel();
+ this.ciEnvironmentStatusInterval.cancel();
+ };
+
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data);
@@ -131,18 +136,6 @@
}
};
- MergeRequestWidget.prototype.pollCIStatus = function() {
- return this.fetchBuildStatusInterval = setInterval(((function(_this) {
- return function() {
- if (!_this.readyForCICheck) {
- return;
- }
- _this.getCIStatus(true);
- return _this.readyForCICheck = false;
- };
- })(this)), 10000);
- };
-
MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this;
_this = this;
@@ -150,23 +143,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
var message, status, title;
- if (_this.cancel) {
- return;
- }
- _this.readyForCICheck = true;
if (data.status === '') {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
+ if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
- // The first check should only update the UI, a notification
- // should only be displayed on status changes
- if (showNotification && !_this.firstCICheck) {
+ if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
title = _this.opts.ci_title.preparing;
@@ -184,24 +171,13 @@
return Turbolinks.visit(_this.opts.builds_path);
});
}
- return _this.firstCICheck = false;
}
};
})(this));
};
- MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
- this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
- if (!this.readyForCIEnvironmentCheck) return;
- this.getCIEnvironmentsStatus();
- this.readyForCIEnvironmentCheck = false;
- }, 300000);
- };
-
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => {
- if (this.cancel) return;
- this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments);
});
};
@@ -212,11 +188,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
-
+
if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove();
}
-
+
if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else {
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index 72c6c4a1fcd..fb95648e1c7 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -4,7 +4,7 @@
((global) => {
class Pipelines {
- constructor(options) {
+ constructor(options = {}) {
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
@@ -14,9 +14,11 @@
}
addMarginToBuildColumns() {
- this.pipelineGraph = document.querySelector('.pipeline-graph');
- const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
- for (buildNodeIndex in secondChildBuildNodes) {
+ this.pipelineGraph = document.querySelector('.js-pipeline-graph');
+
+ const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+
+ for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
@@ -28,6 +30,7 @@
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
+
this.pipelineGraph.classList.remove('hidden');
}
}
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6
index 5eb15dba79b..40f67637c7c 100644
--- a/app/assets/javascripts/smart_interval.js.es6
+++ b/app/assets/javascripts/smart_interval.js.es6
@@ -7,24 +7,31 @@
(() => {
class SmartInterval {
/**
- * @param { function } callback Function to be called on each iteration (required)
- * @param { milliseconds } startingInterval `currentInterval` is set to this initially
- * @param { milliseconds } maxInterval `currentInterval` will be incremented to this
- * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
- * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
+ * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
+ * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
+ * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
+ * when the page is hidden
+ * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
+ * @param { boolean } opts.lazyStart Configure if timer is initialized on
+ * instantiation or lazily
+ * @param { boolean } opts.immediateExecution Configure if callback should
+ * be executed before the first interval.
*/
- constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) {
+ constructor(opts = {}) {
this.cfg = {
- callback,
- startingInterval,
- maxInterval,
- incrementByFactorOf,
- lazyStart,
+ callback: opts.callback,
+ startingInterval: opts.startingInterval,
+ maxInterval: opts.maxInterval,
+ hiddenInterval: opts.hiddenInterval,
+ incrementByFactorOf: opts.incrementByFactorOf,
+ lazyStart: opts.lazyStart,
+ immediateExecution: opts.immediateExecution,
};
this.state = {
intervalId: null,
- currentInterval: startingInterval,
+ currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible',
};
@@ -36,6 +43,11 @@
const cfg = this.cfg;
const state = this.state;
+ if (cfg.immediateExecution) {
+ cfg.immediateExecution = false;
+ cfg.callback();
+ }
+
state.intervalId = window.setInterval(() => {
cfg.callback();
@@ -54,14 +66,29 @@
this.stopTimer();
}
+ onVisibilityHidden() {
+ if (this.cfg.hiddenInterval) {
+ this.setCurrentInterval(this.cfg.hiddenInterval);
+ this.resume();
+ } else {
+ this.cancel();
+ }
+ }
+
// start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
+ onVisibilityVisible() {
+ this.cancel();
+ this.start();
+ }
+
destroy() {
this.cancel();
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload');
}
@@ -80,11 +107,7 @@
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
- $(document)
- .off('visibilitychange').on('visibilitychange', (e) => {
- this.state.pageVisibility = e.target.visibilityState;
- this.handleVisibilityChange();
- });
+ document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
initPageUnloadHandling() {
@@ -92,10 +115,11 @@
$(document).on('page:before-unload', () => this.cancel());
}
- handleVisibilityChange() {
- const state = this.state;
-
- const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;
+ handleVisibilityChange(e) {
+ this.state.pageVisibility = e.target.visibilityState;
+ const intervalAction = this.isPageVisible() ?
+ this.onVisibilityVisible :
+ this.onVisibilityHidden;
intervalAction.apply(this);
}
@@ -111,6 +135,7 @@
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
+ if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) {
@@ -120,6 +145,8 @@
this.setCurrentInterval(nextInterval);
}
+ isPageVisible() { return this.state.pageVisibility === 'visible'; }
+
stopTimer() {
const state = this.state;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 18716813c48..a1d5f6427f4 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -486,6 +486,7 @@ $jq-ui-default-color: #777;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
+$label-border-radius: 14px;
/*
* Lint
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index de3d2ba549f..e716f24c8e2 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,6 +1,8 @@
-.deployments-container {
- width: 100%;
- overflow: auto;
+@media (max-width: $screen-md-max) {
+ .deployments-container {
+ width: 100%;
+ overflow: auto;
+ }
}
.environments-list-loading {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 90587b9425b..407c0afbac8 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -30,6 +30,7 @@
.color-label {
padding: 6px 10px;
+ border-radius: $label-border-radius;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index b1ccd644450..25c91203ff4 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -104,7 +104,8 @@
}
.color-label {
- padding: 3px 4px;
+ padding: 3px 7px;
+ border-radius: $label-border-radius;
}
.dropdown-labels-error {
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 94fbbef3c77..7d61390a439 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -1,5 +1,9 @@
.notification-list-item {
line-height: 34px;
+
+ .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
}
.notification {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 08062b85504..6822f916cc5 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -37,12 +37,13 @@
}
}
-.content-list {
-
- &.pipelines,
- &.builds-content-list {
- width: 100%;
- overflow: auto;
+@media (max-width: $screen-md-max) {
+ .content-list {
+ &.pipelines,
+ &.builds-content-list {
+ width: 100%;
+ overflow: auto;
+ }
}
}
@@ -666,10 +667,6 @@
min-width: 900px;
}
- .content-list.pipelines {
- overflow: auto;
- }
-
.stage {
max-width: 100px;
width: 100px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 72b6685d940..6e0f6b1cd81 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -188,6 +188,10 @@
margin-left: 10px;
}
+ .notification-dropdown .dropdown-menu {
+ @extend .dropdown-menu-align-right;
+ }
+
.download-button {
@media (max-width: $screen-md-max) {
margin-left: 0;
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 857eb76131a..ff13b86acf0 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -1,3 +1,13 @@
+.snippet-row {
+ .title {
+ margin-bottom: 2px;
+ }
+
+ .snippet-filename {
+ padding: 0 2px;
+ }
+}
+
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
@@ -24,11 +34,17 @@
padding-bottom: $gl-padding;
}
+.snippet-header {
+ padding: $gl-padding 0;
+}
+
.snippet-title {
font-size: 24px;
font-weight: 600;
- padding: $gl-padding;
- padding-left: 0;
+}
+
+.snippet-edited-ago {
+ color: $gray-darkest;
}
.snippet-actions {
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 0825a4311cb..2c097cb4d8d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def update
- release.update_attributes(release_params)
+ # Release belongs to Tag which is not active record object,
+ # it exists only to save a description to each Tag.
+ # If description is empty we should destroy the existing record.
+ if release_params[:description].present?
+ release.update_attributes(release_params)
+ else
+ release.destroy
+ end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index e290a0eadda..0720be2e55d 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(current_user, {
+ @snippets = SnippetsFinder.new.execute(
+ current_user,
filter: :by_project,
- project: @project
- })
+ project: @project,
+ scope: params[:scope]
+ )
@snippets = @snippets.page(params[:page])
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 38e7c6f4a48..8c698695202 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
end
end
+ def destroy
+ super
+ # hide the signed_out notice
+ flash[:notice] = nil
+ end
+
private
# Handle an "initial setup" state, where there's only one user, it's an admin,
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 00ff1611039..da6e6e87a6f 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,14 +1,17 @@
class SnippetsFinder
def execute(current_user, params = {})
filter = params[:filter]
+ user = params.fetch(:user, current_user)
case filter
when :all then
snippets(current_user).fresh
+ when :public then
+ Snippet.are_public.fresh
when :by_user then
- by_user(current_user, params[:user], params[:scope])
+ by_user(current_user, user, params[:scope])
when :by_project
- by_project(current_user, params[:project])
+ by_project(current_user, params[:project], params[:scope])
end
end
@@ -29,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope)
snippets = user.snippets.fresh
- return snippets.are_public unless current_user
-
- if user == current_user
- case scope
- when 'are_internal' then
- snippets.are_internal
- when 'are_private' then
- snippets.are_private
- when 'are_public' then
- snippets.are_public
- else
- snippets
- end
+ if current_user
+ include_private = user == current_user
+ by_scope(snippets, scope, include_private)
else
- snippets.public_and_internal
+ snippets.are_public
end
end
- def by_project(current_user, project)
+ def by_project(current_user, project, scope)
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user) || current_user.admin?
- snippets
- else
- snippets.public_and_internal
- end
+ include_private = project.team.member?(current_user) || current_user.admin?
+ by_scope(snippets, scope, include_private)
else
snippets.are_public
end
end
+
+ def by_scope(snippets, scope = nil, include_private = false)
+ case scope.to_s
+ when 'are_private'
+ include_private ? snippets.are_private : Snippet.none
+ when 'are_internal'
+ snippets.are_internal
+ when 'are_public'
+ snippets.are_public
+ else
+ include_private ? snippets : snippets.public_and_internal
+ end
+ end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index dee3c78df47..4c7c16d694c 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
- title = data[:title] || 'Copy to Clipboard'
+ title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8e19752a8a1..d9f5e01f0dc 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -4,25 +4,7 @@ module CiStatusHelper
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
end
- def ci_status_with_icon(status, target = nil)
- content = ci_icon_for_status(status) + ci_text_for_status(status)
- klass = "ci-status ci-#{status}"
-
- if target
- link_to content, target, class: klass
- else
- content_tag :span, content, class: klass
- end
- end
-
- def ci_text_for_status(status)
- if detailed_status?(status)
- status.text
- else
- status
- end
- end
-
+ # Is used by Commit and Merge Request Widget
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 27975b7ddb7..ff8550439d0 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -14,10 +14,12 @@ module EnvironmentHelper
end
end
- def deployment_link(deployment)
+ def deployment_link(deployment, text: nil)
return unless deployment
- link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ link_label = text ? text : "##{deployment.iid}"
+
+ link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
end
def last_deployment_link_for_environment_build(project, build)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index af9087d8326..99db73c9ee0 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
+ # Snippets
+ def personal_snippet_url(snippet, *args)
+ snippet_url(snippet)
+ end
+
# Groups
## Members
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 7e33a562077..8c02b4061ca 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -8,6 +8,17 @@ module SnippetsHelper
end
end
+ # Return the path of a snippets index for a user or for a project
+ #
+ # @returns String, path to snippet index
+ def subject_snippets_path(subject = nil, opts = nil)
+ if subject.is_a?(Project)
+ namespace_project_snippets_path(subject.namespace, subject, opts)
+ else # assume subject === User
+ dashboard_snippets_path(opts)
+ end
+ end
+
# Get an array of line numbers surrounding a matching
# line, bounded by min/max.
#
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 88c46076df6..e7cf606a7ae 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -100,6 +100,12 @@ module Ci
end
end
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Build::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
+
def manual?
self.when == 'manual'
end
@@ -123,8 +129,13 @@ module Ci
end
end
+ def cancelable?
+ active?
+ end
+
def retryable?
- project.builds_enabled? && commands.present? && complete?
+ project.builds_enabled? && commands.present? &&
+ (success? || failed? || canceled?)
end
def retried?
@@ -148,7 +159,7 @@ module Ci
end
def environment_action
- self.options.fetch(:environment, {}).fetch(:action, 'start')
+ self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
end
def outdated_deployment?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fda8228a1e9..54f73171fd4 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -336,8 +336,10 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end
- def detailed_status
- Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate!
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Pipeline::Factory
+ .new(self, current_user)
+ .fabricate!
end
private
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d2a37c0a827..7ef59445d77 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -22,8 +22,10 @@ module Ci
@status ||= statuses.latest.status
end
- def detailed_status
- Gitlab::Ci::Status::Stage::Factory.new(self).fabricate!
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Stage::Factory
+ .new(self, current_user)
+ .fabricate!
end
def statuses
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cf90475f4d4..31cd381dcd2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -131,4 +131,10 @@ class CommitStatus < ActiveRecord::Base
def has_trace?
false
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index d36bb9da296..1108a64c59e 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -7,6 +7,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy
validates_associated :route
+ validates :route, presence: true
before_validation :update_route_path, if: :full_path_changed?
end
@@ -28,17 +29,17 @@ module Routable
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
- where_paths_in([path]).reorder(order_sql).take
+ where_full_path_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple objects by their full paths.
#
# Usage:
#
- # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
+ # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
#
# Returns an ActiveRecord::Relation.
- def where_paths_in(paths)
+ def where_full_path_in(paths)
wheres = []
cast_lower = Gitlab::Database.postgresql?
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 86a06321e21..fe6d7aabb22 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -3,7 +3,8 @@ require "addressable/uri"
class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com"
- prop_accessor :project_url, :token, :enable_ssl_verification
+ prop_accessor :project_url, :token
+ boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 5e4dd101c53..adc78a427ee 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,5 +1,6 @@
class DroneCiService < CiService
- prop_accessor :drone_url, :token, :enable_ssl_verification
+ prop_accessor :drone_url, :token
+ boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index e0083c43adb..79285cbd26d 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -1,6 +1,6 @@
class EmailsOnPushService < Service
- prop_accessor :send_from_committer_email
- prop_accessor :disable_diffs
+ boolean_accessor :send_from_committer_email
+ boolean_accessor :disable_diffs
prop_accessor :recipients
validates :recipients, presence: true, if: :activated?
@@ -24,20 +24,20 @@ class EmailsOnPushService < Service
return unless supported_events.include?(push_data[:object_kind])
EmailsOnPushWorker.perform_async(
- project_id,
- recipients,
- push_data,
- send_from_committer_email: send_from_committer_email?,
- disable_diffs: disable_diffs?
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
)
end
def send_from_committer_email?
- self.send_from_committer_email == "1"
+ Gitlab::Utils.to_boolean(self.send_from_committer_email)
end
def disable_diffs?
- self.disable_diffs == "1"
+ Gitlab::Utils.to_boolean(self.disable_diffs)
end
def fields
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 660a8ae3421..915f6fed74c 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd
]
- prop_accessor :token, :room, :server, :notify, :color, :api_version
- boolean_accessor :notify_only_broken_builds
+ prop_accessor :token, :room, :server, :color, :api_version
+ boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
@@ -75,7 +75,7 @@ class HipchatService < Service
end
def message_options(data = nil)
- { notify: notify.present? && notify == '1', color: message_color(data) }
+ { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end
def create_message(data)
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index ce7d1c5d5b1..7355918feab 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
- prop_accessor :colorize_messages, :recipients, :channels
+ prop_accessor :recipients, :channels
+ boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated?
before_validation :get_channels
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 894315a8593..2d969d2fcb6 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
diff --git a/app/models/user.rb b/app/models/user.rb
index b9bb4a9e3f7..1bd28203523 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -304,10 +304,6 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token
end
- def by_username_or_id(name_or_id)
- find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
- end
-
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index 62335527654..5a3fe814b77 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy
elsif @user == target_user
can! :destroy_group_member
end
+
+ additional_rules!
+ end
+
+ def additional_rules!
+ # This is meant to be overriden in EE
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index b65fb68cd88..6f943feb2a7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy
if globally_viewable && @subject.request_access_enabled && !member
can! :request_access
end
+
+ additional_rules!(master)
end
def can_read_group?
@@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(@subject).execute(@user).any?
end
+
+ def additional_rules!(master)
+ # This is meant to be overriden in EE
+ end
end
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 46c5aa1a5be..d3913986cd8 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
+ can! :destroy_personal_snippet
can! :admin_personal_snippet
end
+ unless @user.external?
+ can! :create_personal_snippet
+ end
+
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index db5f2bf9b2e..4d410f66c55 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -35,7 +35,7 @@ module Commits
success
else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
+ A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index ec40391a3e3..b5f96363230 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -8,7 +8,7 @@
%span
Overview
= nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
+ = link_to admin_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e51f4ac1d93..5238623e936 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -116,7 +116,7 @@
.light-well.well-centered
%h4 Projects
.data
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
%h1= number_with_delimiter(Project.cached_count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 664bb417c6a..4efeec0ea4e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -2,7 +2,7 @@
%li.group-row{ class: css_class }
.controls
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+ = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
%span
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 40871e32913..71a605f33b1 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -2,7 +2,7 @@
%h3.page-title
Group: #{@group.name}
- = link_to edit_admin_group_path(@group), class: "btn pull-right" do
+ = link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -88,7 +88,7 @@
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
- = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
+ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
%div.prepend-top-10
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index b37b8d4fee7..8bc7dc7dd51 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,7 +7,7 @@
%div{ class: container_class }
.top-area
.prepend-top-default
- = form_tag admin_namespaces_projects_path, method: :get do |f|
+ = form_tag admin_projects_path, method: :get do |f|
.search-holder
.search-field-holder
= search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
@@ -41,19 +41,19 @@
= button_tag "Search", class: "btn btn-primary btn-search"
%ul.nav-links
- - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
+ - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
- = link_to admin_namespaces_projects_path do
+ = link_to admin_projects_path do
All
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
- = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
+ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
.nav-controls
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 73038164056..ca503e35623 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -91,7 +91,7 @@
%strong ##{build.id}
%td.status
- = ci_status_with_icon(build.status)
+ = render 'ci/status/badge', status: build.detailed_status(current_user)
%td.status
- if project
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
new file mode 100644
index 00000000000..f2135af2686
--- /dev/null
+++ b/app/views/ci/status/_badge.html.haml
@@ -0,0 +1,10 @@
+- status = local_assigns.fetch(:status)
+
+- if status.has_details?
+ = link_to status.details_path, class: "ci-status ci-#{status}" do
+ = custom_icon(status.icon)
+ = status.text
+- else
+ %span{ class: "ci-status ci-#{status}" }
+ = custom_icon(status.icon)
+ = status.text
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index b25e8ea1f0c..02e90bbfa55 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,7 +1,13 @@
-%ul.nav-links
- = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
- Your Snippets
- = nav_link(page: explore_snippets_path) do
- = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
- Explore Snippets
+.top-area
+ %ul.nav-links
+ = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
+ = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ Your Snippets
+ = nav_link(page: explore_snippets_path) do
+ = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
+ Explore Snippets
+
+ - if current_user
+ .nav-controls.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
+ New snippet
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index b2af438ea57..85cbe0bf0e6 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head'
+= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.nav-block
- .controls.hidden-xs
- = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
- = icon('plus')
- New snippet
+.visible-xs
+ &nbsp;
+ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
- .nav-links.snippet-scope-menu
- %li{ class: ("active" unless params[:scope]) }
- = link_to dashboard_snippets_path do
- All
- %span.badge
- = current_user.snippets.count
-
- %li{ class: ("active" if params[:scope] == "are_private") }
- = link_to dashboard_snippets_path(scope: 'are_private') do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- %li{ class: ("active" if params[:scope] == "are_internal") }
- = link_to dashboard_snippets_path(scope: 'are_internal') do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- %li{ class: ("active" if params[:scope] == "are_public") }
- = link_to dashboard_snippets_path(scope: 'are_public') do
- Public
- %span.badge
- = current_user.snippets.are_public.count
-
- .visible-xs
- = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
- = icon('plus')
- New snippet
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7def9eacdc9..e5706d04736 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -6,12 +6,4 @@
- else
= render 'explore/head'
-.row-content-block
- - if current_user
- = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
-
- .oneline
- Public snippets created by you and other users are listed here
-
-= render 'snippets/snippets'
+= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 324a116a50e..b4aa4f24d9e 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -6,13 +6,13 @@
- if group_issues(@group).exists?
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls
- - if current_user
+ - if current_user
+ .nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index e6953d94531..dbbdb583a24 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -2,8 +2,9 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
+ - if current_user
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 844fce59704..d79a1a9f368 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -30,7 +30,7 @@
%br
.clearfix
.form-group.pull-left.global-notification-setting
- = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index f6aa20c4579..057a720a54a 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,6 +1,6 @@
.content-block.build-header
.header-content
- = ci_status_with_icon(@build.status)
+ = render 'ci/status/badge', status: @build.detailed_status(current_user)
Build
%strong ##{@build.id}
in pipeline
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 108674dbba6..cdeb81372ee 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -46,8 +46,7 @@
- else
This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
- and will overwrite the
- = link_to 'latest deployment', deployment_link(environment.last_deployment)
+ and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
.prepend-top-default
- if @build.erased?
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 18b3b04154f..f1cb0201032 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -9,10 +9,7 @@
%tr.build.commit{class: ('retried' if retried)}
%td.status
- - if can?(current_user, :read_build, build)
- = ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
- - else
- = ci_status_with_icon(build.status)
+ = render "ci/status/badge", status: build.detailed_status(current_user)
%td.branch-commit
- if can?(current_user, :read_build, build)
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 423a1282eb2..ad1a7360a8b 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,10 +1,10 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do
= ci_icon_for_status('play')
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
.ci-status-text= subject.name
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index b58dceb58c9..3f05a21990f 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,13 +1,10 @@
- status = pipeline.status
-- detailed_status = pipeline.detailed_status
- show_commit = local_assigns.fetch(:show_commit, true)
- show_branch = local_assigns.fetch(:show_branch, true)
%tr.commit
%td.commit-link
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do
- = ci_icon_for_status(detailed_status)
- = ci_text_for_status(detailed_status)
+ = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index f6e3d5e76f5..782f558e8b0 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -13,7 +13,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
- = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
+ = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
@@ -23,12 +23,11 @@
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
- - nonce = SecureRandom.hex
- = label_tag "create_merge_request-#{nonce}" do
- = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ = label_tag do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
Start a <strong>new merge request</strong> with these changes
- else
- = hidden_field_tag 'create_merge_request', 1
+ = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 65151ac3a56..c08ed8f6c16 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,13 +1,8 @@
.page-content-header
.header-main-content
- %strong Commit
- %strong.monospace.js-details-short= @commit.short_id
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
- %span.text-expander
- \...
- %span.js-details-content.hide
- %strong.monospace.commit-hash-full= @commit.id
+ %strong
= clipboard_button(clipboard_text: @commit.id)
+ = @commit.short_id
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span by
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index c7b5c1124b3..08d3443b3d0 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -24,7 +24,7 @@
in
= time_interval_in_words pipeline.duration
- .row-content-block.build-content.middle-block.hidden
+ .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
= render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 7f751d9ae2e..9f444f076c0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -8,10 +8,7 @@
%tr.generic_commit_status{class: ('retried' if retried)}
%td.status
- - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
- = ci_status_with_icon(generic_commit_status.status, generic_commit_status.target_url)
- - else
- = ci_status_with_icon(generic_commit_status.status)
+ = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
%td.generic_commit_status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 7b82d913d29..1bba0443154 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,4 +1,4 @@
-%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
+%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } }
- if subject.target_url
= link_to subject.target_url do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index ba8895438c5..778a32e6345 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -65,7 +65,7 @@
.note-text.md
= preserve do
= note.redacted_note_html
- = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
.note-awards
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 229bdfb0e8d..b00ba2d5307 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,6 +1,6 @@
.page-content-header
.header-main-content
- = ci_status_with_icon(@pipeline.detailed_status)
+ = render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
%strong Pipeline ##{@commit.pipelines.last.id}
triggered #{time_ago_with_tooltip(@commit.authored_date)} by
= author_avatar(@commit, size: 24)
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 739e5930822..88af41aa835 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -12,7 +12,7 @@
.tab-content
#js-tab-pipeline.tab-pane
- .build-content.middle-block
+ .build-content.middle-block.js-pipeline-graph
= render "projects/pipelines/graph", pipeline: pipeline
#js-tab-builds.tab-pane
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 32e1f8a21b0..068a6610350 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
- New snippet
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :update_project_snippet, @snippet)
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
+ New snippet
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index e77e1b026f6..84e05cd6d88 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,11 +1,19 @@
- page_title "Snippets"
-.sub-header-block
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
- New snippet
+- if current_user
+ .top-area
+ - include_private = @project.team.member?(current_user) || current_user.admin?
+ = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
+
+ .nav-controls.hidden-xs
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
+ New snippet
- .oneline
- Share code pastes with others out of git repository
+- if can?(current_user, :create_project_snippet, @project)
+ .visible-xs
+ &nbsp;
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
+ New snippet
= render 'snippets/snippets'
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
index 1d8fa10db0c..bf8c75b6e5c 100644
--- a/app/views/projects/stage/_graph.html.haml
+++ b/app/views/projects/stage/_graph.html.haml
@@ -19,4 +19,4 @@
%li.build
.curve
.dropdown.inline.build-content
- = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses
+ = render 'projects/stage/in_stage_group', name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 21e378b8735..d37c376c36b 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -5,14 +5,11 @@
%tr
%th Name
%th.hidden-xs
- .pull-left Last Commit
+ .pull-left Last commit
.last-commit.hidden-sm.pull-left
- &nbsp;
- %i.fa.fa-angle-right
- &nbsp;
- %small.light
+ %small.light
+ = clipboard_button(clipboard_text: @commit.id)
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
- &ndash;
= time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title
%small.commit-history-link-spacer &#124;
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index a585147ddd1..94e5a5d9709 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -2,7 +2,7 @@
#{@message}.
%p
- = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
+ = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1)
%p
You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index 93db151329e..0902c50d052 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -1,6 +1,6 @@
#{@message}.
\
-View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
+View details: #{admin_projects_url(last_repository_check_failed: 1)}
You are receiving this message because you are a GitLab administrator
for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index c414acb6a11..027d42396b4 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info
- = "##{snippet_title.id}"
+ = snippet_title.to_reference
%span
by
= link_to user_snippets_path(snippet_title.author) do
diff --git a/app/views/shared/icons/_icon_status_manual.svg b/app/views/shared/icons/_icon_status_manual.svg
new file mode 100755
index 00000000000..c98839f51a9
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></svg>
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 1f7df0bcd19..fbad0d05de3 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,4 +1,3 @@
-- left_align = local_assigns[:left_align]
- if notification_setting
.dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
@@ -19,7 +18,7 @@
= notification_title(notification_setting.level)
= icon("caret-down")
- = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index d3258ee64cb..85ad74f9a39 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -1,5 +1,4 @@
-- left_align = local_assigns[:left_align]
-%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index d7506e07ff6..d084f5e9684 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -8,10 +8,6 @@
%span.creator
authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- - if @snippet.updated_at != @snippet.created_at
- %span
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
@@ -20,5 +16,9 @@
- else
= render "snippets/actions"
-%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown_field(@snippet, :title)
+.snippet-header
+ %h2.snippet-title.prepend-top-0.append-bottom-0
+ = markdown_field(@snippet, :title)
+
+ - if @snippet.updated_at != @snippet.created_at
+ = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago')
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index ea17bec8677..5d2d2317f22 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,17 +1,16 @@
+- link_project = local_assigns.fetch(:link_project, false)
+
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- - if snippet.private?
- %span.label.label-gray.hidden-xs
- = icon('lock')
- private
- %span.monospace.pull-right.hidden-xs
- = snippet.file_name
+ - if snippet.file_name
+ %span.snippet-filename.monospace.hidden-xs
+ = snippet.file_name
- %ul.controls.visible-xs
+ %ul.controls
%li
- note_count = snippet.notes.user.count
= link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
@@ -22,11 +21,17 @@
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level, fw: false)
- %small.pull-right.cgray.hidden-xs
- - if snippet.project_id?
- = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
-
- .snippet-info.hidden-xs
+ .snippet-info
+ #{snippet.to_reference} &middot;
+ authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
+ by
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- authored #{time_ago_with_tooltip(snippet.created_at)}
+ - if link_project && snippet.project_id?
+ %span.hidden-xs
+ in
+ = link_to namespace_project_path(snippet.project.namespace, snippet.project) do
+ = snippet.project.name_with_namespace
+
+ .pull-right.snippet-updated-at
+ %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 1d0e549ed3d..95fc7198104 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
- New snippet
- - if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
- Delete
- if can?(current_user, :update_personal_snippet, @snippet)
- = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
+ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
+ - if can?(current_user, :admin_personal_snippet, @snippet)
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
+ Delete
+ - if current_user
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ New snippet
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 77b66ca74b6..ac3701233ad 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,8 +1,9 @@
- remote = local_assigns.fetch(:remote, false)
+- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
%ul.content-list
- = render partial: 'shared/snippets/snippet', collection: @snippets
+ = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
.nothing-here-block Nothing here.
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
new file mode 100644
index 00000000000..2dda5fed647
--- /dev/null
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -0,0 +1,31 @@
+- subject = local_assigns.fetch(:subject, current_user)
+- include_private = local_assigns.fetch(:include_private, false)
+
+.nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to subject_snippets_path(subject) do
+ All
+ %span.badge
+ - if include_private
+ = subject.snippets.count
+ - else
+ = subject.snippets.public_and_internal.count
+
+ - if include_private
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to subject_snippets_path(subject, scope: 'are_private') do
+ Private
+ %span.badge
+ = subject.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to subject_snippets_path(subject, scope: 'are_internal') do
+ Internal
+ %span.badge
+ = subject.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to subject_snippets_path(subject, scope: 'are_public') do
+ Public
+ %span.badge
+ = subject.snippets.are_public.count
diff --git a/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
new file mode 100644
index 00000000000..742b10e72aa
--- /dev/null
+++ b/changelogs/unreleased/19550-fix-contributer-graph-duplicates.yml
@@ -0,0 +1,4 @@
+---
+title: group authors in contribution graph with case insensitive email handle comparison
+merge_request: 8021
+author:
diff --git a/changelogs/unreleased/20052-actions-table-vscroll.yml b/changelogs/unreleased/20052-actions-table-vscroll.yml
new file mode 100644
index 00000000000..779cd08de09
--- /dev/null
+++ b/changelogs/unreleased/20052-actions-table-vscroll.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent overflow with vertical scroll when we have space to show content
+merge_request: 8061
+author:
diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml
new file mode 100644
index 00000000000..49e6c5e56e5
--- /dev/null
+++ b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml
@@ -0,0 +1,4 @@
+---
+title: Use SmartInterval for MR widget and improve visibilitychange functionality
+merge_request: 7762
+author:
diff --git a/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
new file mode 100644
index 00000000000..b8ba9391530
--- /dev/null
+++ b/changelogs/unreleased/24949-view-2-up-swipe-onion-skin-controls-for-merge-request-diff-containing-an-image.yml
@@ -0,0 +1,4 @@
+---
+title: Add image controls to MR diffs
+merge_request: 7919
+author:
diff --git a/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
new file mode 100644
index 00000000000..62030d3fc45
--- /dev/null
+++ b/changelogs/unreleased/25106-hide-issue-mr-button-for-not-loggedin.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent user creating issue or MR without signing in for a group
+merge_request: 7902
+author:
diff --git a/changelogs/unreleased/25136-last-deployment-link.yml b/changelogs/unreleased/25136-last-deployment-link.yml
new file mode 100644
index 00000000000..eab1534aa66
--- /dev/null
+++ b/changelogs/unreleased/25136-last-deployment-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Latest deployment link is broken
+merge_request: 7839
+author:
diff --git a/changelogs/unreleased/25294-remove-signed-out-msg.yml b/changelogs/unreleased/25294-remove-signed-out-msg.yml
new file mode 100644
index 00000000000..567294fe5f7
--- /dev/null
+++ b/changelogs/unreleased/25294-remove-signed-out-msg.yml
@@ -0,0 +1,4 @@
+---
+title: 'fix: removed signed_out notification'
+merge_request: 7958
+author: jnoortheen
diff --git a/changelogs/unreleased/25482-fix-api-sudo.yml b/changelogs/unreleased/25482-fix-api-sudo.yml
new file mode 100644
index 00000000000..4c11fe1622e
--- /dev/null
+++ b/changelogs/unreleased/25482-fix-api-sudo.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Memoize the current_user so that sudo can work properly'
+merge_request: 8017
+author:
diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml
new file mode 100644
index 00000000000..d6c92014bea
--- /dev/null
+++ b/changelogs/unreleased/25483-broken-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab
+merge_request: 8009
+author:
diff --git a/changelogs/unreleased/allow-more-filenames.yml b/changelogs/unreleased/allow-more-filenames.yml
new file mode 100644
index 00000000000..7989f94e528
--- /dev/null
+++ b/changelogs/unreleased/allow-more-filenames.yml
@@ -0,0 +1,4 @@
+---
+title: Allow all alphanumeric characters in file names
+merge_request: 8002
+author: winniehell
diff --git a/changelogs/unreleased/api-cherry-pick.yml b/changelogs/unreleased/api-cherry-pick.yml
new file mode 100644
index 00000000000..5f4cee450b9
--- /dev/null
+++ b/changelogs/unreleased/api-cherry-pick.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Ability to cherry pick a commit'
+merge_request: 8047
+author: Robert Schilling
diff --git a/changelogs/unreleased/api-simple-group-project.yml b/changelogs/unreleased/api-simple-group-project.yml
new file mode 100644
index 00000000000..54c8de610a6
--- /dev/null
+++ b/changelogs/unreleased/api-simple-group-project.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Simple representation of group''s projects'
+merge_request: 8060
+author: Robert Schilling
diff --git a/changelogs/unreleased/awards_handler.yml b/changelogs/unreleased/awards_handler.yml
new file mode 100644
index 00000000000..1f9904c0691
--- /dev/null
+++ b/changelogs/unreleased/awards_handler.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for awards_handler_spec
+merge_request: 7661
+author: winniehell
diff --git a/changelogs/unreleased/chomp-git-status-message.yml b/changelogs/unreleased/chomp-git-status-message.yml
new file mode 100644
index 00000000000..f70607df7a1
--- /dev/null
+++ b/changelogs/unreleased/chomp-git-status-message.yml
@@ -0,0 +1,5 @@
+---
+title: For single line git commit messages, the close quote should be on the same
+ line as the open quote
+merge_request:
+author:
diff --git a/changelogs/unreleased/features-api-snippets.yml b/changelogs/unreleased/features-api-snippets.yml
new file mode 100644
index 00000000000..80c7bb75359
--- /dev/null
+++ b/changelogs/unreleased/features-api-snippets.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Endpoint to expose personal snippets as /snippets'
+merge_request: 6373
+author: Bernard Guyzmo Pratz
diff --git a/changelogs/unreleased/issue_13270.yml b/changelogs/unreleased/issue_13270.yml
new file mode 100644
index 00000000000..9c15c436876
--- /dev/null
+++ b/changelogs/unreleased/issue_13270.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to delete tag release note
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_25030.yml b/changelogs/unreleased/issue_25030.yml
new file mode 100644
index 00000000000..e18b8d6a79b
--- /dev/null
+++ b/changelogs/unreleased/issue_25030.yml
@@ -0,0 +1,4 @@
+---
+title: Allow branch names with dots on API endpoint
+merge_request:
+author:
diff --git a/changelogs/unreleased/process-commit-worker-migration-encoding.yml b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
new file mode 100644
index 00000000000..26aabd9b647
--- /dev/null
+++ b/changelogs/unreleased/process-commit-worker-migration-encoding.yml
@@ -0,0 +1,4 @@
+---
+title: Encode input when migrating ProcessCommitWorker jobs to prevent migration errors
+merge_request:
+author:
diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml
new file mode 100644
index 00000000000..755b0379a16
--- /dev/null
+++ b/changelogs/unreleased/unescape-relative-path.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid escaping relative links in Markdown twice
+merge_request: 7940
+author: winniehell
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 9ddd1554811..0ee1b1ec634 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -302,7 +302,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
diff --git a/config/routes.rb b/config/routes.rb
index 03b47261e7e..06d565df469 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,5 @@
require 'sidekiq/web'
require 'sidekiq/cron/web'
-require 'api/api'
require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 5ae985da561..0dd2c8f7aef 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -28,9 +28,19 @@ namespace :admin do
resources :applications
- resources :groups, constraints: { id: /[^\/]+/ } do
- member do
+ resources :groups, only: [:index, :new, :create]
+
+ scope(path: 'groups/*id',
+ controller: :groups,
+ constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+
+ scope(as: :group) do
put :members_update
+ get :edit, action: :edit
+ get '/', action: :show
+ patch '/', action: :update
+ put '/', action: :update
+ delete '/', action: :destroy
end
end
@@ -50,14 +60,13 @@ namespace :admin do
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
- resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- root to: 'projects#index', as: :projects
+ resources :projects, only: [:index]
+ scope(path: 'projects/*namespace_id', as: :namespace) do
resources(:projects,
path: '/',
- constraints: { id: /[a-zA-Z.0-9_\-]+/ },
- only: [:index, :show]) do
- root to: 'projects#show'
+ constraints: { id: Gitlab::Regex.project_route_regex },
+ only: [:show]) do
member do
put :transfer
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index 453a44e271a..77e0c40d850 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -47,14 +47,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
hash = {
id: commit.oid,
- message: commit.message,
+ message: encode(commit.message),
parent_ids: commit.parent_ids,
authored_date: commit.author[:time],
- author_name: commit.author[:name],
- author_email: commit.author[:email],
+ author_name: encode(commit.author[:name]),
+ author_email: encode(commit.author[:email]),
committed_date: commit.committer[:time],
- committer_email: commit.committer[:email],
- committer_name: commit.committer[:name]
+ committer_email: encode(commit.committer[:email]),
+ committer_name: encode(commit.committer[:name])
}
payload['args'][2] = hash
@@ -89,4 +89,14 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
end
end
end
+
+ def encode(data)
+ encoding = Encoding::UTF_8
+
+ if data.encoding == encoding
+ data
+ else
+ data.encode(encoding, invalid: :replace, undef: :replace)
+ end
+ end
end
diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
new file mode 100644
index 00000000000..6958500306f
--- /dev/null
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLowerPathIndexToRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));'
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ remove_index :routes, name: :index_on_routes_lower_path
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 08b1590e484..4711b7873af 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: 20161206153754) do
+ActiveRecord::Schema.define(version: 20161212142807) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1290,4 +1290,4 @@ ActiveRecord::Schema.define(version: 20161206153754) 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
+end \ No newline at end of file
diff --git a/doc/README.md b/doc/README.md
index 66c8c26e4f0..eba1e9845b1 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,4 +1,4 @@
-# Documentation
+# GitLab Community Edition documentation
## User documentation
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 0170af00e0e..5c11d0f83bb 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -183,6 +183,44 @@ Example response:
}
```
+## Cherry pick a commit
+
+> [Introduced][ce-8047] in GitLab 8.15.
+
+Cherry picks a commit to a given branch.
+
+```
+POST /projects/:id/repository/commits/:sha/cherry_pick
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
+| `sha` | string | yes | The commit hash |
+| `branch` | string | yes | The name of the branch |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
+```
+
+Example response:
+
+```json
+{
+ "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title": "Feature added",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "created_at": "2016-12-12T20:10:39.000+01:00",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+}
+```
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -438,3 +476,4 @@ Example response:
```
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
+[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 5e6f498c365..134d7bda22f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -50,12 +50,17 @@ GET /groups/:id/projects
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+Example response:
```json
[
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 81df55ab4ab..662cc9da733 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
| `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash
-curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
```
## Accept MR
diff --git a/doc/api/services.md b/doc/api/services.md
index a5d733fe6c7..3dad953cd1e 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,6 +139,43 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
+## Build-Emails
+
+Get emails for GitLab CI builds.
+
+### Create/Edit Build-Emails service
+
+Set Build-Emails service for a project.
+
+```
+PUT /projects/:id/services/builds-email
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
+
+
+### Delete Build-Emails service
+
+Delete Build-Emails service for a project.
+
+```
+DELETE /projects/:id/services/builds-email
+```
+
+### Get Build-Emails service settings
+
+Get Build-Emails service settings for a project.
+
+```
+GET /projects/:id/services/builds-email
+```
+
## Campfire
Simple web-based real-time group chat
@@ -476,12 +513,11 @@ PUT /projects/:id/services/jira
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `active` | boolean| no | Enable/disable the JIRA service. |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
-| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
+| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
@@ -491,6 +527,78 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
+## Mattermost Slash Commands
+
+Ability to receive slash commands from a Mattermost chat instance.
+
+### Create/Edit Mattermost Slash Command service
+
+Set Mattermost Slash Command for a project.
+
+```
+PUT /projects/:id/services/mattermost-slash-commands
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | yes | The Mattermost token |
+
+
+### Delete Mattermost Slash Command service
+
+Delete Mattermost Slash Command service for a project.
+
+```
+DELETE /projects/:id/services/mattermost-slash-commands
+```
+
+### Get Mattermost Slash Command service settings
+
+Get Mattermost Slash Command service settings for a project.
+
+```
+GET /projects/:id/services/mattermost-slash-commands
+```
+
+## Pipeline-Emails
+
+Get emails for GitLab CI pipelines.
+
+### Create/Edit Pipeline-Emails service
+
+Set Pipeline-Emails service for a project.
+
+```
+PUT /projects/:id/services/pipelines-email
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `recipients` | string | yes | Comma-separated list of recipient email addresses |
+| `add_pusher` | boolean | no | Add pusher to recipients list |
+| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
+
+
+### Delete Pipeline-Emails service
+
+Delete Pipeline-Emails service for a project.
+
+```
+DELETE /projects/:id/services/pipelines-email
+```
+
+### Get Pipeline-Emails service settings
+
+Get Pipeline-Emails service settings for a project.
+
+```
+GET /projects/:id/services/pipelines-email
+```
+
## PivotalTracker
Project Management Software (Source Commits Endpoint)
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
new file mode 100644
index 00000000000..5a5dc162ffe
--- /dev/null
+++ b/doc/api/snippets.md
@@ -0,0 +1,232 @@
+# Snippets
+
+> [Introduced][ce-6373] in GitLab 8.15.
+
+### Snippet visibility level
+
+Snippets in GitLab can be either private, internal, or public.
+You can set it with the `visibility_level` field in the snippet.
+
+Constants for snippet visibility levels are:
+
+| Visibility | Visibility level | Description |
+| ---------- | ---------------- | ----------- |
+| Private | `0` | The snippet is visible only to the snippet creator |
+| Internal | `10` | The snippet is visible for any logged in user |
+| Public | `20` | The snippet can be accessed without any authentication |
+
+## List snippets
+
+Get a list of current user's snippets.
+
+```
+GET /snippets
+```
+
+## Single snippet
+
+Get a single snippet.
+
+```
+GET /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "test",
+ "file_name": "add.rb",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "expires_at": null,
+ "updated_at": "2012-06-28T10:52:04Z",
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Create new snippet
+
+Creates a new snippet. The user must have permission to create new snippets.
+
+```
+POST /snippets
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `title` | String | yes | The title of a snippet |
+| `file_name` | String | yes | The name of a snippet file |
+| `content` | String | yes | The content of a snippet |
+| `visibility_level` | Integer | yes | The snippet's visibility |
+
+
+``` bash
+curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "This is a snippet",
+ "file_name": "test.txt",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "expires_at": null,
+ "updated_at": "2012-06-28T10:52:04Z",
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Update snippet
+
+Updates an existing snippet. The user must have permission to change an existing snippet.
+
+```
+PUT /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+| `title` | String | no | The title of a snippet |
+| `file_name` | String | no | The name of a snippet file |
+| `content` | String | no | The content of a snippet |
+| `visibility_level` | Integer | no | The snippet's visibility |
+
+
+``` bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
+```
+
+Example response:
+
+``` json
+{
+ "id": 1,
+ "title": "test",
+ "file_name": "add.rb",
+ "author": {
+ "id": 1,
+ "username": "john_smith",
+ "email": "john@example.com",
+ "name": "John Smith",
+ "state": "active",
+ "created_at": "2012-05-23T08:00:58Z"
+ },
+ "expires_at": null,
+ "updated_at": "2012-06-28T10:52:04Z",
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/snippets/1",
+}
+```
+
+## Delete snippet
+
+Deletes an existing snippet.
+
+```
+DELETE /snippets/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+
+```
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
+```
+
+upon successful delete a `204 No content` HTTP code shall be expected, with no data,
+but if the snippet is non-existent, a `404 Not Found` will be returned.
+
+## Explore all public snippets
+
+```
+GET /snippets/public
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `per_page` | Integer | no | number of snippets to return per page |
+| `page` | Integer | no | the page to retrieve |
+
+``` bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
+```
+
+Example response:
+
+``` json
+[
+ {
+ "author": {
+ "avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
+ "id": 12,
+ "name": "Libby Rolfson",
+ "state": "active",
+ "username": "elton_wehner",
+ "web_url": "http://localhost:3000/elton_wehner"
+ },
+ "created_at": "2016-11-25T16:53:34.504Z",
+ "file_name": "oconnerrice.rb",
+ "id": 49,
+ "raw_url": "http://localhost:3000/snippets/49/raw",
+ "title": "Ratione cupiditate et laborum temporibus.",
+ "updated_at": "2016-11-25T16:53:34.504Z",
+ "web_url": "http://localhost:3000/snippets/49"
+ },
+ {
+ "author": {
+ "avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
+ "id": 16,
+ "name": "Llewellyn Flatley",
+ "state": "active",
+ "username": "adaline",
+ "web_url": "http://localhost:3000/adaline"
+ },
+ "created_at": "2016-11-25T16:53:34.479Z",
+ "file_name": "muellershields.rb",
+ "id": 48,
+ "raw_url": "http://localhost:3000/snippets/48/raw",
+ "title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
+ "updated_at": "2016-11-25T16:53:34.479Z",
+ "web_url": "http://localhost:3000/snippets/48"
+ }
+]
+```
+
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index a66165dc973..c679ea4e298 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an
also dynamically URL.
The details of the Review Apps implementation depend widely on your real
-technology stack and on your deployment process. The simplest case it to
+technology stack and on your deployment process. The simplest case is to
deploy a simple static HTML website, but it will not be that straightforward
when your app is using a database for example. To make a branch be deployed
on a temporary instance and booting up this instance with all required software
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index a0e895773ce..c64d3407461 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-14-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.0
+sudo -u git -H git checkout v4.0.3
```
### 6. Update gitlab-workhorse
diff --git a/features/admin/settings.feature b/features/admin/settings.feature
deleted file mode 100644
index e38eea2cfed..00000000000
--- a/features/admin/settings.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-@admin
-Feature: Admin Settings
- Background:
- Given I sign in as an admin
- And I visit admin settings page
-
- Scenario: Change application settings
- When I modify settings and save form
- Then I should see application settings saved
-
- Scenario: Change Slack Service Template settings
- When I click on "Service Templates"
- And I click on "Slack" service
- And I fill out Slack settings
- Then I check all events and submit form
- And I should see service template settings saved
- Then I click on "Slack" service
- And I should see all checkboxes checked
- And I should see Slack settings saved
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index 5e7d539add6..a3bebfa4b71 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New snippet"' do
- click_link "New snippet"
+ first(:link, "New snippet").click
end
step 'I click link "Snippet one"' do
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 2bd8ea745e4..82c07d4f536 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -168,7 +168,7 @@ module SharedPaths
end
step 'I visit admin projects page' do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
end
step 'I visit admin users page' do
@@ -203,10 +203,6 @@ module SharedPaths
visit admin_teams_path
end
- step 'I visit admin settings page' do
- visit admin_application_settings_path
- end
-
step 'I visit spam logs page' do
visit admin_spam_logs_path
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 67109ceeef9..cec2702e44d 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -64,6 +64,7 @@ module API
mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
+ mount ::API::Snippets
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 73aed624ea7..0950c3d2e88 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -23,9 +23,9 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- get ':id/repository/branches/:branch' do
+ get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
@@ -39,11 +39,11 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
end
- put ':id/repository/branches/:branch/protect' do
+ put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
@@ -76,9 +76,9 @@ module API
success Entities::RepoBranch
end
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- put ':id/repository/branches/:branch/unprotect' do
+ put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
@@ -112,9 +112,9 @@ module API
desc 'Delete a branch'
params do
- requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ requires :branch, type: String, desc: 'The name of the branch'
end
- delete ":id/repository/branches/:branch" do
+ delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 2670a2d413a..cf2489dbb67 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -1,7 +1,6 @@
require 'mime/types'
module API
- # Projects commits API
class Commits < Grape::API
include PaginationParams
@@ -121,6 +120,41 @@ module API
present paginate(notes), with: Entities::CommitNote
end
+ desc 'Cherry pick commit into a branch' do
+ detail 'This feature was introduced in GitLab 8.15'
+ success Entities::RepoCommit
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha to be cherry picked'
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ post ':id/repository/commits/:sha/cherry_pick' do
+ authorize! :push_code, user_project
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ commit_params = {
+ commit: commit,
+ create_merge_request: false,
+ source_project: user_project,
+ source_branch: commit.cherry_pick_branch_name,
+ target_branch: params[:branch]
+ }
+
+ result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
+
+ if result[:status] == :success
+ branch = user_project.repository.find_branch(params[:branch])
+ present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
desc 'Post comment to commit' do
success Entities::CommitNote
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 006d5f9f44e..01c0f5072ba 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -201,6 +201,19 @@ module API
end
end
+ class PersonalSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: Entities::UserBasic
+ expose :updated_at, :created_at
+
+ expose :web_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ expose :raw_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet) + "/raw"
+ end
+ end
+
class ProjectEntity < Grape::Entity
expose :id, :iid
expose(:project_id) { |entity| entity.project.id }
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index fbf7513302b..9b9d3df7435 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,7 +1,7 @@
module API
class Groups < Grape::API
include PaginationParams
-
+
before { authenticate! }
helpers do
@@ -117,12 +117,24 @@ module API
success Entities::Project
end
params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: %w[public internal private],
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
use :pagination
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
- present paginate(projects), with: Entities::Project, user: current_user
+ projects = filter_projects(projects)
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
+ present paginate(projects), with: entity, user: current_user
end
desc 'Transfer a project to the group namespace. Available only for admin.' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8b0f8deadfa..746849ef4c0 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -7,67 +7,23 @@ module API
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
- def private_token
- params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
- end
-
- def warden
- env['warden']
- end
-
- # Check the Rails session for valid authentication details
- #
- # Until CSRF protection is added to the API, disallow this method for
- # state-changing endpoints
- def find_user_from_warden
- warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
- end
-
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
declared(params, options).to_h.symbolize_keys
end
- def find_user_by_private_token
- token = private_token
- return nil unless token.present?
-
- User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
- end
-
def current_user
- @current_user ||= find_user_by_private_token
- @current_user ||= doorkeeper_guard
- @current_user ||= find_user_from_warden
-
- unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
- return nil
- end
+ return @current_user if defined?(@current_user)
- identifier = sudo_identifier
+ @current_user = initial_current_user
- if identifier
- # We check for private_token because we cannot allow PAT to be used
- forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
- forbidden!('Private token must be specified in order to use sudo') unless private_token_used?
-
- @impersonator = @current_user
- @current_user = User.by_username_or_id(identifier)
- not_found!("No user id or username for: #{identifier}") if @current_user.nil?
- end
+ sudo!
@current_user
end
- def sudo_identifier
- identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
-
- # Regex for integers
- if !!(identifier =~ /\A[0-9]+\z/)
- identifier.to_i
- else
- identifier
- end
+ def sudo?
+ initial_current_user != current_user
end
def user_project
@@ -78,6 +34,14 @@ module API
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end
+ def find_user(id)
+ if id =~ /^\d+$/
+ User.find_by(id: id)
+ else
+ User.find_by(username: id)
+ end
+ end
+
def find_project(id)
if id =~ /^\d+$/
Project.find_by(id: id)
@@ -96,17 +60,6 @@ module API
end
end
- def project_service(project = user_project)
- @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore)
- @project_service || not_found!("Service")
- end
-
- def service_attributes
- @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
- arr << hash[:name].to_sym
- end
- end
-
def find_group(id)
if id =~ /^\d+$/
Group.find_by(id: id)
@@ -354,6 +307,69 @@ module API
private
+ def private_token
+ params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+ end
+
+ def warden
+ env['warden']
+ end
+
+ # Check the Rails session for valid authentication details
+ #
+ # Until CSRF protection is added to the API, disallow this method for
+ # state-changing endpoints
+ def find_user_from_warden
+ warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
+ end
+
+ def find_user_by_private_token
+ token = private_token
+ return nil unless token.present?
+
+ User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
+ end
+
+ def initial_current_user
+ return @initial_current_user if defined?(@initial_current_user)
+
+ @initial_current_user ||= find_user_by_private_token
+ @initial_current_user ||= doorkeeper_guard
+ @initial_current_user ||= find_user_from_warden
+
+ unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
+ @initial_current_user = nil
+ end
+
+ @initial_current_user
+ end
+
+ def sudo!
+ return unless sudo_identifier
+ return unless initial_current_user
+
+ unless initial_current_user.is_admin?
+ forbidden!('Must be admin to use sudo')
+ end
+
+ # Only private tokens should be used for the SUDO feature
+ unless private_token == initial_current_user.private_token
+ forbidden!('Private token must be specified in order to use sudo')
+ end
+
+ sudoed_user = find_user(sudo_identifier)
+
+ if sudoed_user
+ @current_user = sudoed_user
+ else
+ not_found!("No user id or username for: #{sudo_identifier}")
+ end
+ end
+
+ def sudo_identifier
+ @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
@@ -386,10 +402,6 @@ module API
links.join(', ')
end
- def private_token_used?
- private_token == @current_user.private_token
- end
-
def secret_token
Gitlab::Shell.secret_token
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 55bdbc6a47c..5d1fe22f2df 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -143,8 +143,8 @@ module API
success Entities::MergeRequest
end
params do
- optional :title, type: String, desc: 'The title of the merge request'
- optional :target_branch, type: String, desc: 'The target branch'
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen merge],
desc: 'Status of the merge request'
use :optional_params
diff --git a/lib/api/services.rb b/lib/api/services.rb
index bc427705777..fde2e2746f1 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,84 +1,602 @@
module API
- # Projects API
class Services < Grape::API
+ services = {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Passord of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'builds-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :add_pusher,
+ type: Boolean,
+ desc: 'Add pusher to recipients list'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ },
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'gemnasium' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'Your personal API key on gemnasium.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: "The project's slug on gemnasium.com"
+ }
+ ],
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com'
+ },
+ {
+ required: true,
+ name: :project_key,
+ type: String,
+ desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: Integer,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_builds,
+ type: Boolean,
+ desc: 'Notify only broken builds'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...'
+ },
+ {
+ required: false,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The user name'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The channel name'
+ }
+ ],
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }.freeze
+
+ trigger_services = {
+ 'mattermost-slash-commands' => [
+ {
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ]
+ }.freeze
+
resource :projects do
before { authenticate! }
before { authorize_admin_project }
- # Set <service_slug> service for project
- #
- # Example Request:
- #
- # PUT /projects/:id/services/gitlab-ci
- #
- put ':id/services/:service_slug' do
- if project_service
- validators = project_service.class.validators.select do |s|
- s.class == ActiveRecord::Validations::PresenceValidator &&
- s.attributes != [:project_id]
+ helpers do
+ def service_attributes(service)
+ service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
end
+ end
+ end
- required_attributes! validators.map(&:attributes).flatten.uniq
- attrs = attributes_for_keys service_attributes
+ services.each do |service_slug, settings|
+ desc "Set #{service_slug} service for project"
+ params do
+ settings.each do |setting|
+ if setting[:required]
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ else
+ optional setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ end
+ put ":id/services/#{service_slug}" do
+ service = user_project.find_or_initialize_service(service_slug.underscore)
+ service_params = declared_params(include_missing: false).merge(active: true)
- if project_service.update_attributes(attrs.merge(active: true))
+ if service.update_attributes(service_params)
true
else
- not_found!
+ render_api_error!('400 Bad Request', 400)
end
end
end
- # Delete <service_slug> service for project
- #
- # Example Request:
- #
- # DELETE /project/:id/services/gitlab-ci
- #
- delete ':id/services/:service_slug' do
- if project_service
- attrs = service_attributes.inject({}) do |hash, key|
- hash.merge!(key => nil)
- end
+ desc "Delete a service for project"
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ delete ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
- if project_service.update_attributes(attrs.merge(active: false))
- true
- else
- not_found!
- end
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
+
+ if service.update_attributes(attrs.merge(active: false))
+ true
+ else
+ render_api_error!('400 Bad Request', 400)
end
end
- # Get <service_slug> service settings for project
- #
- # Example Request:
- #
- # GET /project/:id/services/gitlab-ci
- #
- get ':id/services/:service_slug' do
- present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
+ desc 'Get the service settings for project' do
+ success Entities::ProjectService
+ end
+ params do
+ requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ end
+ get ":id/services/:service_slug" do
+ service = user_project.find_or_initialize_service(params[:service_slug].underscore)
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
end
end
- resource :projects do
- desc 'Trigger a slash command' do
- detail 'Added in GitLab 8.13'
+ trigger_services.each do |service_slug, settings|
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
end
- post ':id/services/:service_slug/trigger' do
- project = find_project(params[:id])
+ resource :projects do
+ desc "Trigger a slash command for #{service_slug}" do
+ detail 'Added in GitLab 8.13'
+ end
+ params do
+ settings.each do |setting|
+ requires setting[:name], type: setting[:type], desc: setting[:desc]
+ end
+ end
+ post ":id/services/#{service_slug.underscore}/trigger" do
+ project = find_project(params[:id])
- # This is not accurate, but done to prevent leakage of the project names
- not_found!('Service') unless project
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Service') unless project
- service = project_service(project)
+ service = project.find_or_initialize_service(service_slug.underscore)
- result = service.try(:active?) && service.try(:trigger, params)
+ result = service.try(:active?) && service.try(:trigger, params)
- if result
- status result[:status] || 200
- present result
- else
- not_found!('Service')
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Service')
+ end
end
end
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
new file mode 100644
index 00000000000..e096e636806
--- /dev/null
+++ b/lib/api/snippets.rb
@@ -0,0 +1,137 @@
+module API
+ # Snippets API
+ class Snippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :snippets do
+ helpers do
+ def snippets_for_current_user
+ SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ end
+
+ def public_snippets
+ SnippetsFinder.new.execute(current_user, filter: :public)
+ end
+ end
+
+ desc 'Get a snippets list for authenticated user' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get do
+ present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
+ end
+
+ desc 'List all public snippets current_user has access to' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ use :pagination
+ end
+ get 'public' do
+ present paginate(public_snippets), with: Entities::PersonalSnippet
+ end
+
+ desc 'Get a single snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ':id' do
+ snippet = snippets_for_current_user.find(params[:id])
+ present snippet, with: Entities::PersonalSnippet
+ end
+
+ desc 'Create new snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of a snippet'
+ requires :file_name, type: String, desc: 'The name of a snippet file'
+ requires :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ default: Gitlab::VisibilityLevel::INTERNAL,
+ desc: 'The visibility level of the snippet'
+ end
+ post do
+ attrs = declared_params(include_missing: false)
+ snippet = CreateSnippetService.new(nil, current_user, attrs).execute
+
+ if snippet.persisted?
+ present snippet, with: Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ optional :title, type: String, desc: 'The title of a snippet'
+ optional :file_name, type: String, desc: 'The name of a snippet file'
+ optional :content, type: String, desc: 'The content of a snippet'
+ optional :visibility_level, type: Integer,
+ values: Gitlab::VisibilityLevel.values,
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :content, :visibility_level
+ end
+ put ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :update_personal_snippet, snippet
+
+ attrs = declared_params(include_missing: false)
+
+ UpdateSnippetService.new(nil, current_user, snippet, attrs).execute
+ if snippet.persisted?
+ present snippet, with: Entities::PersonalSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Remove snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ success Entities::PersonalSnippet
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ delete ':id' do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+ authorize! :destroy_personal_snippet, snippet
+ snippet.destroy
+ no_content!
+ end
+
+ desc 'Get a raw snippet' do
+ detail 'This feature was introduced in GitLab 8.15.'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of a snippet'
+ end
+ get ":id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:id))
+ return not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 1dab799dd61..c7db2d71017 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -353,7 +353,7 @@ module API
success Entities::UserPublic
end
get do
- present current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic
+ present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
end
desc "Get the currently authenticated user's SSH keys" do
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index d904a8bd4ae..fd74eeaebe7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -248,7 +248,7 @@ module Banzai
end
def projects_relation_for_paths(paths)
- Project.where_paths_in(paths).includes(:namespace)
+ Project.where_full_path_in(paths).includes(:namespace)
end
# Returns projects for the given paths.
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index f09d78be0ce..9e23c8f8c55 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -46,7 +46,7 @@ module Banzai
end
def rebuild_relative_uri(uri)
- file_path = relative_file_path(uri.path)
+ file_path = relative_file_path(uri)
uri.path = [
relative_url_root,
@@ -59,8 +59,10 @@ module Banzai
uri
end
- def relative_file_path(path)
- nested_path = build_relative_path(path, context[:requested_path])
+ def relative_file_path(uri)
+ path = Addressable::URI.unescape(uri.path)
+ request_path = Addressable::URI.unescape(context[:requested_path])
+ nested_path = build_relative_path(path, request_path)
file_exists?(nested_path) ? nested_path : path
end
@@ -108,11 +110,7 @@ module Banzai
end
def uri_type(path)
- @uri_types[path] ||= begin
- unescaped_path = Addressable::URI.unescape(path)
-
- current_commit.uri_type(unescaped_path)
- end
+ @uri_types[path] ||= current_commit.uri_type(path)
end
def current_commit
diff --git a/lib/gitlab/allowable.rb b/lib/gitlab/allowable.rb
new file mode 100644
index 00000000000..f48abcc86d5
--- /dev/null
+++ b/lib/gitlab/allowable.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module Allowable
+ def can?(user, action, subject)
+ Ability.allowed?(user, action, subject)
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
new file mode 100644
index 00000000000..a979fe7d573
--- /dev/null
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Cancelable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'ban'
+ end
+
+ def action_path
+ cancel_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def action_title
+ 'Cancel'
+ end
+
+ def self.matches?(build, user)
+ build.cancelable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
new file mode 100644
index 00000000000..3fec2c5d4db
--- /dev/null
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ module Common
+ def has_details?
+ can?(user, :read_build, subject)
+ end
+
+ def details_path
+ namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
new file mode 100644
index 00000000000..eee9a64120b
--- /dev/null
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::Build::Stop, Status::Build::Play,
+ Status::Build::Cancelable, Status::Build::Retryable]
+ end
+
+ def self.common_helpers
+ Status::Build::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
new file mode 100644
index 00000000000..5c506e6d59f
--- /dev/null
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Play < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual play action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'play'
+ end
+
+ def action_title
+ 'Play'
+ end
+
+ def action_class
+ 'ci-play-icon'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && !build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
new file mode 100644
index 00000000000..8e38d6a8523
--- /dev/null
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Retryable < SimpleDelegator
+ include Status::Extended
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'refresh'
+ end
+
+ def action_title
+ 'Retry'
+ end
+
+ def action_path
+ retry_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.retryable?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
new file mode 100644
index 00000000000..f8ffa95cde4
--- /dev/null
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Stop < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'manual'
+ end
+
+ def label
+ 'manual stop action'
+ end
+
+ def icon
+ 'icon_status_manual'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'stop'
+ end
+
+ def action_title
+ 'Stop'
+ end
+
+ def action_path
+ play_namespace_project_build_path(subject.project.namespace,
+ subject.project,
+ subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.playable? && build.stops_environment?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index ce4108fdcf2..46fef8262c1 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -4,10 +4,14 @@ module Gitlab
# Base abstract class fore core status
#
class Core
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
+ include Gitlab::Allowable
- def initialize(subject)
+ attr_reader :subject, :user
+
+ def initialize(subject, user)
@subject = subject
+ @user = user
end
def icon
@@ -18,10 +22,6 @@ module Gitlab
raise NotImplementedError
end
- def title
- "#{@subject.class.name.demodulize}: #{label}"
- end
-
# Deprecation warning: this method is here because we need to maintain
# backwards compatibility with legacy statuses. We often do something
# like "ci-status ci-status-#{status}" to set CSS class.
@@ -34,7 +34,7 @@ module Gitlab
end
def has_details?
- raise NotImplementedError
+ false
end
def details_path
@@ -42,16 +42,27 @@ module Gitlab
end
def has_action?
- raise NotImplementedError
+ false
end
def action_icon
raise NotImplementedError
end
+ def action_class
+ end
+
def action_path
raise NotImplementedError
end
+
+ def action_method
+ raise NotImplementedError
+ end
+
+ def action_title
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
index 6bfb5d38c1f..d367c9bda69 100644
--- a/lib/gitlab/ci/status/extended.rb
+++ b/lib/gitlab/ci/status/extended.rb
@@ -2,8 +2,12 @@ module Gitlab
module Ci
module Status
module Extended
- def matches?(_subject)
- raise NotImplementedError
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def matches?(_subject, _user)
+ raise NotImplementedError
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index b2f896f2211..ae9ef895df4 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -2,10 +2,9 @@ module Gitlab
module Ci
module Status
class Factory
- attr_reader :subject
-
- def initialize(subject)
+ def initialize(subject, user)
@subject = subject
+ @user = user
end
def fabricate!
@@ -16,27 +15,32 @@ module Gitlab
end
end
+ def self.extended_statuses
+ []
+ end
+
+ def self.common_helpers
+ Module.new
+ end
+
private
- def subject_status
- @subject_status ||= subject.status
+ def simple_status
+ @simple_status ||= @subject.status || :created
end
def core_status
Gitlab::Ci::Status
- .const_get(subject_status.capitalize)
- .new(subject)
+ .const_get(simple_status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
end
def extended_status
- @extended ||= extended_statuses.find do |status|
- status.matches?(subject)
+ @extended ||= self.class.extended_statuses.find do |status|
+ status.matches?(@subject, @user)
end
end
-
- def extended_statuses
- []
- end
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
index 25e52bec3da..76bfd18bf40 100644
--- a/lib/gitlab/ci/status/pipeline/common.rb
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -4,13 +4,13 @@ module Gitlab
module Pipeline
module Common
def has_details?
- true
+ can?(user, :read_pipeline, subject)
end
def details_path
- namespace_project_pipeline_path(@subject.project.namespace,
- @subject.project,
- @subject)
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject)
end
def has_action?
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 4ac4ec671d0..16dcb326be9 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -3,14 +3,12 @@ module Gitlab
module Status
module Pipeline
class Factory < Status::Factory
- private
-
- def extended_statuses
+ def self.extended_statuses
[Pipeline::SuccessWithWarnings]
end
- def core_status
- super.extend(Status::Pipeline::Common)
+ def self.common_helpers
+ Status::Pipeline::Common
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
index 4b040d60df8..a7c98f9e909 100644
--- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
+++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
@@ -3,7 +3,7 @@ module Gitlab
module Status
module Pipeline
class SuccessWithWarnings < SimpleDelegator
- extend Status::Extended
+ include Status::Extended
def text
'passed'
@@ -21,7 +21,7 @@ module Gitlab
'success_with_warnings'
end
- def self.matches?(pipeline)
+ def self.matches?(pipeline, user)
pipeline.success? && pipeline.has_warnings?
end
end
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index 14c437d2b98..7852f492e1d 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -4,14 +4,14 @@ module Gitlab
module Stage
module Common
def has_details?
- true
+ can?(user, :read_pipeline, subject.pipeline)
end
def details_path
- namespace_project_pipeline_path(@subject.project.namespace,
- @subject.project,
- @subject.pipeline,
- anchor: @subject.name)
+ namespace_project_pipeline_path(subject.project.namespace,
+ subject.project,
+ subject.pipeline,
+ anchor: subject.name)
end
def has_action?
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index c6522d5ada1..689a5dd45bc 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -3,10 +3,8 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
- private
-
- def core_status
- super.extend(Status::Stage::Common)
+ def self.common_helpers
+ Status::Stage::Common
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index a06cf6a989c..d9d1e3cccca 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -61,7 +61,7 @@ module Gitlab
end
def file_name_regex
- @file_name_regex ||= /\A[a-zA-Z0-9_\-\.\@]*\z/.freeze
+ @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
end
def file_name_regex_message
@@ -69,7 +69,7 @@ module Gitlab
end
def file_path_regex
- @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/\@]*\z/.freeze
+ @file_path_regex ||= /\A[[[:alnum:]]_\-\.\/\@]*\z/.freeze
end
def file_path_regex_message
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index 5132177de51..632e2d87500 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -1,5 +1,11 @@
module Gitlab
module Routing
+ extend ActiveSupport::Concern
+
+ included do
+ include Gitlab::Routing.url_helpers
+ end
+
# Returns the URL helpers Module.
#
# This method caches the output as Rails' "url_helpers" method creates an
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 99d0c28e749..ccb456bcc94 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -24,6 +24,8 @@ module Gitlab
wiki_page_url
when ProjectSnippet
project_snippet_url(object)
+ when Snippet
+ personal_snippet_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 141a0b74ec0..f5caca3ddbf 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -1,8 +1,12 @@
+require Rails.root.join('lib/gitlab/database')
+require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
+require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
+ AddLowerPathIndexToRoutes.new.up
end
diff --git a/package.json b/package.json
index 961989f8012..49b8210e427 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
- "eslint": "eslint --ext .js,.js.es6 .",
+ "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
"eslint-fix": "npm run eslint -- --fix",
"eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html"
},
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
new file mode 100644
index 00000000000..9fd5c3b85f6
--- /dev/null
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Projects::ReleasesController do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user) }
+ let!(:release) { create(:release, project: project) }
+ let!(:tag) { release.tag }
+
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ describe 'GET #edit' do
+ it 'initializes a new release' do
+ tag_id = release.tag
+ project.releases.destroy_all
+
+ get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: tag_id
+
+ release = assigns(:release)
+ expect(release).not_to be_nil
+ expect(release).not_to be_persisted
+ end
+
+ it 'retrieves an existing release' do
+ get :edit, namespace_id: project.namespace.path, project_id: project.path, tag_id: release.tag
+
+ release = assigns(:release)
+ expect(release).not_to be_nil
+ expect(release).to be_persisted
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'updates release note description' do
+ update_release('description updated')
+
+ release = project.releases.find_by_tag(tag)
+ expect(release.description).to eq("description updated")
+ end
+
+ it 'deletes release note when description is null' do
+ expect { update_release('') }.to change(project.releases, :count).by(-1)
+ end
+ end
+
+ def update_release(description)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ tag_id: release.tag,
+ release: { description: description }
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index c443af09075..62466c06194 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -12,12 +12,14 @@ FactoryGirl.define do
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
+
options do
{
image: "ruby:2.1",
services: ["postgres"]
}
end
+
yaml_variables do
[
{ key: :DB_NAME, value: 'postgres', public: true }
@@ -60,15 +62,20 @@ FactoryGirl.define do
end
trait :teardown_environment do
- options do
- { environment: { action: 'stop' } }
- end
+ environment 'staging'
+ options environment: { name: 'staging',
+ action: 'stop' }
end
trait :allowed_to_fail do
allow_failure true
end
+ trait :playable do
+ skipped
+ manual
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index ebd3595ea64..ece6beb9fa9 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -19,5 +19,9 @@ FactoryGirl.define do
trait :access_requestable do
request_access_enabled true
end
+
+ trait :nested do
+ parent factory: :group
+ end
end
end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index f6d625fa7f6..0aa01fc499a 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -21,7 +21,7 @@ feature 'Admin Groups', feature: true do
scenario 'shows the visibility level radio populated with the group visibility_level value' do
group = create(:group, :private)
- visit edit_admin_group_path(group)
+ visit admin_group_edit_path(group)
expect_selected_visibility(group.visibility_level)
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 30ded9202a4..a36bfd574cb 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true do
describe "GET /admin/projects" do
before do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
end
it "is ok" do
- expect(current_path).to eq(admin_namespaces_projects_path)
+ expect(current_path).to eq(admin_projects_path)
end
it "has projects list" do
@@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true do
describe "GET /admin/projects/:id" do
before do
- visit admin_namespaces_projects_path
+ visit admin_projects_path
click_link "#{@project.name}"
end
diff --git a/features/steps/admin/settings.rb b/spec/features/admin/admin_settings_spec.rb
index 11dc7f580f0..8cd66f189be 100644
--- a/features/steps/admin/settings.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -1,62 +1,53 @@
-class Spinach::Features::AdminSettings < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
- include Gitlab::CurrentSettings
+require 'spec_helper'
- step 'I modify settings and save form' do
+feature 'Admin updates settings', feature: true do
+ before(:each) do
+ login_as :admin
+ visit admin_application_settings_path
+ end
+
+ scenario 'Change application settings' do
uncheck 'Gravatar enabled'
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
fill_in 'Help page text', with: 'Example text'
click_button 'Save'
- end
- step 'I should see application settings saved' do
expect(current_application_settings.gravatar_enabled).to be_falsey
expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
expect(page).to have_content "Application settings saved successfully"
end
- step 'I click on "Service Templates"' do
+ scenario 'Change Slack Service template settings' do
click_link 'Service Templates'
- end
-
- step 'I click on "Slack" service' do
click_link 'Slack'
- end
-
- step 'I check all events and submit form' do
- page.check('Active')
- page.check('Push')
- page.check('Tag push')
- page.check('Note')
- page.check('Issue')
- page.check('Merge request')
- page.check('Build')
- page.check('Pipeline')
- click_on 'Save'
- end
-
- step 'I fill out Slack settings' do
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
page.check('Notify only broken builds')
- end
- step 'I should see service template settings saved' do
+ check_all_events
+ click_on 'Save'
+
expect(page).to have_content 'Application settings saved successfully'
- end
- step 'I should see all checkboxes checked' do
+ click_link 'Slack'
+
page.all('input[type=checkbox]').each do |checkbox|
expect(checkbox).to be_checked
end
- end
-
- step 'I should see Slack settings saved' do
expect(find_field('Webhook').value).to eq 'http://localhost'
expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel'
end
+
+ def check_all_events
+ page.check('Active')
+ page.check('Push')
+ page.check('Tag push')
+ page.check('Note')
+ page.check('Issue')
+ page.check('Merge request')
+ page.check('Build')
+ page.check('Pipeline')
+ end
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 365cb445df1..44dfc2dff45 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -36,7 +36,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do
visit user_snippets_path(user)
wait_for_ajax()
- page.find('.js-timeago').hover
+ page.find('.js-timeago.snippet-created-ago').hover
end
it 'has the datetime formated correctly' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c421da97d76..cd0512a37e6 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -2,8 +2,9 @@ require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user) }
+ let(:user) { create(:user, username: 'someone.special') }
let(:project) { create(:project) }
+ let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) }
before do
@@ -23,21 +24,69 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).to have_selector('.atwho-container')
end
- it 'opens autocomplete menu when field is prefixed with non-text character' do
+ it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys('testing')
find('#note_note').native.send_keys('@')
end
- expect(page).to have_selector('.atwho-container')
+ expect(page).not_to have_selector('.atwho-view')
end
- it 'doesnt open autocomplete menu character is prefixed with text' do
- page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('testing')
- find('#note_note').native.send_keys('@')
+ context 'if a selected value has special characters' do
+ it 'wraps the result in double quotes' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("~#{label.title[0]}")
+ sleep 1
+ note.click
+ end
+
+ label_item = find('.atwho-view li', text: label.title)
+
+ expect_to_wrap(true, label_item, note, label.title)
end
- expect(page).not_to have_selector('.atwho-view')
+ it 'doesn\'t wrap for assignee values' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys("@#{user.username[0]}")
+ sleep 1
+ note.click
+ end
+
+ user_item = find('.atwho-view li', text: user.username)
+
+ expect_to_wrap(false, user_item, note, user.username)
+ end
+
+ it 'doesn\'t wrap for emoji values' do
+ note = find('#note_note')
+ page.within '.timeline-content-form' do
+ note.native.send_keys('')
+ note.native.send_keys(":cartwheel")
+ sleep 1
+ note.click
+ end
+
+ emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
+
+ expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
+ end
+
+ def expect_to_wrap(should_wrap, item, note, value)
+ expect(item).to have_content(value)
+ expect(item).not_to have_content("\"#{value}\"")
+
+ item.click
+
+ if should_wrap
+ expect(note.value).to include("\"#{value}\"")
+ else
+ expect(note.value).not_to include("\"#{value}\"")
+ end
+ end
end
end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index a0ccc472d11..8c4d4320dc5 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -227,6 +227,43 @@ feature 'Builds', :feature do
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end
end
+
+ context 'when build starts environment' do
+ let(:environment) { create(:environment, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'build is successfull and has deployment' do
+ let(:deployment) { create(:deployment) }
+ let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
+
+ it 'shows a link for the build' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link environment.name
+ end
+ end
+
+ context 'build is complete and not successfull' do
+ let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
+
+ it 'shows a link for the build' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link environment.name
+ end
+ end
+
+ context 'build creates a new deployment' do
+ let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
+ let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
+
+ it 'shows a link to lastest deployment' do
+ visit namespace_project_build_path(project.namespace, project, build)
+
+ expect(page).to have_link('latest deployment')
+ end
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
new file mode 100644
index 00000000000..ae448706130
--- /dev/null
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'User wants to create a file', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ background do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, project.default_branch)
+ end
+
+ def submit_new_file(options)
+ file_name = find('#file_name')
+ file_name.set options[:file_name] || 'README.md'
+
+ file_content = find('#file-content')
+ file_content.set options[:file_content] || 'Some content'
+
+ click_button 'Commit Changes'
+ end
+
+ scenario 'file name contains Chinese characters' do
+ submit_new_file(file_name: '测试.md')
+ expect(page).to have_content 'The file has been successfully created.'
+ end
+
+ scenario 'directory name contains Chinese characters' do
+ submit_new_file(file_name: '中文/测试.md')
+ expect(page).to have_content 'The file has been successfully created.'
+ end
+
+ scenario 'file name contains invalid characters' do
+ submit_new_file(file_name: '\\')
+ expect(page).to have_content 'Your changes could not be committed, because the file name can contain only'
+ end
+
+ scenario 'file name contains directory traversal' do
+ submit_new_file(file_name: '../README.md')
+ expect(page).to have_content 'Your changes could not be committed, because the file name cannot include directory traversal.'
+ end
+end
diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb
index fe8cd7b7602..e180ca53eb5 100644
--- a/spec/features/security/admin_access_spec.rb
+++ b/spec/features/security/admin_access_spec.rb
@@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do
include AccessMatchers
describe "GET /admin/projects" do
- subject { admin_namespaces_projects_path }
+ subject { admin_projects_path }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_denied_for :user }
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 28bdc18e840..975e99c5807 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -9,65 +9,74 @@ describe SnippetsFinder do
let(:project2) { create(:empty_project, :private, group: group) }
context ':all filter' do
- before do
- @snippet1 = create(:personal_snippet, :private)
- @snippet2 = create(:personal_snippet, :internal)
- @snippet3 = create(:personal_snippet, :public)
- end
+ let!(:snippet1) { create(:personal_snippet, :private) }
+ let!(:snippet2) { create(:personal_snippet, :internal) }
+ let!(:snippet3) { create(:personal_snippet, :public) }
it "returns all private and internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :all)
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
+ expect(snippets).to include(snippet2, snippet3)
+ expect(snippets).not_to include(snippet1)
end
it "returns all public snippets" do
snippets = SnippetsFinder.new.execute(nil, filter: :all)
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
- before do
- @snippet1 = create(:personal_snippet, :private, author: user)
- @snippet2 = create(:personal_snippet, :internal, author: user)
- @snippet3 = create(:personal_snippet, :public, author: user)
+ context ':public filter' do
+ let!(:snippet1) { create(:personal_snippet, :private) }
+ let!(:snippet2) { create(:personal_snippet, :internal) }
+ let!(:snippet3) { create(:personal_snippet, :public) }
+
+ it "returns public public snippets" do
+ snippets = SnippetsFinder.new.execute(nil, filter: :public)
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
+ end
+
+ context ':by_user filter' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
snippets = SnippetsFinder.new.execute(user1, filter: :by_user, user: user)
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
+ expect(snippets).to include(snippet2, snippet3)
+ expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
- expect(snippets).to include(@snippet2)
- expect(snippets).not_to include(@snippet1, @snippet3)
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_private")
- expect(snippets).to include(@snippet1)
- expect(snippets).not_to include(@snippet2, @snippet3)
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user, scope: "are_public")
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
snippets = SnippetsFinder.new.execute(user, filter: :by_user, user: user)
- expect(snippets).to include(@snippet1, @snippet2, @snippet3)
+ expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
snippets = SnippetsFinder.new.execute(nil, filter: :by_user, user: user)
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet2, @snippet1)
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet2, snippet1)
end
end
@@ -84,16 +93,39 @@ describe SnippetsFinder do
expect(snippets).not_to include(@snippet1, @snippet2)
end
- it "returns public and internal snippets for none project members" do
+ it "returns public and internal snippets for non project members" do
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
+ it "returns public snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ expect(snippets).to include(@snippet3)
+ expect(snippets).not_to include(@snippet1, @snippet2)
+ end
+
+ it "returns internal snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ expect(snippets).to include(@snippet2)
+ expect(snippets).not_to include(@snippet1, @snippet3)
+ end
+
+ it "does not return private snippets for non project members" do
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
+ end
+
it "returns all snippets for project members" do
project1.team << [user, :developer]
snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1)
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
+
+ it "returns private snippets for project members" do
+ project1.team << [user, :developer]
+ snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ expect(snippets).to include(@snippet1)
+ end
end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index ac1404f6e1c..7b2e3db6218 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -33,9 +33,9 @@
};
describe('AwardsHandler', function() {
- fixture.preload('awards_handler.html');
+ fixture.preload('issues/open-issue.html.raw');
beforeEach(function() {
- fixture.load('awards_handler.html');
+ fixture.load('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
@@ -113,7 +113,7 @@
});
describe('::getAwardUrl', function() {
return it('should return the url for request', function() {
- return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji');
+ return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
});
});
describe('::addAward and ::checkMutuality', function() {
@@ -209,7 +209,7 @@
$('.js-add-award').eq(0).click();
$menu = $('.emoji-menu');
$block = $('.js-awards-block');
- $emoji = $menu.find(".emoji-menu-list-item " + selector);
+ $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
expect($emoji.length).toBe(1);
expect($block.find(selector).length).toBe(0);
$emoji.click();
@@ -224,7 +224,7 @@
openEmojiMenuAndAddEmoji();
$('.js-add-award').eq(0).click();
$block = $('.js-awards-block');
- $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector);
+ $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
$emoji.click();
return expect($block.find(selector).length).toBe(0);
});
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
deleted file mode 100644
index 1ef2e8f8624..00000000000
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ /dev/null
@@ -1,52 +0,0 @@
-.issue-details.issuable-details
- .detail-page-description.content-block
- %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
- .description.js-task-list-container.is-task-list-enabled
- .wiki
- %p Qui exercitationem magnam optio quae fuga earum odio.
- %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
- %small.edited-text
- .content-block.content-block-small
- .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
- %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
- .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
- %span.award-control-text.js-counter 0
- %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
- .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
- %span.award-control-text.js-counter 0
- .award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{:type => "button"}
- %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
- %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
- %span.award-control-text Add
- %section.issuable-discussion
- #notes
- %ul#notes-list.notes.main-notes-list.timeline
- %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
- .timeline-entry-inner
- .timeline-icon
- %a{:href => "/u/agustin"}
- %img.avatar.s40{:alt => "", :src => "#"}/
- .timeline-content
- .note-header
- %a.author_link{:href => "/u/agustin"}
- %span.author Brenna Stokes
- .inline.note-headline-light
- @agustin commented
- %a{:href => "#note_348"}
- %time 11 days ago
- .note-actions
- %span.note-role Reporter
- %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
- %i.fa.fa-spinner.fa-spin
- %i.fa.fa-smile-o.link-highlight
- .js-task-list-container.note-body.is-task-list-enabled
- .note-text
- %p Suscipit sunt quia quisquam sed eveniet ipsam.
- .note-awards
- .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
- .award-menu-holder.js-award-holder
- %button.btn.award-control.js-add-award{:type => "button"}
- %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
- %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
- %span.award-control-text Add
diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml
new file mode 100644
index 00000000000..deca50ceaa7
--- /dev/null
+++ b/spec/javascripts/fixtures/pipeline_graph.html.haml
@@ -0,0 +1,15 @@
+%div.pipeline-visualization.js-pipeline-graph
+ %ul.stage-column-list
+ %li.stage-column
+ .stage-name
+ %a{:href => "/"}
+ Test
+ .builds-container
+ %ul
+ %li.build
+ .curve
+ .build-content
+ %a
+ %svg
+ .ci-status-text
+ stop_review
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6
new file mode 100644
index 00000000000..85c9cf4b4f1
--- /dev/null
+++ b/spec/javascripts/pipelines_spec.js.es6
@@ -0,0 +1,25 @@
+//= require pipelines
+
+(() => {
+ describe('Pipelines', () => {
+ fixture.preload('pipeline_graph');
+
+ beforeEach(() => {
+ fixture.load('pipeline_graph');
+ });
+
+ it('should be defined', () => {
+ expect(window.gl.Pipelines).toBeDefined();
+ });
+
+ it('should create a `Pipelines` instance without options', () => {
+ expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line
+ });
+
+ it('should create a `Pipelines` instance with options', () => {
+ const pipelines = new window.gl.Pipelines({ foo: 'bar' });
+
+ expect(pipelines.pipelineGraph).toBeDefined();
+ });
+ });
+})();
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 49211a6b852..65de1756201 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -21,16 +21,18 @@
return this.project = new Project();
});
return describe('project list', function() {
+ var fakeAjaxResponse = function fakeAjaxResponse(req) {
+ var d;
+ expect(req.url).toBe('/api/v3/projects.json?simple=true');
+ d = $.Deferred();
+ d.resolve(this.projects_data);
+ return d.promise();
+ };
+
beforeEach((function(_this) {
return function() {
_this.projects_data = fixture.load('projects.json')[0];
- return spyOn(jQuery, 'ajax').and.callFake(function(req) {
- var d;
- expect(req.url).toBe('/api/v3/projects.json?simple=true');
- d = $.Deferred();
- d.resolve(_this.projects_data);
- return d.promise();
- });
+ return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
it('to show on toggle click', (function(_this) {
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6
index ed6166a25a8..1b7ca97cde4 100644
--- a/spec/javascripts/smart_interval_spec.js.es6
+++ b/spec/javascripts/smart_interval_spec.js.es6
@@ -14,8 +14,9 @@
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
- delayStartBy: 0,
lazyStart: false,
+ immediateExecution: false,
+ hiddenInterval: null,
};
if (config) {
@@ -114,14 +115,31 @@
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
- interval.state.pageVisibility = 'hidden';
- interval.handleVisibilityChange();
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
done();
}, DEFAULT_SHORT_TIMEOUT);
});
+ it('should change to the hidden interval when page is not visible', function (done) {
+ const HIDDEN_INTERVAL = 1500;
+ const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
+
it('should resume when page is becomes visible at the previous interval', function (done) {
const interval = this.smartInterval;
@@ -129,14 +147,12 @@
expect(interval.state.intervalId).toBeTruthy();
// simulates triggering of visibilitychange event
- interval.state.pageVisibility = 'hidden';
- interval.handleVisibilityChange();
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
expect(interval.state.intervalId).toBeUndefined();
// simulates triggering of visibilitychange event
- interval.state.pageVisibility = 'visible';
- interval.handleVisibilityChange();
+ interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
expect(interval.state.intervalId).toBeTruthy();
@@ -154,6 +170,11 @@
done();
}, DEFAULT_SHORT_TIMEOUT);
});
+
+ it('should execute callback before first interval', function () {
+ const interval = createDefaultSmartInterval({ immediateExecution: true });
+ expect(interval.cfg.immediateExecution).toBeFalsy();
+ });
});
});
})(window.gl || (window.gl = {}));
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 2bfa51deb20..df2dd173b57 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -175,7 +175,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
doc = filter(image(escaped))
- expect(doc.at_css('img')['src']).to match '/raw/'
+ expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}"
end
context 'when requested path is a file in the repo' do
diff --git a/spec/lib/gitlab/allowable_spec.rb b/spec/lib/gitlab/allowable_spec.rb
new file mode 100644
index 00000000000..87733d53e92
--- /dev/null
+++ b/spec/lib/gitlab/allowable_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::Allowable do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ describe '#can?' do
+ let(:user) { create(:user) }
+
+ context 'when user is allowed to do something' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'reports correct ability to perform action' do
+ expect(subject.can?(user, :read_project, project)).to be true
+ end
+ end
+
+ context 'when user is not allowed to do something' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'reports correct ability to perform action' do
+ expect(subject.can?(user, :read_project, project)).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
new file mode 100644
index 00000000000..9376bce17a1
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Cancelable do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'does not override status icon' do
+ expect(status).to receive(:icon)
+
+ subject.icon
+ end
+ end
+
+ describe '#label' do
+ it 'does not override status label' do
+ expect(status).to receive(:label)
+
+ subject.label
+ end
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/cancel" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'ban' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Cancel' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is cancelable' do
+ let(:build) do
+ create(:ci_build, :running)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not cancelable' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb
new file mode 100644
index 00000000000..40b96b1807b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/common_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Common do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ subject do
+ Gitlab::Ci::Status::Core
+ .new(build, user)
+ .extend(described_class)
+ end
+
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
+ end
+
+ describe '#has_details?' do
+ context 'when user has access to read build' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read build' do
+ before { project.update(public_builds: false) }
+
+ it { is_expected.not_to have_details }
+ end
+ end
+
+ describe '#details_path' do
+ it 'links to the build details page' do
+ expect(subject.details_path).to include "builds/#{build.id}"
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
new file mode 100644
index 00000000000..dccb29b5ef6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Factory do
+ let(:user) { create(:user) }
+ let(:project) { build.project }
+
+ subject { described_class.new(build, user) }
+ let(:status) { subject.fabricate! }
+
+ before { project.team << [user, :developer] }
+
+ context 'when build is successful' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'passed'
+ expect(status.icon).to eq 'icon_status_success'
+ expect(status.label).to eq 'passed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is a canceled' do
+ let(:build) { create(:ci_build, :canceled) }
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'canceled'
+ expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.label).to eq 'canceled'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is running' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'fabricates a canceable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'running'
+ expect(status.icon).to eq 'icon_status_running'
+ expect(status.label).to eq 'running'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is pending' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'fabricates a cancelable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'pending'
+ expect(status.icon).to eq 'icon_status_pending'
+ expect(status.label).to eq 'pending'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is skipped' do
+ let(:build) { create(:ci_build, :skipped) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'skipped'
+ expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.label).to eq 'skipped'
+ expect(status).to have_details
+ expect(status).not_to have_action
+ end
+ end
+
+ context 'when build is a manual action' do
+ context 'when build is a play action' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Play
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.label).to eq 'manual play action'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+
+ context 'when build is an environment stop action' do
+ let(:build) { create(:ci_build, :playable, :teardown_environment) }
+
+ it 'fabricates a core skipped status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Stop
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'manual'
+ expect(status.icon).to eq 'icon_status_manual'
+ expect(status.label).to eq 'manual stop action'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
new file mode 100644
index 00000000000..4ddf04a8e11
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Play do
+ let(:status) { double('core') }
+ let(:user) { double('user') }
+
+ subject { described_class.new(status) }
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual play action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'play' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Play' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is playable' do
+ context 'when build stops an environment' do
+ let(:build) do
+ create(:ci_build, :playable, :teardown_environment)
+ end
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'when build does not stop an environment' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
new file mode 100644
index 00000000000..d61e5bbaa6b
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Retryable do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'does not override status icon' do
+ expect(status).to receive(:icon)
+
+ subject.icon
+ end
+ end
+
+ describe '#label' do
+ it 'does not override status label' do
+ expect(status).to receive(:label)
+
+ subject.label
+ end
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/retry" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'refresh' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Retry' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is retryable' do
+ let(:build) do
+ create(:ci_build, :success)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not retryable' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
new file mode 100644
index 00000000000..59a85b55f90
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Stop do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual stop action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe 'action details' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ describe '#has_action?' do
+ context 'when user is allowed to update build' do
+ before { build.project.team << [user, :developer] }
+
+ it { is_expected.to have_action }
+ end
+
+ context 'when user is not allowed to update build' do
+ it { is_expected.not_to have_action }
+ end
+ end
+
+ describe '#action_path' do
+ it { expect(subject.action_path).to include "#{build.id}/play" }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to eq 'stop' }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to eq 'Stop' }
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is playable' do
+ context 'when build stops an environment' do
+ let(:build) do
+ create(:ci_build, :playable, :teardown_environment)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build does not stop an environment' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+
+ context 'when build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 619ecbcba67..4639278ad45 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Canceled do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'canceled' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Canceled do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_canceled' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: canceled' }
- end
end
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index 157302c65a8..2ce176a29d6 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Created do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'created' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Created do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_created' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: created' }
- end
end
diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb
index 120e121aae5..c2d74ca5cde 100644
--- a/spec/lib/gitlab/ci/status/extended_spec.rb
+++ b/spec/lib/gitlab/ci/status/extended_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Extended do
subject do
- Class.new.extend(described_class)
+ Class.new.include(described_class)
end
it 'requires subclass to implement matcher' do
- expect { subject.matches?(double) }
+ expect { subject.matches?(double, double) }
.to raise_error(NotImplementedError)
end
end
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
index d5bd7f7102b..f92a1c149bf 100644
--- a/spec/lib/gitlab/ci/status/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -2,15 +2,17 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Factory do
subject do
- described_class.new(object)
+ described_class.new(resource, user)
end
+ let(:user) { create(:user) }
+
let(:status) { subject.fabricate! }
context 'when object has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
- let(:object) { double(status: core_status) }
+ let(:resource) { double(status: core_status) }
it "fabricates a core status #{core_status}" do
expect(status).to be_a(
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 0b3cb8168e6..9d527e6a7ef 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Failed do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'failed' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Failed do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_failed' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: failed' }
- end
end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 57c901c1202..d03f595d3c7 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Pending do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'pending' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Pending do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_pending' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: pending' }
- end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
index 21adee3f8e7..d665674bf70 100644
--- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb
@@ -1,23 +1,36 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Common do
- let(:pipeline) { create(:ci_pipeline) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
subject do
- Class.new(Gitlab::Ci::Status::Core)
- .new(pipeline).extend(described_class)
+ Gitlab::Ci::Status::Core
+ .new(pipeline, user)
+ .extend(described_class)
end
- it 'does not have action' do
- expect(subject).not_to have_action
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
end
- it 'has details' do
- expect(subject).to have_details
+ describe '#has_details?' do
+ context 'when user has access to read pipeline' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read pipeline' do
+ it { is_expected.not_to have_details }
+ end
end
- it 'links to the pipeline details page' do
- expect(subject.details_path)
- .to include "pipelines/#{pipeline.id}"
+ describe '#details_path' do
+ it 'links to the pipeline details page' do
+ expect(subject.details_path)
+ .to include "pipelines/#{pipeline.id}"
+ end
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index d6243940f2e..d4a2dc7fcc1 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -1,14 +1,21 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Factory do
+ let(:user) { create(:user) }
+ let(:project) { pipeline.project }
+
subject do
- described_class.new(pipeline)
+ described_class.new(pipeline, user)
end
let(:status) do
subject.fabricate!
end
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when pipeline has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
index 02e526e3de2..7e3383c307f 100644
--- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
@@ -29,13 +29,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
end
it 'is a correct match' do
- expect(described_class.matches?(pipeline)).to eq true
+ expect(described_class.matches?(pipeline, double)).to eq true
end
end
context 'when pipeline does not have warnings' do
it 'does not match' do
- expect(described_class.matches?(pipeline)).to eq false
+ expect(described_class.matches?(pipeline, double)).to eq false
end
end
end
@@ -51,13 +51,13 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
end
it 'does not match' do
- expect(described_class.matches?(pipeline)).to eq false
+ expect(described_class.matches?(pipeline, double)).to eq false
end
end
context 'when pipeline does not have warnings' do
it 'does not match' do
- expect(described_class.matches?(pipeline)).to eq false
+ expect(described_class.matches?(pipeline, double)).to eq false
end
end
end
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index c023f1872cc..9f47090d396 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Running do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'running' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Running do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_running' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: running' }
- end
end
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index d4f7f4b3b70..94601648a8d 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Skipped do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'skipped' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Skipped do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_skipped' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: skipped' }
- end
end
diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb
index f3259c6f23e..8814a7614a0 100644
--- a/spec/lib/gitlab/ci/status/stage/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb
@@ -1,26 +1,43 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Stage::Common do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
subject do
Class.new(Gitlab::Ci::Status::Core)
- .new(stage).extend(described_class)
+ .new(stage, user).extend(described_class)
end
it 'does not have action' do
expect(subject).not_to have_action
end
- it 'has details' do
- expect(subject).to have_details
- end
-
it 'links to the pipeline details page' do
expect(subject.details_path)
.to include "pipelines/#{pipeline.id}"
expect(subject.details_path)
.to include "##{stage.name}"
end
+
+ context 'when user has permission to read pipeline' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'has details' do
+ expect(subject).to have_details
+ end
+ end
+
+ context 'when user does not have permission to read pipeline' do
+ it 'does not have details' do
+ expect(subject).not_to have_details
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 17929665c83..6f8721d30c2 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -1,17 +1,26 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Stage::Factory do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:stage) do
+ build(:ci_stage, pipeline: pipeline, name: 'test')
+ end
subject do
- described_class.new(stage)
+ described_class.new(stage, user)
end
let(:status) do
subject.fabricate!
end
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when stage has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 9e261a3aa5f..90f9f615e0d 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Success do
- subject { described_class.new(double('subject')) }
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
describe '#text' do
it { expect(subject.label).to eq 'passed' }
@@ -14,8 +16,4 @@ describe Gitlab::Ci::Status::Success do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_success' }
end
-
- describe '#title' do
- it { expect(subject.title).to eq 'Double: passed' }
- end
end
diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb
new file mode 100644
index 00000000000..01d5acfc15b
--- /dev/null
+++ b/spec/lib/gitlab/routing_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Routing do
+ context 'when module is included' do
+ subject do
+ Class.new.include(described_class).new
+ end
+
+ it 'makes it possible to access url helpers' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+ end
+
+ context 'when module is not included' do
+ subject do
+ Class.new.include(described_class.url_helpers).new
+ end
+
+ it 'exposes url helpers module through a method' do
+ expect(subject).to respond_to(:namespace_project_path)
+ end
+ end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index 52428547a9f..6a93deb5412 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb')
@@ -59,6 +61,10 @@ describe MigrateProcessCommitWorkerJobs do
Sidekiq.redis { |r| r.llen('queue:process_commit') }
end
+ def pop_job
+ JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ end
+
before do
Sidekiq.redis do |redis|
job = JSON.dump(args: [project.id, user.id, commit.oid])
@@ -92,11 +98,28 @@ describe MigrateProcessCommitWorkerJobs do
expect(job_count).to eq(1)
end
+ it 'encodes data to UTF-8' do
+ allow_any_instance_of(Rugged::Repository).to receive(:lookup).
+ with(commit.oid).
+ and_return(commit)
+
+ allow(commit).to receive(:message).
+ and_return('김치'.force_encoding('BINARY'))
+
+ migration.up
+
+ job = pop_job
+
+ # We don't care so much about what is being stored, instead we just want
+ # to make sure the encoding is right so that JSON encoding the data
+ # doesn't produce any errors.
+ expect(job['args'][2]['message'].encoding).to eq(Encoding::UTF_8)
+ end
+
context 'a migrated job' do
let(:job) do
migration.up
-
- JSON.load(Sidekiq.redis { |r| r.lpop('queue:process_commit') })
+ pop_job
end
let(:commit_hash) do
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index d4970e38f7c..7f39aff7639 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -899,21 +899,87 @@ describe Ci::Build, models: true do
end
end
+ describe '#cancelable?' do
+ subject { build }
+
+ context 'when build is cancelable' do
+ context 'when build is pending' do
+ it { is_expected.to be_cancelable }
+ end
+
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.to be_cancelable }
+ end
+ end
+
+ context 'when build is not cancelable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+ end
+ end
+
describe '#retryable?' do
- context 'when build is running' do
- before do
- build.run!
+ subject { build }
+
+ context 'when build is retryable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.to be_retryable }
end
- it { expect(build).not_to be_retryable }
+ context 'when build is canceled' do
+ before do
+ build.cancel!
+ end
+
+ it { is_expected.to be_retryable }
+ end
end
- context 'when build is finished' do
- before do
- build.success!
+ context 'when build is not retryable' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.not_to be_retryable }
end
- it { expect(build).to be_retryable }
+ context 'when build is skipped' do
+ before do
+ build.skip!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
end
end
@@ -1180,4 +1246,13 @@ describe Ci::Build, models: true do
it { is_expected.to eq('review/master') }
end
end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(build.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 8158e71dd55..e78ae14b737 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -442,11 +442,15 @@ describe Ci::Pipeline, models: true do
end
describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ subject { pipeline.detailed_status(user) }
+
context 'when pipeline is created' do
let(:pipeline) { create(:ci_pipeline, status: :created) }
it 'returns detailed status for created pipeline' do
- expect(pipeline.detailed_status.text).to eq 'created'
+ expect(subject.text).to eq 'created'
end
end
@@ -454,7 +458,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :pending) }
it 'returns detailed status for pending pipeline' do
- expect(pipeline.detailed_status.text).to eq 'pending'
+ expect(subject.text).to eq 'pending'
end
end
@@ -462,7 +466,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :running) }
it 'returns detailed status for running pipeline' do
- expect(pipeline.detailed_status.text).to eq 'running'
+ expect(subject.text).to eq 'running'
end
end
@@ -470,7 +474,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :success) }
it 'returns detailed status for successful pipeline' do
- expect(pipeline.detailed_status.text).to eq 'passed'
+ expect(subject.text).to eq 'passed'
end
end
@@ -478,7 +482,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :failed) }
it 'returns detailed status for failed pipeline' do
- expect(pipeline.detailed_status.text).to eq 'failed'
+ expect(subject.text).to eq 'failed'
end
end
@@ -486,7 +490,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :canceled) }
it 'returns detailed status for canceled pipeline' do
- expect(pipeline.detailed_status.text).to eq 'canceled'
+ expect(subject.text).to eq 'canceled'
end
end
@@ -494,7 +498,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :skipped) }
it 'returns detailed status for skipped pipeline' do
- expect(pipeline.detailed_status.text).to eq 'skipped'
+ expect(subject.text).to eq 'skipped'
end
end
@@ -506,7 +510,7 @@ describe Ci::Pipeline, models: true do
end
it 'retruns detailed status for successful pipeline with warnings' do
- expect(pipeline.detailed_status.label).to eq 'passed with warnings'
+ expect(subject.label).to eq 'passed with warnings'
end
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index f232761dba2..8fff38f7cda 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -68,7 +68,9 @@ describe Ci::Stage, models: true do
end
describe '#detailed_status' do
- subject { stage.detailed_status }
+ let(:user) { create(:user) }
+
+ subject { stage.detailed_status(user) }
context 'when build is created' do
let!(:stage_build) { create_job(:ci_build, status: :created) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 1ec08c2a9d0..701f3323c0f 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -234,4 +234,13 @@ describe CommitStatus, models: true do
end
end
end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(commit_status.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Success
+ end
+ end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 0acefc0c1d5..b556135532f 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -3,6 +3,10 @@ require 'spec_helper'
describe Group, 'Routable' do
let!(:group) { create(:group) }
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:route) }
+ end
+
describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) }
end
@@ -35,16 +39,16 @@ describe Group, 'Routable' do
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end
- describe '.where_paths_in' do
+ describe '.where_full_path_in' do
context 'without any paths' do
it 'returns an empty relation' do
- expect(described_class.where_paths_in([])).to eq([])
+ expect(described_class.where_full_path_in([])).to eq([])
end
end
context 'without any valid paths' do
it 'returns an empty relation' do
- expect(described_class.where_paths_in(%w[unknown])).to eq([])
+ expect(described_class.where_full_path_in(%w[unknown])).to eq([])
end
end
@@ -52,13 +56,13 @@ describe Group, 'Routable' do
let!(:nested_group) { create(:group, parent: group) }
it 'returns the projects matching the paths' do
- result = described_class.where_paths_in([group.to_param, nested_group.to_param])
+ result = described_class.where_full_path_in([group.to_param, nested_group.to_param])
expect(result).to contain_exactly(group, nested_group)
end
it 'returns projects regardless of the casing of paths' do
- result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase])
+ result = described_class.where_full_path_in([group.to_param.upcase, nested_group.to_param.upcase])
expect(result).to contain_exactly(group, nested_group)
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 615cfe3142b..6004bfdb7b7 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,8 +1,11 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:pipeline) { FactoryGirl.create :ci_pipeline }
- let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
+ let(:pipeline) { create(:ci_pipeline) }
+
+ let(:generic_commit_status) do
+ create(:generic_commit_status, pipeline: pipeline)
+ end
describe '#context' do
subject { generic_commit_status.context }
@@ -17,6 +20,15 @@ describe GenericCommitStatus, models: true do
it { is_expected.to eq([:external]) }
end
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns detailed status object' do
+ expect(generic_commit_status.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Success
+ end
+ end
+
describe 'set_default_values' do
before do
generic_commit_status.context = nil
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 40f1cf92e00..893c6827a91 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -270,4 +270,11 @@ describe Group, models: true do
expect(group.web_url).to include("groups/#{group.name}")
end
end
+
+ describe 'nested group' do
+ subject { create(:group, :nested) }
+
+ it { is_expected.to be_valid }
+ it { expect(subject.parent).to be_kind_of(Group) }
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bad6ed9e146..8b20ee81614 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -727,17 +727,6 @@ describe User, models: true do
end
end
- describe 'by_username_or_id' do
- let(:user1) { create(:user, username: 'foo') }
-
- it "gets the correct user" do
- expect(User.by_username_or_id(user1.id)).to eq(user1)
- expect(User.by_username_or_id('foo')).to eq(user1)
- expect(User.by_username_or_id(-1)).to be_nil
- expect(User.by_username_or_id('bar')).to be_nil
- end
- end
-
describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do
let(:user) { create(:user) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 55b8c8c0c69..2878e0cb59b 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -11,6 +11,7 @@ describe API::Branches, api: true do
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+ let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do
@@ -37,6 +38,13 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false)
end
+ it "returns the branch information for a single branch with dots in the name" do
+ get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ end
+
context 'on a merged branch' do
it "returns the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/merge-test", user)
@@ -71,6 +79,14 @@ describe API::Branches, api: true do
expect(json_response['developers_can_merge']).to eq(false)
end
+ it "protects a single branch with dots in the name" do
+ put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['protected']).to eq(true)
+ end
+
it 'protects a single branch and developers can push' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
developers_can_push: true
@@ -220,6 +236,14 @@ describe API::Branches, api: true do
expect(json_response['protected']).to eq(false)
end
+ it "update branches with dots in branch name" do
+ put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['protected']).to eq(false)
+ end
+
it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404)
@@ -292,6 +316,13 @@ describe API::Branches, api: true do
expect(json_response['branch_name']).to eq(branch_name)
end
+ it "removes a branch with dots in the branch name" do
+ delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['branch_name']).to eq("with.1.2.3")
+ end
+
it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index e497bce6943..964cded917c 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -456,6 +456,76 @@ describe API::Commits, api: true do
end
end
+ describe 'POST :id/repository/commits/:sha/cherry_pick' do
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+
+ context 'authorized user' do
+ it 'cherry picks a commit' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(master_pickable_commit.title)
+ expect(json_response['message']).to eq(master_pickable_commit.message)
+ expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
+ expect(json_response['committer_name']).to eq(user.name)
+ end
+
+ it 'returns 400 if commit is already included in the target branch' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Sorry, we cannot cherry-pick this commit automatically.
+ A cherry-pick may have already been performed with this commit, or a more recent commit may have updated some of its content.')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ project.team << [user2, :developer]
+ protected_branch = create(:protected_branch, project: project, name: 'feature')
+
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+
+ it 'returns 404 if commit is not found' do
+ post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Commit Not Found')
+ end
+
+ it 'returns 404 if branch is not found' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Branch Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('branch is missing')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not cherry pick the commit' do
+ post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
+
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe 'Post comment to commit' do
context 'authorized user' do
it 'returns comment' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 548ed8e1892..a75ba824e85 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -243,6 +243,28 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['default_branch']).to be_present
+ end
+
+ it "returns the group's projects with simple representation" do
+ get api("/groups/#{group1.id}/projects", user1), simple: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
+ expect(json_response.first['default_branch']).not_to be_present
+ end
+
+ it 'filters the groups projects' do
+ public_project = create(:project, :public, path: 'test1', group: group1)
+
+ get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(public_project.name)
end
it "does not return a non existing group" do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 3f34309f419..4035fd97af5 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe API::Helpers, api: true do
include API::Helpers
- include ApiHelpers
include SentryHelper
let(:user) { create(:user) }
@@ -13,18 +12,18 @@ describe API::Helpers, api: true do
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) }
- def set_env(token_usr, identifier)
+ def set_env(user_or_token, identifier)
clear_env
clear_param
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
- env[API::Helpers::SUDO_HEADER] = identifier
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+ env[API::Helpers::SUDO_HEADER] = identifier.to_s
end
- def set_param(token_usr, identifier)
+ def set_param(user_or_token, identifier)
clear_env
clear_param
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
- params[API::Helpers::SUDO_PARAM] = identifier
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+ params[API::Helpers::SUDO_PARAM] = identifier.to_s
end
def clear_env
@@ -163,6 +162,13 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user)
end
+ it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
+ set_env(admin, user.id)
+
+ expect(current_user).to eq(user)
+ expect(current_user).to eq(user)
+ end
+
it 'handles sudo to oneself' do
set_env(admin, admin.id)
@@ -294,33 +300,48 @@ describe API::Helpers, api: true do
end
end
- describe '.sudo_identifier' do
- it "returns integers when input is an int" do
- set_env(admin, '123')
- expect(sudo_identifier).to eq(123)
- set_env(admin, '0001234567890')
- expect(sudo_identifier).to eq(1234567890)
-
- set_param(admin, '123')
- expect(sudo_identifier).to eq(123)
- set_param(admin, '0001234567890')
- expect(sudo_identifier).to eq(1234567890)
+ describe '.sudo?' do
+ context 'when no sudo env or param is passed' do
+ before do
+ doorkeeper_guard_returns(nil)
+ end
+
+ it 'returns false' do
+ expect(sudo?).to be_falsy
+ end
end
- it "returns string when input is an is not an int" do
- set_env(admin, '12.30')
- expect(sudo_identifier).to eq("12.30")
- set_env(admin, 'hello')
- expect(sudo_identifier).to eq('hello')
- set_env(admin, ' 123')
- expect(sudo_identifier).to eq(' 123')
-
- set_param(admin, '12.30')
- expect(sudo_identifier).to eq("12.30")
- set_param(admin, 'hello')
- expect(sudo_identifier).to eq('hello')
- set_param(admin, ' 123')
- expect(sudo_identifier).to eq(' 123')
+ context 'when sudo env or param is passed', 'user is not an admin' do
+ before do
+ set_env(user, '123')
+ end
+
+ it 'returns an 403 Forbidden' do
+ expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
+ end
+ end
+
+ context 'when sudo env or param is passed', 'user is admin' do
+ context 'personal access token is used' do
+ before do
+ personal_access_token = create(:personal_access_token, user: admin)
+ set_env(personal_access_token.token, user.id)
+ end
+
+ it 'returns an 403 Forbidden' do
+ expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
+ end
+ end
+
+ context 'private access token is used' do
+ before do
+ set_env(admin.private_token, user.id)
+ end
+
+ it 'returns true' do
+ expect(sudo?).to be_truthy
+ end
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 75b270aa93c..f032d1b683d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -533,6 +533,22 @@ describe API::MergeRequests, api: true do
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end
+
+ it 'does not update state when title is empty' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+
+ it 'does not update state when target_branch is empty' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index d30361f53d4..668e39f9dba 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -2,6 +2,7 @@ require "spec_helper"
describe API::Services, api: true do
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
@@ -98,7 +99,7 @@ describe API::Services, api: true do
post api("/projects/#{project.id}/services/idonotexist/trigger")
expect(response).to have_http_status(404)
- expect(json_response["message"]).to eq("404 Service Not Found")
+ expect(json_response["error"]).to eq("404 Not Found")
end
end
@@ -114,7 +115,7 @@ describe API::Services, api: true do
end
it 'when the service is inactive' do
- post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
new file mode 100644
index 00000000000..f6fb6ea5506
--- /dev/null
+++ b/spec/requests/api/snippets_spec.rb
@@ -0,0 +1,157 @@
+require 'rails_helper'
+
+describe API::Snippets, api: true do
+ include ApiHelpers
+ let!(:user) { create(:user) }
+
+ describe 'GET /snippets/' do
+ it 'returns snippets available' do
+ public_snippet = create(:personal_snippet, :public, author: user)
+ private_snippet = create(:personal_snippet, :private, author: user)
+ internal_snippet = create(:personal_snippet, :internal, author: user)
+
+ get api("/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ internal_snippet.id,
+ private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last).to have_key('raw_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:personal_snippet, :private)
+
+ get api("/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'GET /snippets/public' do
+ let!(:other_user) { create(:user) }
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ let!(:private_snippet) { create(:personal_snippet, :private, author: user) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) }
+ let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) }
+ let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) }
+ let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) }
+
+ it 'returns all snippets with public visibility from all users' do
+ get api("/snippets/public", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
+ public_snippet.id,
+ public_snippet_other.id)
+ expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}",
+ "http://localhost/snippets/#{public_snippet_other.id}")
+ expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+ "http://localhost/snippets/#{public_snippet.id}/raw",
+ "http://localhost/snippets/#{public_snippet_other.id}/raw")
+ end
+ end
+
+ describe 'GET /snippets/:id/raw' do
+ let(:snippet) { create(:personal_snippet, author: user) }
+
+ it 'returns raw text' do
+ get api("/snippets/#{snippet.id}/raw", user)
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to eq(snippet.content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'POST /snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ content: 'puts "hello world"',
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ expect do
+ post api("/snippets/", user), params
+ end.to change { PersonalSnippet.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(params[:title])
+ expect(json_response['file_name']).to eq(params[:file_name])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post api("/snippets/", user), params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'PUT /snippets/:id' do
+ let(:other_user) { create(:user) }
+ let(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put api("/snippets/#{public_snippet.id}", user), content: new_content
+
+ expect(response).to have_http_status(200)
+ public_snippet.reload
+ expect(public_snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put api("/snippets/1234", user), title: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it "returns 404 for another user's snippet" do
+ put api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ put api("/snippets/1234", user)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'DELETE /snippets/:id' do
+ let!(:public_snippet) { create(:personal_snippet, :public, author: user) }
+ it 'deletes snippet' do
+ expect do
+ delete api("/snippets/#{public_snippet.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { PersonalSnippet.count }.by(-1)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete api("/snippets/1234", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index c37dbfa0a33..9e317f3a7e9 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -651,13 +651,12 @@ describe API::Users, api: true do
end
describe "GET /user" do
- let(:personal_access_token) { create(:personal_access_token, user: user) }
- let(:private_token) { user.private_token }
+ let(:personal_access_token) { create(:personal_access_token, user: user).token }
context 'with regular user' do
context 'with personal access token' do
it 'returns 403 without private token when sudo is defined' do
- get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
+ get api("/user?private_token=#{personal_access_token}&sudo=123")
expect(response).to have_http_status(403)
end
@@ -665,7 +664,7 @@ describe API::Users, api: true do
context 'with private token' do
it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{private_token}&sudo=#{user.id}")
+ get api("/user?private_token=#{user.private_token}&sudo=123")
expect(response).to have_http_status(403)
end
@@ -676,40 +675,44 @@ describe API::Users, api: true do
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(user.id)
end
end
context 'with admin' do
- let(:user) { create(:admin) }
+ let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
context 'with personal access token' do
it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}")
+ get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
expect(response).to have_http_status(403)
end
- it 'returns current user without private token when sudo not defined' do
- get api("/user?private_token=#{personal_access_token.token}")
+ it 'returns initial current user without private token when sudo not defined' do
+ get api("/user?private_token=#{admin_personal_access_token}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(admin.id)
end
end
context 'with private token' do
- it 'returns current user with private token when sudo defined' do
- get api("/user?private_token=#{private_token}&sudo=#{user.id}")
+ it 'returns sudoed user with private token when sudo defined' do
+ get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/login')
+ expect(json_response['id']).to eq(user.id)
end
- it 'returns current user without private token when sudo not defined' do
- get api("/user?private_token=#{private_token}")
+ it 'returns initial current user without private token when sudo not defined' do
+ get api("/user?private_token=#{admin.private_token}")
expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public')
+ expect(json_response['id']).to eq(admin.id)
end
end
end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 69eeb45ed71..661b671301e 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do
end
it "to #show" do
- expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab')
+ expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce')
+ expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce')
end
end
@@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do
expect(get("/admin/health_check")).to route_to('admin/health_check#show')
end
end
+
+describe Admin::GroupsController, "routing" do
+ it "to #index" do
+ expect(get("/admin/groups")).to route_to('admin/groups#index')
+ end
+
+ it "to #show" do
+ expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab')
+ expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup')
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 90b7e62bc6f..0e8adb68721 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -694,7 +694,7 @@ describe SystemNoteService, services: true do
describe "existing reference" do
before do
- message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'"
+ message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'"
allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index c0b3e83244d..ad1eed5b369 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,7 +75,8 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
- expect(page).to have_content('Signed out successfully')
+ # check the sign_in button
+ expect(page).to have_button('Sign in')
end
# Logout without JavaScript driver
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
index d1c999cad4d..66c93890e31 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/services_shared_context.rb
@@ -16,8 +16,14 @@ Service.available_services_names.each do |service|
hash.merge!(k => 'secrettoken')
elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com")
+ elsif service_klass.method_defined?("#{k}?")
+ hash.merge!(k => true)
elsif service == 'irker' && k == :recipients
hash.merge!(k => 'irc://irc.network.net:666/#channel')
+ elsif service == 'irker' && k == :server_port
+ hash.merge!(k => 1234)
+ elsif service == 'jira' && k == :jira_issue_transition_id
+ hash.merge!(k => 1234)
else
hash.merge!(k => "someword")
end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 16bf0698c4b..e741e3cf9b6 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -13,7 +13,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows the commit SHA' do
render
- expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}")
+ expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
end
it 'shows the last pipeline that ran for the commit' do
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index bf027499c94..a066ea078e6 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -28,7 +28,7 @@ describe 'projects/pipelines/show' do
it 'shows a graph with grouped stages' do
render
- expect(rendered).to have_css('.pipeline-graph')
+ expect(rendered).to have_css('.js-pipeline-graph')
expect(rendered).to have_css('.grouped-pipeline-dropdown')
# stages