summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--app/assets/javascripts/application.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es62
-rw-r--r--app/assets/javascripts/dispatcher.js.es65
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js4
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es68
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es611
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js101
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.es6126
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es614
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es620
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es62
-rw-r--r--app/assets/javascripts/sidebar.js.es623
-rw-r--r--app/assets/javascripts/todos.js.es620
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/labels.scss5
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss43
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss87
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb2
-rw-r--r--app/controllers/projects/compare_controller.rb3
-rw-r--r--app/controllers/projects/environments_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests_controller.rb18
-rw-r--r--app/finders/environments_finder.rb55
-rw-r--r--app/helpers/commits_helper.rb13
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/environment.rb37
-rw-r--r--app/models/group.rb7
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/project.rb36
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb2
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb4
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb4
-rw-r--r--app/models/repository.rb18
-rw-r--r--app/models/timelog.rb18
-rw-r--r--app/serializers/environment_entity.rb2
-rw-r--r--app/services/ci/stop_environments_service.rb7
-rw-r--r--app/services/projects/transfer_service.rb17
-rw-r--r--app/views/projects/blob/_actions.html.haml3
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml21
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/environments/_stop.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml16
-rw-r--r--app/views/projects/notes/_note.html.haml11
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml30
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml17
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml32
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml18
-rw-r--r--app/views/shared/issuable/_filter.html.haml10
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--changelogs/unreleased/24147-delete-env-button.yml4
-rw-r--r--changelogs/unreleased/24716-fix-ctrl-click-links.yml4
-rw-r--r--changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml4
-rw-r--r--changelogs/unreleased/26059-segoe-ui-vertical.yml4
-rw-r--r--changelogs/unreleased/26908-make-timelogs-use-foreign-keys4
-rw-r--r--changelogs/unreleased/27352-search-label-filter-header.yml4
-rw-r--r--changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml4
-rw-r--r--changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml4
-rw-r--r--changelogs/unreleased/27822-default-bulk-assign-labels.yml4
-rw-r--r--changelogs/unreleased/fix-anchor-scrolling.yml4
-rw-r--r--changelogs/unreleased/improve-handleLocationHash-tests.yml4
-rw-r--r--changelogs/unreleased/no-sidebar-on-action-btn-click.yml4
-rw-r--r--changelogs/unreleased/refresh-permissions-when-moving-projects.yml4
-rw-r--r--changelogs/unreleased/route-map.yml4
-rw-r--r--db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb54
-rw-r--r--db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb23
-rw-r--r--db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb32
-rw-r--r--db/schema.rb11
-rw-r--r--doc/ci/environments.md51
-rw-r--r--doc/ci/img/view_on_env_blob.pngbin0 -> 111663 bytes
-rw-r--r--doc/ci/img/view_on_env_mr.pngbin0 -> 1005195 bytes
-rw-r--r--doc/user/project/integrations/bamboo.md39
-rw-r--r--doc/user/project/integrations/bugzilla.md5
-rw-r--r--doc/user/project/integrations/builds_emails.md13
-rw-r--r--doc/user/project/integrations/emails_on_push.md9
-rw-r--r--doc/user/project/integrations/hipchat.md9
-rw-r--r--doc/user/project/integrations/img/accessing_integrations.pngbin0 -> 8941 bytes
-rw-r--r--doc/user/project/integrations/img/builds_emails_service.pngbin19203 -> 0 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_config_help.pngbin63138 -> 102890 bytes
-rw-r--r--doc/user/project/integrations/img/project_services.pngbin0 -> 25753 bytes
-rw-r--r--doc/user/project/integrations/img/slack_setup.pngbin126412 -> 86314 bytes
-rw-r--r--doc/user/project/integrations/index.md10
-rw-r--r--doc/user/project/integrations/irker.md5
-rw-r--r--doc/user/project/integrations/jira.md5
-rw-r--r--doc/user/project/integrations/kubernetes.md3
-rw-r--r--doc/user/project/integrations/mattermost.md5
-rw-r--r--doc/user/project/integrations/mattermost_slash_commands.md8
-rw-r--r--doc/user/project/integrations/project_services.md53
-rw-r--r--doc/user/project/integrations/redmine.md6
-rw-r--r--doc/user/project/integrations/slack.md5
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md11
-rw-r--r--doc/user/project/integrations/webhooks.md7
-rw-r--r--lib/gitlab/database.rb14
-rw-r--r--lib/gitlab/route_map.rb50
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb8
-rw-r--r--spec/factories/timelogs.rb2
-rw-r--r--spec/features/environment_spec.rb48
-rw-r--r--spec/features/environments_spec.rb16
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb218
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb20
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb100
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb (renamed from spec/features/projects/commits/cherry_pick_spec.rb)0
-rw-r--r--spec/features/projects/compare_spec.rb (renamed from spec/features/compare_spec.rb)0
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb22
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb21
-rw-r--r--spec/features/projects/view_on_env_spec.rb140
-rw-r--r--spec/finders/environments_finder_spec.rb110
-rw-r--r--spec/helpers/commits_helper_spec.rb19
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js.es62
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js.es611
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js.es68
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es62
-rw-r--r--spec/javascripts/environments/mock_data.js.es610
-rw-r--r--spec/javascripts/fixtures/projects.rb28
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es666
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js50
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es64
-rw-r--r--spec/javascripts/project_dashboard_spec.js.es686
-rw-r--r--spec/javascripts/test_bundle.js8
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js.es62
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es66
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js.es68
-rw-r--r--spec/lib/gitlab/database_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml4
-rw-r--r--spec/lib/gitlab/route_map_spec.rb90
-rw-r--r--spec/models/deployment_spec.rb4
-rw-r--r--spec/models/environment_spec.rb79
-rw-r--r--spec/models/group_spec.rb13
-rw-r--r--spec/models/merge_request_spec.rb16
-rw-r--r--spec/models/namespace_spec.rb7
-rw-r--r--spec/models/project_spec.rb170
-rw-r--r--spec/models/repository_spec.rb36
-rw-r--r--spec/models/timelog_spec.rb28
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb4
-rw-r--r--spec/services/projects/transfer_service_spec.rb26
-rw-r--r--spec/spec_helper.rb1
152 files changed, 2273 insertions, 694 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 21e01cf94f5..5ab3648d9f2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -451,9 +451,9 @@ pages:
script:
- mv public/ .public/
- mkdir public/
- - mv coverage public/coverage-ruby
- - mv coverage-javascript/default/ public/coverage-javascript/
- - mv eslint-report.html public/
+ - mv coverage/ public/coverage-ruby/ || true
+ - mv coverage-javascript/default/ public/coverage-javascript/ || true
+ - mv eslint-report.html public/ || true
artifacts:
paths:
- public
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 637fca4d4da..ea3f13bd00f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -247,5 +247,7 @@ window.ES6Promise.polyfill();
new Aside();
// bind sidebar events
new gl.Sidebar();
+
+ gl.utils.initTimeagoTimeout();
});
}).call(this);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
index f1b41911b73..11a3449d99a 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
+++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
@@ -26,7 +26,7 @@ class PipelinesStore {
*/
startTimeAgoLoops() {
const startTimeLoops = () => {
- this.timeLoopInterval = setInterval(() => {
+ this.timeLoopInterval = setInterval(function timeloopInterval() {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 1e764a950ca..f8efca76b13 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -160,6 +160,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:commit:pipelines':
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-table',
+ }).bindEvents();
+ break;
case 'projects:commits:show':
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index c290e1a8355..5cdf11c6a2c 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -78,8 +78,8 @@ require('../window')(function(w){
},
destroy: function() {
- if (this.listTemplate) {
- var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
}
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 6a3d996f69c..33a99231315 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -147,12 +147,12 @@ require('./environment_terminal_button');
},
/**
- * Returns the value of the `stoppable?` key provided in the response.
+ * Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
- isStoppable() {
- return this.model['stoppable?'];
+ hasStopAction() {
+ return this.model['stop_action?'];
},
/**
@@ -508,7 +508,7 @@ require('./environment_terminal_button');
</external-url-component>
</div>
- <div v-if="isStoppable && canCreateDeployment"
+ <div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop-url="model.stop_path">
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 0ee29a75c62..5becf688652 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -69,6 +69,9 @@
var hash = w.gl.utils.getLocationHash();
if (!hash) return;
+ // This is required to handle non-unicode characters in hash
+ hash = decodeURIComponent(hash);
+
var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix');
@@ -134,6 +137,14 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
+ gl.utils.isMetaClick = function(e) {
+ // Identify following special clicks
+ // 1) Cmd + Click on Mac (e.metaKey)
+ // 2) Ctrl + Click on PC (e.ctrlKey)
+ // 3) Middle-click or Mouse Wheel Click (e.which is 2)
+ return e.metaKey || e.ctrlKey || e.which === 2;
+ };
+
gl.utils.scrollToElement = function($el) {
var top = $el.offset().top;
gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
deleted file mode 100644
index 5128ffd8c6f..00000000000
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
-/* global timeago */
-/* global dateFormat */
-
-window.timeago = require('vendor/timeago');
-window.dateFormat = require('vendor/date.format');
-
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
-
- w.gl.utils.formatDate = function(datetime) {
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- };
-
- w.gl.utils.getDayName = function(date) {
- return this.days[date.getDay()];
- };
-
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
- if (setTimeago == null) {
- setTimeago = true;
- }
-
- $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
- var $el = $(this);
- $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
-
- if (setTimeago) {
- // Recreate with custom template
- $el.tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- });
- }
-
- $el.attr('data-timeago-rendered', true);
- gl.utils.renderTimeago($el);
- });
- };
-
- w.gl.utils.getTimeago = function() {
- var locale = function(number, index) {
- return [
- ['less than a minute ago', 'a while'],
- ['less than a minute ago', 'in %s seconds'],
- ['about a minute ago', 'in 1 minute'],
- ['%s minutes ago', 'in %s minutes'],
- ['about an hour ago', 'in 1 hour'],
- ['about %s hours ago', 'in %s hours'],
- ['a day ago', 'in 1 day'],
- ['%s days ago', 'in %s days'],
- ['a week ago', 'in 1 week'],
- ['%s weeks ago', 'in %s weeks'],
- ['a month ago', 'in 1 month'],
- ['%s months ago', 'in %s months'],
- ['a year ago', 'in 1 year'],
- ['%s years ago', 'in %s years']
- ][index];
- };
-
- timeago.register('gl_en', locale);
- return timeago();
- };
-
- w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
- var timefor;
- if (!time) {
- return '';
- }
- suffix || (suffix = 'remaining');
- expiredLabel || (expiredLabel = 'Past due');
- timefor = gl.utils.getTimeago().format(time).replace('in', '');
- if (timefor.indexOf('ago') > -1) {
- timefor = expiredLabel;
- } else {
- timefor = timefor.trim() + ' ' + suffix;
- }
- return timefor;
- };
-
- w.gl.utils.renderTimeago = function($element) {
- var timeagoInstance = gl.utils.getTimeago();
- timeagoInstance.render($element, 'gl_en');
- };
-
- w.gl.utils.getDayDifference = function(a, b) {
- var millisecondsPerDay = 1000 * 60 * 60 * 24;
- var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
- var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
-
- return Math.floor((date2 - date1) / millisecondsPerDay);
- };
- })(window);
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
new file mode 100644
index 00000000000..56300926188
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
@@ -0,0 +1,126 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
+/* global timeago */
+/* global dateFormat */
+
+window.timeago = require('vendor/timeago');
+window.dateFormat = require('vendor/date.format');
+
+(function() {
+ (function(w) {
+ var base;
+ var timeagoInstance;
+
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+ w.gl.utils.formatDate = function(datetime) {
+ return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+ };
+
+ w.gl.utils.getDayName = function(date) {
+ return this.days[date.getDay()];
+ };
+
+ w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
+ $timeagoEls.each((i, el) => {
+ el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
+
+ if (setTimeago) {
+ // Recreate with custom template
+ $(el).tooltip({
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+ });
+ }
+
+ el.classList.add('js-timeago-render');
+ });
+
+ gl.utils.renderTimeago($timeagoEls);
+ };
+
+ w.gl.utils.getTimeago = function() {
+ var locale;
+
+ if (!timeagoInstance) {
+ locale = function(number, index) {
+ return [
+ ['less than a minute ago', 'a while'],
+ ['less than a minute ago', 'in %s seconds'],
+ ['about a minute ago', 'in 1 minute'],
+ ['%s minutes ago', 'in %s minutes'],
+ ['about an hour ago', 'in 1 hour'],
+ ['about %s hours ago', 'in %s hours'],
+ ['a day ago', 'in 1 day'],
+ ['%s days ago', 'in %s days'],
+ ['a week ago', 'in 1 week'],
+ ['%s weeks ago', 'in %s weeks'],
+ ['a month ago', 'in 1 month'],
+ ['%s months ago', 'in %s months'],
+ ['a year ago', 'in 1 year'],
+ ['%s years ago', 'in %s years']
+ ][index];
+ };
+
+ timeago.register('gl_en', locale);
+ timeagoInstance = timeago();
+ }
+
+ return timeagoInstance;
+ };
+
+ w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
+ var timefor;
+ if (!time) {
+ return '';
+ }
+ suffix || (suffix = 'remaining');
+ expiredLabel || (expiredLabel = 'Past due');
+ timefor = gl.utils.getTimeago().format(time).replace('in', '');
+ if (timefor.indexOf('ago') > -1) {
+ timefor = expiredLabel;
+ } else {
+ timefor = timefor.trim() + ' ' + suffix;
+ }
+ return timefor;
+ };
+
+ w.gl.utils.cachedTimeagoElements = [];
+ w.gl.utils.renderTimeago = function($els) {
+ if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
+ w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
+ } else if ($els) {
+ w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
+ }
+
+ w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
+ };
+
+ w.gl.utils.updateTimeagoText = function(el) {
+ const timeago = gl.utils.getTimeago();
+ const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
+
+ if (el.textContent !== formattedDate) {
+ el.textContent = formattedDate;
+ }
+ };
+
+ w.gl.utils.initTimeagoTimeout = function() {
+ gl.utils.renderTimeago();
+
+ gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
+ };
+
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ };
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
index 107e85f1225..af1ba9ecaf3 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -82,12 +82,18 @@ require('./flash');
$(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .on('click', this.clickTab);
}
unbindEvents() {
$(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
}
showTab(e) {
@@ -95,6 +101,14 @@ require('./flash');
this.activateTab($(e.target).data('action'));
}
+ clickTab(e) {
+ if (e.target && gl.utils.isMetaClick(e)) {
+ const targetLink = e.target.getAttribute('href');
+ e.stopImmediatePropagation();
+ window.open(targetLink, '_blank');
+ }
+ }
+
tabShown(e) {
const $target = $(e.target);
const action = $target.data('action');
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 05b9a63765f..e5d2d706fc7 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -51,6 +51,8 @@ require('./smart_interval');
this.getCIStatus(false);
this.retrieveSuccessIcon();
+ this.initMiniPipelineGraph();
+
this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
@@ -66,6 +68,7 @@ require('./smart_interval');
incrementByFactorOf: 15000,
immediateExecution: true,
});
+
notifyPermissions();
}
@@ -236,17 +239,20 @@ require('./smart_interval');
case "failed":
case "canceled":
case "not_found":
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
+ break;
case "running":
- return this.setMergeButtonClass('btn-info');
+ this.setMergeButtonClass('btn-info');
+ break;
case "success":
case "success_with_warnings":
- return this.setMergeButtonClass('btn-create');
+ this.setMergeButtonClass('btn-create');
}
} else {
$('.ci_widget.ci-error').show();
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
}
+ this.initMiniPipelineGraph();
};
MergeRequestWidget.prototype.showCICoverage = function(coverage) {
@@ -269,6 +275,12 @@ require('./smart_interval');
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
};
+ MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-inline-mr-widget-graph:visible',
+ }).bindEvents();
+ };
+
return MergeRequestWidget;
})();
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
index 80549532ea9..4becbc32681 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -21,8 +21,6 @@
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
-
- this.bindEvents();
}
/**
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index ee172f2fa6f..cbb2ae9f1bd 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,9 +1,7 @@
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
-((global) => {
- let singleton;
-
+(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
@@ -23,11 +21,12 @@
class Sidebar {
constructor() {
- if (!singleton) {
- singleton = this;
- singleton.init();
+ if (!Sidebar.singleton) {
+ Sidebar.singleton = this;
+ Sidebar.singleton.init();
}
- return singleton;
+
+ return Sidebar.singleton;
}
init() {
@@ -39,7 +38,7 @@
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
- .on('click', 'html, body', (e) => this.handleClickEvent(e))
+ .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
@@ -88,10 +87,12 @@
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
- setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+ const sidebarContent = $(sidebarContentSelector);
+ setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
- global.Sidebar = Sidebar;
-})(window.gl || (window.gl = {}));
+ window.gl = window.gl || {};
+ gl.Sidebar = Sidebar;
+})();
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 96c7d927509..b07e62a8c30 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -146,14 +146,26 @@
}
goToTodoUrl(e) {
- const todoLink = $(this).data('url');
+ const todoLink = this.dataset.url;
+ let targetLink = e.target.getAttribute('href');
+
+ if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
+ targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
+ }
+
if (!todoLink) {
return;
}
- // Allow Meta-Click or Mouse3-click to open in a new tab
- if (e.metaKey || e.which === 2) {
+
+ if (gl.utils.isMetaClick(e)) {
e.preventDefault();
- return window.open(todoLink, '_blank');
+ // Meta-Click on username leads to different URL than todoLink.
+ // Turbolinks can resolve that URL, but window.open requires URL manually.
+ if (targetLink !== todoLink) {
+ return window.open(targetLink, '_blank');
+ } else {
+ return window.open(todoLink, '_blank');
+ }
} else {
return gl.utils.visitUrl(todoLink);
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 426596027de..2bfdb9f9601 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -307,3 +307,7 @@ ul.controls {
}
}
}
+
+ul.indent-list {
+ padding: 10px 0 0 30px;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4ef95d27f4f..9174976c4c6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -193,7 +193,7 @@
top: $header-height;
bottom: 0;
right: 0;
- z-index: 10;
+ z-index: 8;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 21d9b4c54ea..762b95a657c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -259,3 +259,8 @@
}
}
}
+
+.label-link {
+ display: inline-block;
+ vertical-align: text-top;
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 0c013915a63..b01d8d695d6 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -80,6 +80,10 @@
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-text-color;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
svg {
margin-right: 4px;
@@ -88,12 +92,20 @@
overflow: visible;
}
+ &> span {
+ padding-right: 4px;
+ }
+
&.ci-success_with_warnings {
i {
color: $gl-warning;
}
}
+
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
+ }
}
.mr-widget-body,
@@ -102,6 +114,37 @@
padding: $gl-padding;
}
+ .mr-widget-pipeline-graph {
+ flex-shrink: 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+
+ .ci-action-icon-wrapper {
+ line-height: 16px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ order: 1;
+ margin-top: $gl-padding-top;
+ border-radius: 3px;
+ background-color: $white-light;
+ border: 1px solid $gray-darker;
+ width: 100%;
+ text-align: center;
+
+ .dropdown-menu {
+ margin-left: -97.5px;
+ }
+
+ .arrow-up::before,
+ .arrow-up::after, {
+ margin-left: 97.5px;
+ }
+ }
+ }
+
.normal {
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 367a468e1ba..974100bdff0 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -183,52 +183,11 @@
}
}
- .stage-cell {
- font-size: 0;
- padding: 10px 4px;
-
- > .stage-container > div > button > span > svg,
- > .stage-container > button > svg {
- height: 22px;
- width: 22px;
- position: absolute;
- top: -1px;
- left: -1px;
- z-index: 2;
- overflow: visible;
- }
-
- .stage-container {
- display: inline-block;
- position: relative;
- height: 22px;
- margin: 3px 6px 3px 0;
-
- .tooltip {
- white-space: nowrap;
- }
-
- .tooltip-inner {
- padding: 3px 4px;
- }
-
- &:not(:last-child) {
- &::after {
- content: '';
- width: 7px;
- position: absolute;
- right: -7px;
- top: 10px;
- border-bottom: 2px solid $border-color;
- }
- }
- }
- }
-
.duration,
.finished-at {
color: $gl-text-color-secondary;
margin: 4px 0;
+ white-space: nowrap;
.fa {
font-size: 12px;
@@ -311,6 +270,48 @@
}
}
+.stage-cell {
+ font-size: 0;
+ padding: 10px 4px;
+
+ > .stage-container > div > button > span > svg,
+ > .stage-container > button > svg {
+ height: 22px;
+ width: 22px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ z-index: 2;
+ overflow: visible;
+ }
+
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ height: 22px;
+ margin: 3px 6px 3px 0;
+
+ .tooltip {
+ white-space: nowrap;
+ }
+
+ .tooltip-inner {
+ padding: 3px 4px;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 7px;
+ position: absolute;
+ right: -7px;
+ top: 10px;
+ border-bottom: 2px solid $border-color;
+ }
+ }
+ }
+}
+
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
@@ -666,7 +667,7 @@
vertical-align: bottom;
display: inline-block;
position: relative;
- font-weight: 200;
+ font-weight: normal;
}
// Dropdown button in mini pipeline graph
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9940263ae24..4c39fe98028 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end
def edit
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index f880a9862c6..e10d7992db7 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -94,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
+
+ @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end
def define_note_vars
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 321cde255c3..c6651254d70 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -57,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController
@diffs = @compare.diffs(diff_options)
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@diff_notes_disabled = true
@grouped_diff_discussions = {}
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 87cc36253f1..0ec8f5bd64a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
- @environments = project.environments
+ @environments = project.environments.includes(:last_deployment)
respond_to do |format|
format.html
@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def stop
- return render_404 unless @environment.stoppable?
+ return render_404 unless @environment.available?
- new_action = @environment.stop!(current_user)
- redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
+ stop_action = @environment.stop_with_action!(current_user)
+
+ if stop_action
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
+ else
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ end
end
def terminal
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 38a1946a71e..fbad66c5c40 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -103,6 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ @environment = @merge_request.environments_for(current_user).last
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@@ -227,9 +229,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html { define_new_vars }
format.json do
- render json: { pipelines: PipelineSerializer
+ define_pipelines_vars
+
+ render json: PipelineSerializer
.new(project: @project, user: @current_user)
- .represent(@pipelines) }
+ .represent(@pipelines)
end
end
end
@@ -248,7 +252,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@diff_notes_disabled = true
- render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) }
end
end
end
@@ -447,14 +453,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_environments_status
environments =
begin
- @merge_request.environments.map do |environment|
- next unless can?(current_user, :read_environment, environment)
-
+ @merge_request.environments_for(current_user).map do |environment|
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
- if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb
new file mode 100644
index 00000000000..a59f8c1efa3
--- /dev/null
+++ b/app/finders/environments_finder.rb
@@ -0,0 +1,55 @@
+class EnvironmentsFinder
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user, params = {})
+ @project, @current_user, @params = project, current_user, params
+ end
+
+ def execute
+ deployments = project.deployments
+ deployments =
+ if ref
+ deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
+ deployments.where(deployments_query, ref: ref.to_s)
+ elsif commit
+ deployments.where(sha: commit.sha)
+ else
+ deployments.none
+ end
+
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments = project.environments.available
+ .where(id: environment_ids).order_by_last_deployed_at.to_a
+
+ environments.select! do |environment|
+ Ability.allowed?(current_user, :read_environment, environment)
+ end
+
+ if ref && commit
+ environments.select! do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
+ if ref && params[:recently_updated]
+ environments.select! do |environment|
+ environment.recently_updated_on_branch?(ref)
+ end
+ end
+
+ environments
+ end
+
+ private
+
+ def ref
+ params[:ref].try(:to_s)
+ end
+
+ def commit
+ params[:commit]
+ end
+end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 6dcb624c4da..8aad39e148b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -194,7 +194,7 @@ module CommitsHelper
end
end
- def view_file_btn(commit_sha, diff_new_path, project)
+ def view_file_button(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)),
@@ -205,6 +205,17 @@ module CommitsHelper
end
end
+ def view_on_environment_button(commit_sha, diff_new_path, environment)
+ return unless environment && commit_sha
+
+ external_url = environment.external_url_for(diff_new_path, commit_sha)
+ return unless external_url
+
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+ icon('external-link')
+ end
+ end
+
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3c1a1ae5933..5213ea9d02b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,6 +9,7 @@ module Ci
belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable
+ has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -183,10 +184,6 @@ module Ci
success? && !last_deployment.try(:last?)
end
- def last_deployment
- deployments.last
- end
-
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fab8497ec7d..bbc358adb83 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -283,13 +283,7 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file ||= begin
- blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
- blob.load_all_data!(project.repository)
- blob.data
- rescue
- nil
- end
+ @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
end
def has_yaml_errors?
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 040e3a2884e..9cf83440784 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, as: :trackable, dependent: :destroy
+ has_many :timelogs, dependent: :destroy
end
def spend_time(options)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 91d85c2279b..afad001d50f 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
- def stoppable?
+ def stop_action?
stop_action.present?
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 577367f1eed..803060b3979 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
- has_many :deployments
+ has_many :deployments, dependent: :destroy
+ has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
+ scope :order_by_last_deployed_at, -> do
+ max_deployment_id_sql =
+ Deployment.select(Deployment.arel_table[:id].maximum).
+ where(Deployment.arel_table[:environment_id].eq(arel_table[:id])).
+ to_sql
+ order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
+ end
state_machine :state, initial: :available do
event :start do
@@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base
ref.to_s == last_deployment.try(:ref)
end
- def last_deployment
- deployments.last
- end
-
def nullify_external_url
self.external_url = nil if self.external_url.blank?
end
@@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit)
end
+ def last_deployed_at
+ last_deployment.try(:created_at)
+ end
+
def update_merge_request_metrics?
(environment_type || name) == "production"
end
@@ -110,15 +118,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
- def stoppable?
+ def stop_action?
available? && stop_action.present?
end
- def stop!(current_user)
- return unless stoppable?
+ def stop_with_action!(current_user)
+ return unless available?
- stop
- stop_action.play(current_user)
+ stop!
+ stop_action.play(current_user) if stop_action
end
def actions_for(environment)
@@ -171,6 +179,15 @@ class Environment < ActiveRecord::Base
self.slug = slugified
end
+ def external_url_for(path, commit_sha)
+ return unless self.external_url
+
+ public_path = project.public_path_for_source_path(path, commit_sha)
+ return unless public_path
+
+ [external_url, public_path].join('/')
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/group.rb b/app/models/group.rb
index 4cdfd022094..a5b92283daa 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -197,7 +197,12 @@ class Group < Namespace
end
def refresh_members_authorized_projects
- UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
+ UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
+ execute
+ end
+
+ def user_ids_for_project_authorizations
+ users_with_parents.pluck(:id)
end
def members_with_parents
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 082adcafcc8..43085f69105 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -715,18 +715,22 @@ class MergeRequest < ActiveRecord::Base
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
- def environments
+ def environments_for(current_user)
return [] unless diff_head_commit
- @environments ||= begin
- target_envs = target_project.environments_for(
- target_branch, commit: diff_head_commit, with_tags: true)
+ @environments ||= Hash.new do |h, current_user|
+ envs = EnvironmentsFinder.new(target_project, current_user,
+ ref: target_branch, commit: diff_head_commit, with_tags: true).execute
- source_envs = source_project.environments_for(
- source_branch, commit: diff_head_commit) if source_project
+ if source_project
+ envs.concat EnvironmentsFinder.new(source_project, current_user,
+ ref: source_branch, commit: diff_head_commit).execute
+ end
- (target_envs.to_a + source_envs.to_a).uniq
+ h[current_user] = envs.uniq
end
+
+ @environments[current_user]
end
def state_human_name
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 2fb2eb44aaa..c5713fb7818 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -213,6 +213,10 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
+ def user_ids_for_project_authorizations
+ [owner_id]
+ end
+
private
def repository_storage_paths
diff --git a/app/models/project.rb b/app/models/project.rb
index 7c5fdad5122..b45f22d94d9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1306,28 +1306,26 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit: nil, with_tags: false)
- deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
-
- environment_ids = deployments
- .where(deployments_query, ref.to_s)
- .group(:environment_id)
- .select(:environment_id)
-
- environments_found = environments.available
- .where(id: environment_ids).to_a
-
- return environments_found unless commit
-
- environments_found.select do |environment|
- environment.includes_commit?(commit)
+ def route_map_for(commit_sha)
+ @route_maps_by_commit ||= Hash.new do |h, sha|
+ h[sha] = begin
+ data = repository.route_map_for(sha)
+ next unless data
+
+ Gitlab::RouteMap.new(data)
+ rescue Gitlab::RouteMap::FormatError
+ nil
+ end
end
+
+ @route_maps_by_commit[commit_sha]
end
- def environments_recently_updated_on_branch(branch)
- environments_for(branch).select do |environment|
- environment.recently_updated_on_branch?(branch)
- end
+ def public_path_for_source_path(path, commit_sha)
+ map = route_map_for(commit_sha)
+ return unless map
+
+ map.public_path_for_source_path(path)
end
private
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 5eb1bd86e9d..8b5bc24fd3c 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
]
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index b0f7a42f9a3..56f42d63b2d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end
def title
- 'Mattermost Command'
+ 'Mattermost slash commands'
end
def description
- "Perform common operations on GitLab in Mattermost"
+ "Perform common operations in Mattermost"
end
def self.to_param
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index c34991e4262..2182c1c7e4b 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
- 'Slack Command'
+ 'Slack slash commands'
end
def description
- "Perform common operations on GitLab in Slack"
+ "Perform common operations in Slack"
end
def self.to_param
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7cf09c52bf4..d2d92a064a4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -464,6 +464,8 @@ class Repository
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
end
def blob_by_oid(oid)
@@ -1160,6 +1162,14 @@ class Repository
end
end
+ def route_map_for(sha)
+ blob_data_at(sha, '.gitlab/route-map.yml')
+ end
+
+ def gitlab_ci_yml_for(sha)
+ blob_data_at(sha, '.gitlab-ci.yml')
+ end
+
protected
def tree_entry_at(branch_name, path)
@@ -1186,6 +1196,14 @@ class Repository
private
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!(self)
+ blob.data
+ end
+
def git_action(index, action)
path = normalize_path(action[:file_path])
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index f768c4e3da5..e166cf69703 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -1,6 +1,22 @@
class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
+ validate :issuable_id_is_present
- belongs_to :trackable, polymorphic: true
+ belongs_to :issue
+ belongs_to :merge_request
belongs_to :user
+
+ def issuable
+ issue || merge_request
+ end
+
+ private
+
+ def issuable_id_is_present
+ if issue_id && merge_request_id
+ errors.add(:base, 'Only Issue ID or Merge Request ID is required')
+ elsif issuable.nil?
+ errors.add(:base, 'Issue or Merge Request ID is required')
+ end
+ end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 5d15eb8d3d3..4c017960628 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
- expose :stoppable?
+ expose :stop_action?
expose :environment_path do |environment|
namespace_project_environment_path(
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index cf590459cb2..42c72aba7dd 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -8,10 +8,9 @@ module Ci
return unless has_ref?
environments.each do |environment|
- next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project)
- environment.stop!(current_user)
+ environment.stop_with_action!(current_user)
end
end
@@ -22,8 +21,8 @@ module Ci
end
def environments
- @environments ||= project
- .environments_recently_updated_on_branch(@ref)
+ @environments ||=
+ EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute
end
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 20b049b5973..484700c8c29 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -25,9 +25,10 @@ module Projects
end
def transfer(project, new_namespace)
+ old_namespace = project.namespace
+
Project.transaction do
old_path = project.path_with_namespace
- old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
@@ -70,8 +71,11 @@ module Projects
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
- true
end
+
+ refresh_permissions(old_namespace, new_namespace)
+
+ true
end
def allowed_transfer?(current_user, project, namespace)
@@ -80,5 +84,14 @@ module Projects
namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace)
end
+
+ def refresh_permissions(old_namespace, new_namespace)
+ # This ensures we only schedule 1 job for every user that has access to
+ # the namespaces.
+ user_ids = old_namespace.user_ids_for_project_authorizations |
+ new_namespace.user_ids_for_project_authorizations
+
+ UserProjectAccessChangedService.new(user_ids).execute
+ end
end
end
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index 26531f0b1a6..7b9cfbbd067 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,3 +1,6 @@
+.btn-group
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
.btn-group.tree-btn-group
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank'
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index cdab1e1b1a6..ac0fd87fd8d 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -40,25 +40,8 @@
- else
Cant find HEAD commit for this branch
- %td.stage-cell
- - pipeline.stages.each do |stage|
- - if stage.status
- - detailed_status = stage.detailed_status(current_user)
- - icon_status = "#{detailed_status.icon}_borderless"
- - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
-
- .stage-container.dropdown.js-mini-pipeline-graph
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
- = custom_icon(icon_status)
- = icon('caret-down')
-
- %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
- .arrow-up
- .js-builds-dropdown-list.scrollable-menu
-
- .js-builds-dropdown-loading.builds-dropdown-loading.hidden
- %span.fa.fa-spinner.fa-spin
-
+ %td
+ = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 7afd3d80ef5..d5fc283aa8d 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,7 +9,7 @@
= render "ci_menu"
- else
.block-connector
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 9c8f58d4aea..0dfc9fe20ed 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
.light-well
.center
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 58c20e225c6..4b49bed835f 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,3 +1,4 @@
+- environment = local_assigns.fetch(:environment, nil)
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
@@ -30,4 +31,4 @@
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index fc478ccc995..75885badac9 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,3 +1,4 @@
+- environment = local_assigns.fetch(:environment, nil)
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
@@ -13,6 +14,7 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_btn(diff_commit.id, diff_file.new_path, project)
+ = view_file_button(diff_commit.id, diff_file.new_path, project)
+ = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
index 69848123c17..14a2d627203 100644
--- a/app/views/projects/environments/_stop.html.haml
+++ b/app/views/projects/environments/_stop.html.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 7800d6ac382..7036325fff8 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -12,7 +12,7 @@
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index f3be343daae..085b2fc2814 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -50,7 +50,7 @@
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
- = link_to_label(label, subject: issue.project)
+ = link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 513f0818169..4dbb97b3228 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -64,7 +64,7 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
- = link_to_label(label, subject: merge_request.project, type: :merge_request)
+ = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
- if merge_request.tasks?
&nbsp;
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
index 74367ab9b7b..627fc4e9671 100644
--- a/app/views/projects/merge_requests/_new_diffs.html.haml
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -1 +1 @@
-= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
+= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 38259faf62f..bd72310c16b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render "projects/merge_requests/show/pipelines", endpoint: link_to(url_for(params))
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 5f048d04b27..7f0913ea516 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index cbe534abedb..de4aa255bbd 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1 +1,3 @@
-= render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index ae134563ead..bef76f16ca7 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,16 +1,20 @@
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
- = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
- = ci_icon_for_status(status)
+ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
+ %div{ class: "ci-status-icon-#{status}" }
+ = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
+ = ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
- for
- = succeed "." do
- = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
+ %span
+ for
+ = succeed "." do
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 09339e520dd..4b1da9c73e5 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -9,9 +9,12 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- = link_to_member(note.project, note.author, avatar: false)
- .note-headline-light
+ %a.visible-xs{ href: user_path(note.author) }
= note.author.to_reference
+ = link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
+ .note-headline-light
+ %span.hidden-xs
+ = note.author.to_reference
- unless note.system
commented
- if note.system
@@ -23,7 +26,7 @@
.note-actions
- access = note_max_access_for_user(note)
- if access
- %span.note-role.hidden-xs= access
+ %span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
@@ -59,7 +62,7 @@
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 8ca4c51a064..3a323d94cc2 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,16 +1,19 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
-To setup this service:
-%ul.list-unstyled
+%p To setup this service:
+%ul.list-unstyled.indent-list
%li
1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Enable custom slash commands
+ = icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
- in Mattermost with these options:
-
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Mattermost team with these options:
%hr
.help-form
@@ -83,9 +86,14 @@ To setup this service:
%hr
-%ul.list-unstyled
+%ul.list-unstyled.indent-list
%li
- 3. After adding the slash command, paste the
-
- %strong token
+ 3. Paste the
+ %strong Token
into the field below
+ %li
+ 4. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Mattermost!
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index c1e576b42fc..a04fd5035a6 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,13 +1,16 @@
- enabled = Gitlab.config.mattermost.enabled
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Mattermost.
- %br
- See list of available commands in Mattermost after setting up this service,
- by entering
- %code /&lt;command_trigger_word&gt; help
-
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 04b9100acc6..0d973a20d4c 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,21 +1,25 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
-- run_actions_text = "Perform common operations on this project: #{pretty_name}"
+- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Slack.
- %br
- See list of available commands in Slack after setting up this service,
- by entering
- %code /&lt;command&gt; help
- %br
- %br
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
- unless @service.template?
- To setup this service:
- %ul.list-unstyled
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
%li
1.
- = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
in your Slack team with these options:
%hr
@@ -82,7 +86,7 @@
%hr
- %ul.list-unstyled
+ %ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
new file mode 100644
index 00000000000..b0778653d4e
--- /dev/null
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -0,0 +1,18 @@
+.stage-cell
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.dropdown{ class: klass }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
+ = custom_icon(icon_status)
+ = icon('caret-down')
+
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .arrow-up
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 2ad06dcf25b..f17ae9f28eb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -54,7 +54,7 @@
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
- = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
@@ -62,13 +62,13 @@
%a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
- = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
- = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 55360dadbc4..173fa922f56 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -101,7 +101,7 @@
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml
new file mode 100644
index 00000000000..14e80cacbfb
--- /dev/null
+++ b/changelogs/unreleased/24147-delete-env-button.yml
@@ -0,0 +1,4 @@
+---
+title: Adds back ability to stop all environments
+merge_request: 7379
+author:
diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
new file mode 100644
index 00000000000..13de5db5e41
--- /dev/null
+++ b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Ctrl+Click support for Todos and Merge Request page tabs
+merge_request: 8898
+author:
diff --git a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
new file mode 100644
index 00000000000..d35ad0be0db
--- /dev/null
+++ b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
@@ -0,0 +1,4 @@
+---
+title: Show organisation membership and delete comment on smaller viewports, plus change comment author name to username
+merge_request:
+author:
diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml
new file mode 100644
index 00000000000..fc3f1af5b61
--- /dev/null
+++ b/changelogs/unreleased/26059-segoe-ui-vertical.yml
@@ -0,0 +1,4 @@
+---
+title: Align Segoe UI label text
+merge_request:
+author:
diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
new file mode 100644
index 00000000000..0e8f7093b34
--- /dev/null
+++ b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
@@ -0,0 +1,4 @@
+---
+title: Refactor Timelogs structure to use foreign keys.
+merge_request: 8769
+author:
diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml
new file mode 100644
index 00000000000..191b530aee8
--- /dev/null
+++ b/changelogs/unreleased/27352-search-label-filter-header.yml
@@ -0,0 +1,4 @@
+---
+title: 27352-search-label-filter-header
+merge_request:
+author:
diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
new file mode 100644
index 00000000000..0531ef2c038
--- /dev/null
+++ b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
@@ -0,0 +1,4 @@
+---
+title: Layer award emoji dropdown over the right sidebar
+merge_request: 9004
+author:
diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
new file mode 100644
index 00000000000..aa89d9f9850
--- /dev/null
+++ b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
@@ -0,0 +1,4 @@
+---
+title: Give ci status text on pipeline graph a better font-weight
+merge_request:
+author:
diff --git a/changelogs/unreleased/27822-default-bulk-assign-labels.yml b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
new file mode 100644
index 00000000000..ee2431869f0
--- /dev/null
+++ b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Add default labels to bulk assign dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-anchor-scrolling.yml b/changelogs/unreleased/fix-anchor-scrolling.yml
new file mode 100644
index 00000000000..43b3b9bf96e
--- /dev/null
+++ b/changelogs/unreleased/fix-anchor-scrolling.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken anchor links when special characters are used
+merge_request: 8961
+author: Andrey Krivko
diff --git a/changelogs/unreleased/improve-handleLocationHash-tests.yml b/changelogs/unreleased/improve-handleLocationHash-tests.yml
new file mode 100644
index 00000000000..8ae3dfe079c
--- /dev/null
+++ b/changelogs/unreleased/improve-handleLocationHash-tests.yml
@@ -0,0 +1,4 @@
+---
+title: Improve gl.utils.handleLocationHash tests
+merge_request:
+author:
diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
new file mode 100644
index 00000000000..09e0b3a12d8
--- /dev/null
+++ b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
@@ -0,0 +1,4 @@
+---
+title: dismiss sidebar on repo buttons click
+merge_request: 8798
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
new file mode 100644
index 00000000000..a94bcdaa9a3
--- /dev/null
+++ b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Refresh authorizations when transferring projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml
new file mode 100644
index 00000000000..9b6df0c54af
--- /dev/null
+++ b/changelogs/unreleased/route-map.yml
@@ -0,0 +1,4 @@
+---
+title: Add 'View on [env]' link to blobs and individual files in diffs
+merge_request: 8867
+author:
diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
new file mode 100644
index 00000000000..69bfa2d3fc4
--- /dev/null
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -0,0 +1,54 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddForeignKeysToTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ change_table :timelogs do |t|
+ t.column :issue_id, :integer
+ t.column :merge_request_id, :integer
+ end
+
+ add_concurrent_index :timelogs, :issue_id
+ add_concurrent_index :timelogs, :merge_request_id
+
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID;
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID;
+ EOF
+ else
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;"
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;"
+ end
+
+ Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id")
+ Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id")
+ end
+
+ def down
+ Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
+ Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
+
+ remove_columns :timelogs, :issue_id, :merge_request_id
+ end
+end
diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
new file mode 100644
index 00000000000..89aa753646c
--- /dev/null
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ remove_columns :timelogs, :trackable_id, :trackable_type
+ end
+end
diff --git a/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
new file mode 100644
index 00000000000..f397ef919cc
--- /dev/null
+++ b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id";
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id";
+ EOF
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index aeb0d8210f0..850772ba356 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: 20170206071414) do
+ActiveRecord::Schema.define(version: 20170206101030) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1151,14 +1151,15 @@ ActiveRecord::Schema.define(version: 20170206071414) do
create_table "timelogs", force: :cascade do |t|
t.integer "time_spent", null: false
- t.integer "trackable_id"
- t.string "trackable_type"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "issue_id"
+ t.integer "merge_request_id"
end
- add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree
+ add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
+ add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
create_table "todos", force: :cascade do |t|
@@ -1340,6 +1341,8 @@ ActiveRecord::Schema.define(version: 20170206071414) do
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "subscriptions", "projects", on_delete: :cascade
+ add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
+ add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 579135c2052..cb62ed723f0 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -442,6 +442,57 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
+### Go directly from source files to public pages on the environment
+
+> Introduced in GitLab 8.17.
+
+To go one step further, we can specify a Route Map to get GitLab to show us "View on [environment URL]" buttons to go directly from a file to that file's representation on the deployed website. It will be exposed in a few places:
+
+| In the diff for a merge request, comparison or commit | In the file view |
+| ------ | ------ |
+| !["View on env" button in merge request diff](img/view_on_env_mr.png) | !["View on env" button in file view](img/view_on_env_blob.png) |
+
+To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map.
+
+A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website).
+
+This is an example of a route map for [Middleman](https://middlemanapp.com) static websites like [http://about.gitlab.com](https://gitlab.com/gitlab-com/www-gitlab-com):
+
+```yaml
+# Team data
+- source: 'data/team.yml' # data/team.yml
+ public: 'team/' # team/
+
+# Blogposts
+- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+# HTML files
+- source: /source\/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+# Other files
+- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+```
+
+Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys:
+
+- `source`
+ - a string, starting and ending with `'`, for an exact match
+ - a regular expression, starting and ending with `/`, for a pattern match
+ - The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
+ - Can include capture groups denoted by `()` that can be referred to in the `public` path.
+ - Slashes (`/`) can, but don't have to, be escaped as `\/`.
+ - Literal periods (`.`) should be escaped as `\.`.
+- `public`
+ - a string, starting and ending with `'`.
+ - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`.
+
+The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate.
+
+In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`.
+
---
We now have a full development cycle, where our app is tested, built, deployed
diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/img/view_on_env_blob.png
new file mode 100644
index 00000000000..f4fe99046f0
--- /dev/null
+++ b/doc/ci/img/view_on_env_blob.png
Binary files differ
diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/img/view_on_env_mr.png
new file mode 100644
index 00000000000..47ddb40bdc1
--- /dev/null
+++ b/doc/ci/img/view_on_env_mr.png
Binary files differ
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
index 51668128c62..cad4757f287 100644
--- a/doc/user/project/integrations/bamboo.md
+++ b/doc/user/project/integrations/bamboo.md
@@ -12,49 +12,48 @@ need to be configured in a Bamboo build plan before GitLab can integrate.
## Setup
-### Complete these steps in Bamboo:
+### Complete these steps in Bamboo
1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
-dropdown.
+ dropdown.
1. Select the 'Triggers' tab.
1. Click 'Add trigger'.
1. Enter a description such as 'GitLab trigger'
1. Choose 'Repository triggers the build when changes are committed'
1. Check one or more repositories checkboxes
-1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
-whitelist of IP addresses that are allowed to trigger Bamboo builds.
+1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
+ whitelist of IP addresses that are allowed to trigger Bamboo builds.
1. Save the trigger.
-1. In the left pane, select a build stage. If you have multiple build stages
-you want to select the last stage that contains the git checkout task.
+1. In the left pane, select a build stage. If you have multiple build stages
+ you want to select the last stage that contains the git checkout task.
1. Select the 'Miscellaneous' tab.
-1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
-in the 'Labels' box.
+1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
+ in the 'Labels' box.
1. Save
Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
-service in GitLab
+service in GitLab.
-### Complete these steps in GitLab:
+### Complete these steps in GitLab
1. Navigate to the project you want to configure to trigger builds.
-1. Select 'Settings' in the top navigation.
-1. Select 'Services' in the left navigation.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click 'Atlassian Bamboo CI'
1. Select the 'Active' checkbox.
1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
-1. Enter the build key from your Bamboo build plan. Build keys are a short,
-all capital letter, identifier that is unique. It will be something like PR-BLD
-1. If necessary, enter username and password for a Bamboo user that has
-access to trigger the build plan. Leave these fields blank if you do not require
-authentication.
+1. Enter the build key from your Bamboo build plan. Build keys are a short,
+ all capital letter, identifier that is unique. It will be something like PR-BLD
+1. If necessary, enter username and password for a Bamboo user that has
+ access to trigger the build plan. Leave these fields blank if you do not require
+ authentication.
1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
-will actually trigger a build in Bamboo.
+ will actually trigger a build in Bamboo.
## Troubleshooting
If builds are not triggered, these are a couple of things to keep in mind.
1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
-IP addresses'.
+ IP addresses'.
1. Remember that GitLab only triggers builds on push events. A commit via the
-web interface will not trigger CI currently.
+ web interface will not trigger CI currently.
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
index 215ed6fe9cc..0b219e84478 100644
--- a/doc/user/project/integrations/bugzilla.md
+++ b/doc/user/project/integrations/bugzilla.md
@@ -1,7 +1,8 @@
# Bugzilla Service
-Go to your project's **Settings > Services > Bugzilla** and fill in the required
-details as described in the table below.
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services),
+select the **Bugzilla** service and fill in the required details as described
+in the table below.
| Field | Description |
| ----- | ----------- |
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
index af0b1a287c7..f769dece242 100644
--- a/doc/user/project/integrations/builds_emails.md
+++ b/doc/user/project/integrations/builds_emails.md
@@ -1,7 +1,10 @@
-## Enabling build emails
+# Enabling build emails
-To receive e-mail notifications about the result status of your builds, visit
-your project's **Settings > Services > Builds emails** and activate the service.
+By enabling this service, you will be able to receive e-mail notifications about
+the result status of your builds.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Builds emails** service to configure it.
In the _Recipients_ area, provide a list of e-mails separated by comma.
@@ -10,7 +13,3 @@ e-mail notifications about each build's status.
If you enable the _Notify only broken builds_ option, e-mail notifications will
be sent only for failed builds.
-
----
-
-![Builds emails service settings](img/builds_emails_service.png)
diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md
index 2f9f36f962e..18109fc049c 100644
--- a/doc/user/project/integrations/emails_on_push.md
+++ b/doc/user/project/integrations/emails_on_push.md
@@ -1,7 +1,10 @@
-## Enabling emails on push
+# Enabling emails on push
-To receive email notifications for every change that is pushed to the project, visit
-your project's **Settings > Services > Emails on push** and activate the service.
+By enabling this service, you will be able to receive email notifications for
+every change that is pushed to your project.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Emails on push** service to configure it.
In the _Recipients_ area, provide a list of emails separated by commas.
diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md
index 021a93a288f..eee779c50d4 100644
--- a/doc/user/project/integrations/hipchat.md
+++ b/doc/user/project/integrations/hipchat.md
@@ -16,7 +16,7 @@ HipChat v2 API has tokens that are can be created using the Integrations tab
in the Group or Room admin page. By design, these are lightweight tokens that
allow GitLab to send messages only to *one* room.
-### Complete these steps in HipChat:
+### Complete these steps in HipChat
1. Go to: https://admin.hipchat.com/admin
1. Click on "Group Admin" -> "Integrations".
@@ -26,17 +26,16 @@ allow GitLab to send messages only to *one* room.
see a URL in the format:
```
- https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
+https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
```
HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
service in GitLab.
-### Complete these steps in GitLab:
+### Complete these steps in GitLab
1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click "HipChat".
1. Select the "Active" checkbox.
1. Insert the `token` field from the URL into the `Token` field on the Web page.
diff --git a/doc/user/project/integrations/img/accessing_integrations.png b/doc/user/project/integrations/img/accessing_integrations.png
new file mode 100644
index 00000000000..3b941f64998
--- /dev/null
+++ b/doc/user/project/integrations/img/accessing_integrations.png
Binary files differ
diff --git a/doc/user/project/integrations/img/builds_emails_service.png b/doc/user/project/integrations/img/builds_emails_service.png
deleted file mode 100644
index 9dbbed03833..00000000000
--- a/doc/user/project/integrations/img/builds_emails_service.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png
index a62e4b792f9..dd3481bc1f6 100644
--- a/doc/user/project/integrations/img/mattermost_config_help.png
+++ b/doc/user/project/integrations/img/mattermost_config_help.png
Binary files differ
diff --git a/doc/user/project/integrations/img/project_services.png b/doc/user/project/integrations/img/project_services.png
new file mode 100644
index 00000000000..25b6cd5690b
--- /dev/null
+++ b/doc/user/project/integrations/img/project_services.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png
index f69817f2b78..7928fb7d495 100644
--- a/doc/user/project/integrations/img/slack_setup.png
+++ b/doc/user/project/integrations/img/slack_setup.png
Binary files differ
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
index 766ffb1f65c..99093ebaed5 100644
--- a/doc/user/project/integrations/index.md
+++ b/doc/user/project/integrations/index.md
@@ -1,5 +1,11 @@
# Project integrations
+You can find the available integrations under the **Integrations** page by
+navigating to the cog icon in the upper right corner of your project. You need
+to have at least [master permission][permissions] on the project.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
## Project services
Project services allow you to integrate GitLab with other applications.
@@ -8,7 +14,7 @@ adding functionality to GitLab.
[Learn more about project services.](project_services.md)
-## Webhooks
+## Project webhooks
Project webhooks allow you to trigger a URL if for example new code is pushed or
a new issue is created. You can configure webhooks to listen for specific events
@@ -16,3 +22,5 @@ like pushes, issues or merge requests. GitLab will send a POST request with data
to the webhook URL.
[Learn more about webhooks.](webhooks.md)
+
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md
index 25c0c3ad2a6..c63ea1316fe 100644
--- a/doc/user/project/integrations/irker.md
+++ b/doc/user/project/integrations/irker.md
@@ -23,11 +23,10 @@ from the GitLab service.
If the Irker server runs on the same machine, you are done. If not, you will
need to follow the firsts steps of the next section.
-## Complete these steps in GitLab:
+## Complete these steps in GitLab
1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click "Irker".
1. Select the "Active" checkbox.
1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 233a2583c36..4c64d1e0907 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -90,8 +90,9 @@ password as they will be needed when configuring GitLab in the next section.
the configuration options you have to enter. If you are using an older version,
[follow this documentation][jira-repo-old-docs].
-To enable JIRA integration in a project, navigate to your project's
-**Services ➔ JIRA** and fill in the required details on the page as described
+To enable JIRA integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **JIRA** service, and fill in the required details on the page as described
in the table below.
| Field | Description |
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index 7b33327f6cf..cc67e667472 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -12,6 +12,9 @@ template, see the [Services Templates](services_templates.md) document.
## Configuration
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+of your project and select the **Kubernetes** service to configure it.
+
![Kubernetes configuration settings](img/kubernetes_configuration.png)
The Kubernetes service takes the following arguments:
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index fbc7dfeee6d..09ba9994d3a 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -18,8 +18,9 @@ Display name override is not enabled by default, you need to ask your admin to e
After you set up Mattermost, it's time to set up GitLab.
-Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
-checkbox with the following events that can be triggered:
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Mattermost notifications** service to configure it.
+There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md
index 9a09e6d37ea..488f61c77a3 100644
--- a/doc/user/project/integrations/mattermost_slash_commands.md
+++ b/doc/user/project/integrations/mattermost_slash_commands.md
@@ -53,9 +53,11 @@ the administrator console.
### Step 2. Open the Mattermost slash commands service in GitLab
-1. Open a new tab for GitLab and go to your project's settings
- **Services ➔ Mattermost command**. A screen will appear with all the values you
- need to copy in Mattermost as described in the next step. Leave the window open.
+1. Open a new tab for GitLab, go to your project's
+ [Integrations page](project_services.md#accessing-the-project-services)
+ and select the **Mattermost command** service to configure it.
+ A screen will appear with all the values you need to copy in Mattermost as
+ described in the next step. Leave the window open.
>**Note:**
GitLab will propose some values for the Mattermost settings. The only one
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 547d855d777..a3a163a4c6b 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -1,28 +1,31 @@
-# Project Services
+# Project services
-Project services allow you to integrate GitLab with other applications. Below
-is list of the currently supported ones.
+Project services allow you to integrate GitLab with other applications. They
+are a bit like plugins in that they allow a lot of freedom in adding
+functionality to GitLab.
-You can find these within GitLab in the Services page under Project Settings if
-you are at least a master on the project.
-Project Services are a bit like plugins in that they allow a lot of freedom in
-adding functionality to GitLab. For example there is also a service that can
-send an email every time someone pushes new commits.
+## Accessing the project services
-Because GitLab is open source we can ship with the code and tests for all
-plugins. This allows the community to keep the plugins up to date so that they
-always work in newer GitLab versions.
+You can find the available services under the **Integrations** page in your
+project's settings.
-For an overview of what projects services are available without logging in,
-please see the [project_services directory][projects-code].
+1. Navigate to the cog icon in the upper right corner of your project. You need
+ to have at least [master permission][permissions] on the project.
-[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
+ ![Accessing the services](img/accessing_integrations.png)
-Click on the service links to see
-further configuration instructions and details. Contributions are welcome.
+1. There are more than 20 services to integrate with. Click on the one that you
+ want to configure.
+
+ ![Project services list](img/project_services.png)
+
+Below, you will find a list of the currently supported ones accompanied with
+comprehensive documentation.
## Services
+Click on the service links to see further configuration instructions and details.
+
| Service | Description |
| ------- | ----------- |
| Asana | Asana - Teamwork without email |
@@ -51,9 +54,23 @@ further configuration instructions and details. Contributions are welcome.
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
-## Services Templates
+## Services templates
Services templates is a way to set some predefined values in the Service of
your liking which will then be pre-filled on each project's Service.
-Read more about [Services Templates in this document](services_templates.md).
+Read more about [Services templates in this document](services_templates.md).
+
+## Contributing to project services
+
+Because GitLab is open source we can ship with the code and tests for all
+plugins. This allows the community to keep the plugins up to date so that they
+always work in newer GitLab versions.
+
+For an overview of what projects services are available, please see the
+[project_services source directory][projects-code].
+
+Contributions are welcome!
+
+[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
index b9830ea7c38..89c0312d3c2 100644
--- a/doc/user/project/integrations/redmine.md
+++ b/doc/user/project/integrations/redmine.md
@@ -1,7 +1,9 @@
# Redmine Service
-Go to your project's **Settings > Services > Redmine** and fill in the required
-details as described in the table below.
+To enable the Redmine integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **Redmine** service, and fill in the required details on the page as described
+in the table below.
| Field | Description |
| ----- | ----------- |
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index eaceb2be137..57a9492044b 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -15,8 +15,9 @@ Slack:
After you set up Slack, it's time to set up GitLab.
-Go to your project's **Settings > Integrations > Slack Notifications** and you will see a
-checkbox with the following events that can be triggered:
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack notifications** service to configure it.
+There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
index 6efc2a168fa..56f1ba7311e 100644
--- a/doc/user/project/integrations/slack_slash_commands.md
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -5,19 +5,20 @@
Slack commands give users an extra interface to perform common operations
from the chat environment. This allows one to, for example, create an issue as
soon as the idea was discussed in chat.
-For all available commands try the help subcommand, for example: `/gitlab help`,
+For all available commands try the help subcommand, for example: `/gitlab help`,
all review the [full list of commands](../../../integration/chat_commands.md).
## Prerequisites
-A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you.
+A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
+Slack should be created beforehand, GitLab cannot create it for you.
## Configuration
-First, navigate to the Slack Slash commands service page, found at your project's
-**Settings** > **Services**, and you find the instructions there:
+Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack slash commands** service to configure it.
- ![Slack setup instructions](img/slack_setup.png)
+![Slack setup instructions](img/slack_setup.png)
Once you've followed the instructions, mark the service as active and insert the token
you've received from Slack. After saving the service you are good to go!
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 9d775355c4c..9df0c765f84 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -14,8 +14,11 @@ to the webhook URL.
Webhooks can be used to update an external issue tracker, trigger CI builds,
update a backup mirror, or even deploy to your production server.
-Navigate to the webhooks page by choosing **Webhooks** from your project's
-settings which can be found under the wheel icon in the upper right corner.
+Navigate to the webhooks page by going to the **Integrations** page from your
+project's settings which can be found under the wheel icon in the upper right
+corner.
+
+![Accessing the integrations](img/accessing_integrations.png)
## Webhook endpoint tips
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 55b8f888d53..dc2537d36aa 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -35,6 +35,20 @@ module Gitlab
order
end
+ def self.nulls_first_order(field, direction = 'ASC')
+ order = "#{field} #{direction}"
+
+ if Gitlab::Database.postgresql?
+ order << ' NULLS FIRST'
+ else
+ # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
+ # columns. In the (default) ascending order, `0` comes first.
+ order.prepend("#{field} IS NULL, ") if direction == 'DESC'
+ end
+
+ order
+ end
+
def self.random
Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
end
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
new file mode 100644
index 00000000000..72d00abfcc2
--- /dev/null
+++ b/lib/gitlab/route_map.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ class RouteMap
+ class FormatError < StandardError; end
+
+ def initialize(data)
+ begin
+ entries = YAML.safe_load(data)
+ rescue
+ raise FormatError, 'Route map is not valid YAML'
+ end
+
+ raise FormatError, 'Route map is not an array' unless entries.is_a?(Array)
+
+ @map = entries.map { |entry| parse_entry(entry) }
+ end
+
+ def public_path_for_source_path(path)
+ mapping = @map.find { |mapping| mapping[:source] === path }
+ return unless mapping
+
+ path.sub(mapping[:source], mapping[:public])
+ end
+
+ private
+
+ def parse_entry(entry)
+ raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
+ raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
+ raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
+
+ source_pattern = entry['source']
+ public_path = entry['public']
+
+ if source_pattern.start_with?('/') && source_pattern.end_with?('/')
+ source_pattern = source_pattern[1...-1].gsub('\/', '/')
+
+ begin
+ source_pattern = /\A#{source_pattern}\z/
+ rescue RegexpError => e
+ raise FormatError, "Route map entry source is not a valid regular expression: #{e}"
+ end
+ end
+
+ {
+ source: source_pattern,
+ public: public_path
+ }
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index d18e8c37901..63780802cfa 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -33,11 +33,17 @@ describe Projects::MergeRequestsController do
end
context 'when rendering JSON response' do
+ before do
+ create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id,
+ ref: 'remove-submodule',
+ project: fork_project)
+ end
+
it 'renders JSON including serialized pipelines' do
submit_new_merge_request(format: :json)
- expect(json_response).to have_key('pipelines')
expect(response).to be_ok
+ expect(json_response).not_to be_empty
end
end
end
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index 12fc4ec4486..6f1545418eb 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -4,6 +4,6 @@ FactoryGirl.define do
factory :timelog do
time_spent 3600
user
- association :trackable, factory: :issue
+ issue
end
end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
index 511c95b758f..2f49e89b4e4 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/environment_spec.rb
@@ -64,10 +64,6 @@ feature 'Environment', :feature do
expect(page).to have_link('Re-deploy')
end
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
-
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
@@ -116,27 +112,43 @@ feature 'Environment', :feature do
end
end
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ context 'when environment is available' do
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
- scenario 'does show stop button' do
- expect(page).to have_link('Stop')
- end
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop')
+ end
- scenario 'does allow to stop environment' do
- click_link('Stop')
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
- expect(page).to have_content('close_app')
- end
+ expect(page).to have_content('close_app')
+ end
- context 'for reporter' do
- let(:role) { :reporter }
+ context 'for reporter' do
+ let(:role) { :reporter }
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
end
end
+
+ context 'without stop action' do
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+ end
+ end
+ end
+
+ context 'when environment is stopped' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
end
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index c033b693213..78be7d36f47 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do
scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet')
end
+
+ context 'for available environment' do
+ given(:environment) { create(:environment, project: project, state: :available) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+
+ context 'for stopped environment' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
end
context 'with deployments' do
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 5079eb8dd00..c6a88e1b7b0 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,39 +1,47 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do
- include WaitForAjax
-
- let!(:project) { create(:empty_project) }
- let!(:user) { create(:user) }
- let!(:bug_label) { create(:label, project: project, title: 'bug') }
- let!(:uppercase_label) { create(:label, project: project, title: 'BUG') }
- let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
- let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
- let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
- let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')}
- let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')}
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_label) { '#js-dropdown-label' }
+ let(:filter_dropdown) { find("#{js_dropdown_label} .filter-dropdown") }
+
+ shared_context 'with labels' do
+ let!(:bug_label) { create(:label, project: project, title: 'bug-label') }
+ let!(:uppercase_label) { create(:label, project: project, title: 'BUG-LABEL') }
+ let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
+ let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
+ let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
+ let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()') }
+ let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') }
+ end
- def send_keys_to_filtered_search(input)
- input.split("").each do |i|
- filtered_search.send_keys(i)
- sleep 3
- wait_for_ajax
- sleep 3
- end
+ def init_label_search
+ filtered_search.set('label:')
+ # This ensures the dropdown is shown
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
- def dropdown_label_size
- page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size
+ def search_for_label(label)
+ init_label_search
+ filtered_search.send_keys(label)
end
def click_label(text)
- find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click
+ filter_dropdown.find('.filter-dropdown-item', text: text).click
+ end
+
+ def dropdown_label_size
+ filter_dropdown.all('.filter-dropdown-item').size
+ end
+
+ def clear_search_field
+ find('.filtered-search-input-container .clear-search').click
end
before do
- project.team << [user, :master]
+ project.add_master(user)
login_as(user)
create(:issue, project: project)
@@ -42,11 +50,12 @@ describe 'Dropdown label', js: true, feature: true do
describe 'keyboard navigation' do
it 'selects label' do
- send_keys_to_filtered_search('label:')
+ bug_label = create(:label, project: project, title: 'bug-label')
+ init_label_search
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq("label:~#{special_label.name} ")
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
end
@@ -54,171 +63,177 @@ describe 'Dropdown label', js: true, feature: true do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+ expect(page).to have_css(js_dropdown_label)
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened and hides it when loaded' do
filtered_search.set('label:')
- expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true)
- end
-
- it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('label:')
-
- expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading')
+ expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the labels when opened' do
- send_keys_to_filtered_search('label:')
+ it 'loads all the labels when opened' do
+ bug_label = create(:label, project: project, title: 'bug-label')
+ filtered_search.set('label:')
- expect(dropdown_label_size).to be > 0
+ expect(filter_dropdown).to have_content(bug_label.title)
+ expect(dropdown_label_size).to eq(1)
end
end
describe 'filtering' do
- before do
- filtered_search.set('label')
- end
-
- it 'filters by name' do
- send_keys_to_filtered_search(':b')
+ include_context 'with labels'
- expect(dropdown_label_size).to eq(2)
+ before do
+ init_label_search
end
- it 'filters by case insensitive name' do
- send_keys_to_filtered_search(':B')
+ it 'filters by case-insensitive name with or without symbol' do
+ search_for_label('b')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
expect(dropdown_label_size).to eq(2)
- end
- it 'filters by name with symbol' do
- send_keys_to_filtered_search(':~bu')
+ clear_search_field
+ init_label_search
- expect(dropdown_label_size).to eq(2)
- end
-
- it 'filters by case insensitive name with symbol' do
- send_keys_to_filtered_search(':~BU')
+ search_for_label('~bu')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
expect(dropdown_label_size).to eq(2)
end
- it 'filters by multiple words' do
- send_keys_to_filtered_search(':Hig')
+ it 'filters by multiple words with or without symbol' do
+ filtered_search.send_keys('Hig')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words with symbol' do
- send_keys_to_filtered_search(':~Hig')
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~Hig')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by multiple words containing single quotes' do
- send_keys_to_filtered_search(':won\'t')
+ it 'filters by multiple words containing single quotes with or without symbol' do
+ filtered_search.send_keys('won\'t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words containing single quotes with symbol' do
- send_keys_to_filtered_search(':~won\'t')
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~won\'t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by multiple words containing double quotes' do
- send_keys_to_filtered_search(':won"t')
+ it 'filters by multiple words containing double quotes with or without symbol' do
+ filtered_search.send_keys('won"t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words containing double quotes with symbol' do
- send_keys_to_filtered_search(':~won"t')
+ clear_search_field
+ init_label_search
+ filtered_search.send_keys('~won"t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by special characters' do
- send_keys_to_filtered_search(':^+')
+ it 'filters by special characters with or without symbol' do
+ filtered_search.send_keys('^+')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by special characters with symbol' do
- send_keys_to_filtered_search(':~^+')
+ clear_search_field
+ init_label_search
+ filtered_search.send_keys('~^+')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
end
describe 'selecting from dropdown' do
+ include_context 'with labels'
+
before do
- filtered_search.set('label:')
+ init_label_search
end
it 'fills in the label name when the label has not been filled' do
click_label(bug_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name when the label is partially filled' do
- send_keys_to_filtered_search('bu')
+ filtered_search.send_keys('bu')
click_label(bug_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{special_label.title} ")
end
it 'selects `no label`' do
- find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:none ")
end
end
@@ -226,44 +241,47 @@ describe 'Dropdown label', js: true, feature: true do
describe 'input has existing content' do
it 'opens label dropdown with existing search term' do
filtered_search.set('searchTerm label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing author' do
filtered_search.set('author:@person label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing assignee' do
filtered_search.set('assignee:@person label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing label' do
filtered_search.set('label:~urgent label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing milestone' do
filtered_search.set('milestone:%v2.0 label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
- filtered_search.set('label')
- send_keys_to_filtered_search(':')
- initial_size = dropdown_label_size
+ create(:label, project: project, title: 'bug-label')
+ init_label_search
- expect(initial_size).to be > 0
+ expect(dropdown_label_size).to eq(1)
create(:label, project: project)
- find('.filtered-search-input-container .clear-search').click
- filtered_search.set('label')
- send_keys_to_filtered_search(':')
+ clear_search_field
+ init_label_search
- expect(dropdown_label_size).to eq(initial_size)
+ expect(dropdown_label_size).to eq(1)
end
end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index f1b68a39343..e853fb7e016 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -84,4 +84,24 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_selector('#error_explanation')
expect(page).not_to have_content('The form contains the following error')
end
+
+ context 'when a new merge request has a pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, sha: project.commit('fix').id,
+ ref: 'fix',
+ project: project)
+ end
+
+ it 'shows pipelines for a new merge request' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace, project,
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ page.within('.merge-request') do
+ click_link 'Pipelines'
+
+ expect(page).to have_content "##{pipeline.id}"
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
new file mode 100644
index 00000000000..b08bd36bde9
--- /dev/null
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -0,0 +1,100 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph', :js, :feature do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
+ let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before do
+ build.run
+
+ login_as(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should display a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
+ end
+
+ describe 'build list toggle' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ it 'should expand when hovered' do
+ before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ toggle.hover
+
+ after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ expect(before_width).to be < after_width
+ end
+
+ it 'should show dropdown caret when hovered' do
+ toggle.hover
+
+ expect(toggle).to have_selector('.fa-caret-down')
+ end
+
+ it 'should show tooltip when hovered' do
+ toggle.hover
+
+ expect(toggle.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+
+ describe 'builds list menu' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ before do
+ toggle.click
+ wait_for_ajax
+ end
+
+ it 'should open when toggle is clicked' do
+ expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when toggle is clicked again' do
+ toggle.click
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when clicking somewhere else' do
+ find('body').click
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ describe 'build list build item' do
+ let(:build_item) do
+ find('.mini-pipeline-graph-dropdown-item')
+ first('.mini-pipeline-graph-dropdown-item')
+ end
+
+ it 'should visit the build page when clicked' do
+ build_item.click
+ find('.build-page')
+
+ expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build))
+ end
+
+ it 'should show tooltip when hovered' do
+ build_item.hover
+
+ expect(build_item.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7baf7913424..7baf7913424 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
diff --git a/spec/features/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e58..43eb4000e58 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index 042a1ccab51..f5adb53a2dc 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Setup Mattermost slash commands', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
@@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
end
- describe 'user visits the mattermost slash command config page', js: true do
+ describe 'user visits the mattermost slash command config page' do
it 'shows a help message' do
- wait_for_ajax
+ expect(page).to have_content("This service allows users to perform common")
+ end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content("This service allows GitLab users to perform common")
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
it 'shows the token after saving' do
@@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(true)
+ expect(select_element['disabled']).to eq('disabled')
expect(selected_option).to have_content(team_name.to_s)
end
@@ -93,7 +95,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(false)
+ expect(select_element['disabled']).to be(nil)
expect(selected_option).to have_content('Select team...')
# The 'Select team...' placeholder is item `0`.
expect(select_element.all('option').count).to eq(3)
@@ -135,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
+
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ end
end
end
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index 32b32f7ae8e..db903a0c8f0 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Slack slash commands', feature: true do
- include WaitForAjax
-
given(:user) { create(:user) }
given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service }
@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do
background do
project.team << [user, :master]
login_as(user)
- end
-
- scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
+ end
- wait_for_ajax
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content('This service allows GitLab users to perform common')
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- scenario 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ it 'shows a help message' do
+ expect(page).to have_content('This service allows users to perform common')
+ end
+ it 'shows the token after saving' do
fill_in 'service_token', with: 'token'
click_on 'Save'
@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do
expect(value).to eq('token')
end
- scenario 'shows the correct trigger url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
+ it 'shows the correct trigger url' do
value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
new file mode 100644
index 00000000000..ce5c5f21167
--- /dev/null
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'View on environment', js: true do
+ include WaitForAjax
+
+ let(:branch_name) { 'feature' }
+ let(:file_path) { 'files/ruby/feature.rb' }
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'when the branch has a route map' do
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /files/(.*)\\..*/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: route_map
+ ).execute
+
+ # Update the file so that we still have a commit that will have a file on the environment
+ Files::UpdateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Update feature",
+ file_path: file_path,
+ file_content: "# Noop"
+ ).execute
+ end
+
+ context 'and an active deployment' do
+ let(:sha) { project.commit(branch_name).sha }
+ let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') }
+ let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) }
+
+ context 'when visiting the diff of a merge request for the branch' do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) }
+
+ before do
+ login_as(user)
+
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ within '.diffs' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+
+ context 'when visiting a comparison for the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a comparison for the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_commit_path(project.namespace, project, sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
new file mode 100644
index 00000000000..0c063f6d5ee
--- /dev/null
+++ b/spec/finders/environments_finder_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe EnvironmentsFinder do
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+ let(:environment) { create(:environment, project: project) }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(described_class.new(project, user, ref: 'feature', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+
+ it 'returns environment when commit constraint is not set' do
+ expect(described_class.new(project, user, ref: 'master').execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'commit deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment' do
+ expect(described_class.new(project, user, commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'recently updated' do
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it 'finds recently updated environment' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it 'does not find environment' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to be_empty
+ end
+ end
+
+ context 'when there are two environments that deploy to the same branch' do
+ let(:second_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: second_environment, ref: 'feature')
+ end
+
+ it 'finds both environments' do
+ expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment, second_environment)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 727c25ff529..a2c008790f9 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -26,4 +26,23 @@ describe CommitsHelper do
not_to include('onmouseover="alert(1)"')
end
end
+
+ describe '#view_on_environment_button' do
+ let(:project) { create(:empty_project) }
+ let(:environment) { create(:environment, external_url: 'http://example.com') }
+ let(:path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ allow(environment).to receive(:external_url_for).with(path, sha).and_return('http://example.com/file.html')
+ end
+
+ it 'returns a link tag linking to the file in the environment' do
+ html = helper.view_on_environment_button(sha, path, environment)
+ node = Nokogiri::HTML.parse(html).at_css('a')
+
+ expect(node[:title]).to eq('View on example.com')
+ expect(node[:href]).to eq('http://example.com/file.html')
+ end
+ end
end
diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6
index 5f0f26a013c..188908d66bd 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js.es6
+++ b/spec/javascripts/commit/pipelines/mock_data.js.es6
@@ -88,3 +88,5 @@ const pipeline = {
created_at: '2017-01-16T17:13:59.800Z',
updated_at: '2017-01-25T00:00:17.132Z',
};
+
+module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
index c2b61632827..f09c57978a1 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
@@ -1,18 +1,17 @@
/* global pipeline, Vue */
-require('vue-resource');
-require('flash');
+require('~/flash');
require('~/commit/pipelines/pipelines_store');
require('~/commit/pipelines/pipelines_service');
require('~/commit/pipelines/pipelines_table');
-require('~vue_shared/vue_resource_interceptor');
-require('./mock_data');
+require('~/vue_shared/vue_resource_interceptor');
+const pipeline = require('./mock_data');
describe('Pipelines table in Commits and Merge requests', () => {
- preloadFixtures('pipelines_table');
+ preloadFixtures('static/pipelines_table.html.raw');
beforeEach(() => {
- loadFixtures('pipelines_table');
+ loadFixtures('static/pipelines_table.html.raw');
});
describe('successfull request', () => {
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
index a5a16544ffb..559723bafcc 100644
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
+++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
@@ -1,10 +1,10 @@
-require('~commit/pipelines/pipelines_store');
+require('~/commit/pipelines/pipelines_store');
describe('Store', () => {
- const store = gl.commits.pipelines.PipelinesStore;
+ let store;
beforeEach(() => {
- store.create();
+ store = new gl.commits.pipelines.PipelinesStore();
});
it('should start with a blank state', () => {
@@ -23,7 +23,7 @@ describe('Store', () => {
},
];
- store.store(pipelines);
+ store.storePipelines(pipelines);
expect(store.state.pipelines.length).toBe(pipelines.length);
});
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
index 9858f346c83..d87cc0996c9 100644
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ b/spec/javascripts/environments/environment_item_spec.js.es6
@@ -119,7 +119,7 @@ describe('Environment item', () => {
},
],
},
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
index 58f6fb96afb..80e1cbc6f4d 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -50,7 +50,7 @@ const environmentsList = [
},
manual_actions: [],
},
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -105,7 +105,7 @@ const environmentsList = [
},
manual_actions: [],
},
- 'stoppable?': false,
+ 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -116,7 +116,7 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -127,7 +127,7 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -143,7 +143,7 @@ const environment = {
external_url: 'http://production.',
environment_type: null,
last_deployment: {},
- 'stoppable?': false,
+ 'stop_action?': false,
environment_path: '/root/review-app/environments/4',
stop_path: '/root/review-app/environments/4/stop',
created_at: '2016-12-16T11:51:04.690Z',
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
new file mode 100644
index 00000000000..56513219e1e
--- /dev/null
+++ b/spec/javascripts/fixtures/projects.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ProjectsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('projects/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'projects/dashboard.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ id: project.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index fbb06f3948b..006ede21093 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -41,6 +41,40 @@ require('~/lib/utils/common_utils');
});
});
+ describe('gl.utils.handleLocationHash', () => {
+ beforeEach(() => {
+ spyOn(window.document, 'getElementById').and.callThrough();
+ });
+
+ function expectGetElementIdToHaveBeenCalledWith(elementId) {
+ expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
+ }
+
+ it('decodes hash parameter', () => {
+ window.history.pushState({}, null, '#random-hash');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('random-hash');
+ expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
+ });
+
+ it('decodes cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#definição');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+
+ it('decodes encoded cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+ });
+
describe('gl.utils.getParameterByName', () => {
beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2');
@@ -73,5 +107,37 @@ require('~/lib/utils/common_utils');
expect(normalized[NGINX].nginx).toBe('ok');
});
});
+
+ describe('gl.utils.isMetaClick', () => {
+ it('should identify meta click on Windows/Linux', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify meta click on macOS', () => {
+ const e = {
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify as meta click on middle-click or Mouse-wheel click', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+ });
});
})();
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index d20a59df041..92a0f1c05f7 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -61,6 +61,56 @@ require('vendor/jquery.scrollTo');
expect($('#diffs')).toHaveClass('active');
});
});
+ describe('#opensInNewTab', function () {
+ var commitsLink;
+ var tabUrl;
+
+ beforeEach(function () {
+ commitsLink = '.commits-tab li a';
+ tabUrl = $(commitsLink).attr('href');
+
+ spyOn($.fn, 'attr').and.returnValue(tabUrl);
+ });
+ it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ });
describe('#setCurrentAction', function () {
beforeEach(function () {
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
index a6994f6edf4..7cdade01e00 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
@@ -31,7 +31,7 @@ require('~/mini_pipeline_graph_dropdown');
it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
@@ -41,7 +41,7 @@ require('~/mini_pipeline_graph_dropdown');
it('should make a request to the endpoint provided in the html', () => {
const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6
new file mode 100644
index 00000000000..24833b4eb57
--- /dev/null
+++ b/spec/javascripts/project_dashboard_spec.js.es6
@@ -0,0 +1,86 @@
+require('~/sidebar');
+
+(() => {
+ describe('Project dashboard page', () => {
+ let $pageWithSidebar = null;
+ let $sidebarToggle = null;
+ let sidebar = null;
+ const fixtureTemplate = 'projects/dashboard.html.raw';
+
+ const assertSidebarStateExpanded = (shouldBeExpanded) => {
+ expect(sidebar.isExpanded).toBe(shouldBeExpanded);
+ expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded);
+ };
+
+ preloadFixtures(fixtureTemplate);
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+
+ $pageWithSidebar = $('.page-with-sidebar');
+ $sidebarToggle = $('.toggle-nav-collapse');
+
+ // otherwise instantiating the Sidebar for the second time
+ // won't do anything, as the Sidebar is a singleton class
+ gl.Sidebar.singleton = null;
+ sidebar = new gl.Sidebar();
+ });
+
+ it('can show the sidebar when the toggler is clicked', () => {
+ assertSidebarStateExpanded(false);
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+ });
+
+ it('should dismiss the sidebar when clone button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const cloneButton = $('.project-clone-holder a.clone-dropdown-btn');
+ cloneButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when download button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const downloadButton = $('.project-action-button .btn:has(i.fa-download)');
+ downloadButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when add button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const addButton = $('.project-action-button .btn:has(i.fa-plus)');
+ addButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when notification button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const notifButton = $('.js-notification-toggle-btns .notifications-btn');
+ notifButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when clicking on the body', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ $('body').click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when clicking on the project description header', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ $('.project-home-panel').click();
+ assertSidebarStateExpanded(false);
+ });
+ });
+})();
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index bf11ddbbea8..7df8d2fd8b4 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -34,7 +34,11 @@ testsContext.keys().forEach(function (path) {
try {
testsContext(path);
} catch (err) {
- console.error('[ERROR] WITH SPEC FILE: ', path);
- console.error(err);
+ console.error('[ERROR] Unable to load spec: ', path);
+ describe('Test bundle', function () {
+ it(`includes '${path}'`, function () {
+ expect(err).toBeNull();
+ });
+ });
}
});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6
index 84d169a3fb9..15ab10b9b69 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/commit_spec.js.es6
@@ -1,4 +1,4 @@
-require('~/vue_shared/components/committ');
+require('~/vue_shared/components/commit');
describe('Commit component', () => {
let props;
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
index e83a1749e82..412abfd5e41 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
@@ -1,7 +1,5 @@
-/* global pipeline */
-
-require('~vue_shared/components/pipelines_table_row');
-require('./mock_data');
+require('~/vue_shared/components/pipelines_table_row');
+const pipeline = require('../../commit/pipelines/mock_data');
describe('Pipelines Table Row', () => {
let component;
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
index adc9ea904cc..54d81e2ea7d 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
@@ -1,8 +1,6 @@
-/* global pipeline */
-
-require('~vue_shared/components/pipelines_table');
-require('~lib/utils/datetime_utility');
-require('./mock_data');
+require('~/vue_shared/components/pipelines_table');
+require('~/lib/utils/datetime_utility');
+const pipeline = require('../../commit/pipelines/mock_data');
describe('Pipelines Table', () => {
preloadFixtures('static/environments/element.html.raw');
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 3031559c613..b142b3a2781 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do
end
end
+ describe '.nulls_first_order' do
+ context 'when using PostgreSQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(true) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'}
+ end
+
+ context 'when using MySQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(false) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'}
+ end
+ end
+
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5231ab0ba3f..06617f3b007 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -203,5 +203,6 @@ award_emoji:
priorities:
- label
timelogs:
-- trackable
+- issue
+- merge_request
- user
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 95b230e4f5c..c5ac702d831 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -350,8 +350,8 @@ LabelPriority:
Timelog:
- id
- time_spent
-- trackable_id
-- trackable_type
+- merge_request_id
+- issue_id
- user_id
- created_at
- updated_at
diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb
new file mode 100644
index 00000000000..2370f56a613
--- /dev/null
+++ b/spec/lib/gitlab/route_map_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::RouteMap, lib: true do
+ describe '#initialize' do
+ context 'when the data is not YAML' do
+ it 'raises an error' do
+ expect { described_class.new('"') }.
+ to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/)
+ end
+ end
+
+ context 'when the data is not a YAML array' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump('foo')) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /an array/)
+ end
+ end
+
+ context 'when an entry is not a hash' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump(['foo'])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /a hash/)
+ end
+ end
+
+ context 'when an entry does not have a source key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /source key/)
+ end
+ end
+
+ context 'when an entry does not have a public key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /public key/)
+ end
+ end
+
+ context 'when an entry source is not a valid regex' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /regular expression/)
+ end
+ end
+
+ context 'when all is good' do
+ it 'returns a route map' do
+ route_map = described_class.new(YAML.dump([{ 'source' => 'index.haml', 'public' => 'index.html' }, { 'source' => '/(.*)\.md/', 'public' => '\1.html' }]))
+
+ expect(route_map.public_path_for_source_path('index.haml')).to eq('index.html')
+ expect(route_map.public_path_for_source_path('foo.md')).to eq('foo.html')
+ end
+ end
+ end
+
+ describe '#public_path_for_source_path' do
+ subject do
+ described_class.new(<<-'MAP'.strip_heredoc)
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+
+ # Blogposts
+ - source: /source/posts/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+ # HTML files
+ - source: /source/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+ # Other files
+ - source: /source/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+ MAP
+ end
+
+ it 'returns the public path for a provided source path' do
+ expect(subject.public_path_for_source_path('data/team.yml')).to eq('team/')
+
+ expect(subject.public_path_for_source_path('source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb')).to eq('2017/01/30/around-the-world-in-6-releases/')
+
+ expect(subject.public_path_for_source_path('source/index.html.haml')).to eq('index.html')
+
+ expect(subject.public_path_for_source_path('source/images/blogimages/around-the-world-in-6-releases-cover.png')).to eq('images/blogimages/around-the-world-in-6-releases-cover.png')
+
+ expect(subject.public_path_for_source_path('.gitlab/route-map.yml')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index fc4435a2f64..080ff2f3f43 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -77,8 +77,8 @@ describe Deployment, models: true do
end
end
- describe '#stoppable?' do
- subject { deployment.stoppable? }
+ describe '#stop_action?' do
+ subject { deployment.stop_action? }
context 'when no other actions' do
let(:deployment) { build(:deployment) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index eba392044bf..960f29f3805 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -7,8 +7,6 @@ describe Environment, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
- it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
-
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
@@ -22,6 +20,20 @@ describe Environment, models: true do
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
+ describe '.order_by_last_deployed_at' do
+ let(:project) { create(:project) }
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:environment3) { create(:environment, project: project) }
+ let!(:deployment1) { create(:deployment, environment: environment1) }
+ let!(:deployment2) { create(:deployment, environment: environment2) }
+ let!(:deployment3) { create(:deployment, environment: environment1) }
+
+ it 'returns the environments in order of having been last deployed' do
+ expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1])
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
@@ -112,8 +124,8 @@ describe Environment, models: true do
end
end
- describe '#stoppable?' do
- subject { environment.stoppable? }
+ describe '#stop_action?' do
+ subject { environment.stop_action? }
context 'when no other actions' do
it { is_expected.to be_falsey }
@@ -142,17 +154,39 @@ describe Environment, models: true do
end
end
- describe '#stop!' do
+ describe '#stop_with_action!' do
let(:user) { create(:user) }
- subject { environment.stop!(user) }
+ subject { environment.stop_with_action!(user) }
before do
- expect(environment).to receive(:stoppable?).and_call_original
+ expect(environment).to receive(:available?).and_call_original
end
context 'when no other actions' do
- it { is_expected.to be_nil }
+ context 'environment is available' do
+ before do
+ environment.update(state: :available)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
+
+ context 'environment is already stopped' do
+ before do
+ environment.update(state: :stopped)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
end
context 'when matching action is defined' do
@@ -301,4 +335,33 @@ describe Environment, models: true do
end
end
end
+
+ describe '#external_url_for' do
+ let(:source_path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ environment.external_url = 'http://example.com'
+ end
+
+ context 'when the public path is not known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(environment.external_url_for(source_path, sha)).to be_nil
+ end
+ end
+
+ context 'when the public path is known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return('file.html')
+ end
+
+ it 'returns the full external URL' do
+ expect(environment.external_url_for(source_path, sha)).to eq('http://example.com/file.html')
+ end
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 9ca50555191..a4e6eb4e3a6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -300,4 +300,17 @@ describe Group, models: true do
expect(group.members_with_parents).to include(master)
end
end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ master = create(:user)
+ developer = create(:user)
+
+ group.add_user(master, GroupMember::MASTER)
+ group.add_user(developer, GroupMember::DEVELOPER)
+
+ expect(group.user_ids_for_project_authorizations).
+ to include(master.id, developer.id)
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 32ed1e96749..e1e99300489 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1005,10 +1005,16 @@ describe MergeRequest, models: true do
end
end
- describe "#environments" do
+ describe "#environments_for" do
let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
+ before do
+ merge_request.source_project.add_master(user)
+ merge_request.target_project.add_master(user)
+ end
+
context 'with multiple environments' do
let(:environments) { create_list(:environment, 3, project: project) }
@@ -1018,7 +1024,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(environments.first)
+ expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
end
end
@@ -1042,7 +1048,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
end
context 'with environments on target project' do
@@ -1053,7 +1059,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
end
end
end
@@ -1064,7 +1070,7 @@ describe MergeRequest, models: true do
end
it 'returns an empty array' do
- expect(merge_request.environments).to be_empty
+ expect(merge_request.environments_for(user)).to be_empty
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4e96f19eb6f..7bb1657bc3a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -218,4 +218,11 @@ describe Namespace, models: true do
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end
end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ expect(namespace.user_ids_for_project_authorizations).
+ to eq([namespace.owner_id])
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d7e6da02261..2129bcbd74d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1716,100 +1716,6 @@ describe Project, models: true do
end
end
- describe '#environments_for' do
- let(:project) { create(:project, :repository) }
- let(:environment) { create(:environment, project: project) }
-
- context 'tagged deployment' do
- before do
- create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
- end
-
- it 'returns environment when with_tags is set' do
- expect(project.environments_for('master', commit: project.commit, with_tags: true))
- .to contain_exactly(environment)
- end
-
- it 'does not return environment when no with_tags is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to be_empty
- end
-
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
- end
- end
-
- context 'branch deployment' do
- before do
- create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
- end
-
- it 'returns environment when ref is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to contain_exactly(environment)
- end
-
- it 'does not environment when ref is different' do
- expect(project.environments_for('feature', commit: project.commit))
- .to be_empty
- end
-
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
- end
-
- it 'returns environment when commit constraint is not set' do
- expect(project.environments_for('master'))
- .to contain_exactly(environment)
- end
- end
- end
-
- describe '#environments_recently_updated_on_branch' do
- let(:project) { create(:project, :repository) }
- let(:environment) { create(:environment, project: project) }
-
- context 'when last deployment to environment is the most recent one' do
- before do
- create(:deployment, environment: environment, ref: 'feature')
- end
-
- it 'finds recently updated environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment)
- end
- end
-
- context 'when last deployment to environment is not the most recent' do
- before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: environment, ref: 'master')
- end
-
- it 'does not find environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to be_empty
- end
- end
-
- context 'when there are two environments that deploy to the same branch' do
- let(:second_environment) { create(:environment, project: project) }
-
- before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: second_environment, ref: 'feature')
- end
-
- it 'finds both environments' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment, second_environment)
- end
- end
- end
-
describe '#deployment_variables' do
context 'when project has no deployment service' do
let(:project) { create(:empty_project) }
@@ -1858,6 +1764,82 @@ describe Project, models: true do
it { expect(Project.inside_path(path)).to eq([project1]) }
end
+ describe '#route_map_for' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab/route-map.yml at the commit' do
+ context 'when the route map is valid' do
+ it 'returns a route map' do
+ map = project.route_map_for(project.commit.sha)
+ expect(map).to be_a_kind_of(Gitlab::RouteMap)
+ end
+ end
+
+ context 'when the route map is invalid' do
+ let(:route_map) { 'INVALID' }
+
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.sha)).to be_nil
+ end
+ end
+ end
+
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.parent.sha)).to be_nil
+ end
+ end
+ end
+
+ describe '#public_path_for_source_path' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ Gitlab::RouteMap.new(<<-MAP.strip_heredoc)
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+ let(:sha) { project.commit.id }
+
+ context 'when there is a route map' do
+ before do
+ allow(project).to receive(:route_map_for).with(sha).and_return(route_map)
+ end
+
+ context 'when the source path is mapped' do
+ it 'returns the public path' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to eq('file.html')
+ end
+ end
+
+ context 'when the source path is not mapped' do
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('file.html', sha)).to be_nil
+ end
+ end
+ end
+
+ context 'when there is no route map' do
+ before do
+ allow(project).to receive(:route_map_for).with(sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil
+ end
+ end
+ end
+
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 53b98ba05f8..9bfa6409607 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1782,4 +1782,40 @@ describe Repository, models: true do
repository.refresh_method_caches(%i(readme license))
end
end
+
+ describe '#gitlab_ci_yml_for' do
+ before do
+ repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab-ci.yml at the commit' do
+ it 'returns the content' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab-ci.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
+
+ describe '#route_map_for' do
+ before do
+ repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab/route-map.yml at the commit' do
+ it 'returns the content' do
+ expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index f08935b6425..ebc694213b6 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -2,9 +2,37 @@ require 'rails_helper'
RSpec.describe Timelog, type: :model do
subject { build(:timelog) }
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
it { is_expected.to validate_presence_of(:user) }
+
+ describe 'Issuable validation' do
+ it 'is invalid if issue_id and merge_request_id are missing' do
+ subject.attributes = { issue: nil, merge_request: nil }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is invalid if issue_id and merge_request_id are set' do
+ subject.attributes = { issue: issue, merge_request: merge_request }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is valid if only issue_id is set' do
+ subject.attributes = { issue: issue, merge_request: nil }
+
+ expect(subject).to be_valid
+ end
+
+ it 'is valid if only merge_request_id is set' do
+ subject.attributes = { merge_request: merge_request, issue: nil }
+
+ expect(subject).to be_valid
+ end
+ end
end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 6f7d1a5d28d..560f83d94f7 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -42,10 +42,10 @@ describe Ci::StopEnvironmentsService, services: true do
end
end
- context 'when environment is not stoppable' do
+ context 'when environment is not stopped' do
before do
allow_any_instance_of(Environment)
- .to receive(:stoppable?).and_return(false)
+ .to receive(:state).and_return(:stopped)
end
it 'does not stop environment' do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 5d5812c2c15..5c6fbea8d0e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -83,4 +83,30 @@ describe Projects::TransferService, services: true do
transfer_project(project, user, group)
end
end
+
+ describe 'refreshing project authorizations' do
+ let(:group) { create(:group) }
+ let(:owner) { project.namespace.owner }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_user(owner, GroupMember::MASTER)
+ group.add_user(group_member, GroupMember::DEVELOPER)
+ end
+
+ it 'refreshes the permissions of the old and new namespace' do
+ transfer_project(project, owner, group)
+
+ expect(group_member.authorized_projects).to include(project)
+ expect(owner.authorized_projects).to include(project)
+ end
+
+ it 'only schedules a single job for every user' do
+ expect(UserProjectAccessChangedService).to receive(:new).
+ with([owner.id, group_member.id]).
+ and_call_original
+
+ transfer_project(project, owner, group)
+ end
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ab38dac65c5..5fda7c63cdb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -35,6 +35,7 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include WaitForAjax, type: :feature
config.include StubConfiguration
config.include EmailHelpers, type: :mailer
config.include TestEnv