summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/build.js125
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es613
-rw-r--r--app/assets/javascripts/issuable.js.es614
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js13
-rw-r--r--app/assets/stylesheets/pages/builds.scss150
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss8
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss70
-rw-r--r--app/assets/stylesheets/pages/projects.scss20
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/mattermosts_controller.rb43
-rw-r--r--app/helpers/mattermost_helper.rb9
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb30
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/validators/project_path_validator.rb3
-rw-r--r--app/views/projects/builds/show.html.haml19
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml12
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml44
-rw-r--r--app/views/projects/mattermosts/new.html.haml8
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml2
-rw-r--r--app/views/projects/services/_form.html.haml1
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml91
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml94
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml7
-rw-r--r--app/views/shared/empty_states/_issues.html.haml6
-rw-r--r--app/views/shared/icons/_mattermost_logo.svg.erb1
-rw-r--r--app/views/shared/icons/_scroll_down.svg3
-rw-r--r--app/views/shared/icons/_scroll_down_hover_active.svg3
-rw-r--r--app/views/shared/icons/_scroll_up.svg3
-rw-r--r--app/views/shared/icons/_scroll_up_hover_active.svg3
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--changelogs/unreleased/19620-auto-scroll-log.yml4
-rw-r--r--changelogs/unreleased/22742-filter-protocol-relative-urls.yml4
-rw-r--r--changelogs/unreleased/25368-fix-left-align-system-note.yml4
-rw-r--r--changelogs/unreleased/25898-ci-icon-color-mr.yml4
-rw-r--r--changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml4
-rw-r--r--changelogs/unreleased/25908-fix-grape-after-update.yml4
-rw-r--r--changelogs/unreleased/dz-rename-invalid-groups.yml4
-rw-r--r--changelogs/unreleased/dz-whitelist-dashboard-project-path.yml4
-rw-r--r--changelogs/unreleased/dz-whitelist-more-project-names.yml4
-rw-r--r--changelogs/unreleased/fix-copy-issues-empty-state.yml4
-rw-r--r--changelogs/unreleased/fix-group-path-rename-error.yml4
-rw-r--r--changelogs/unreleased/mattermost-slash-auto-config.yml4
-rw-r--r--changelogs/unreleased/zj-remove-unused-services.yml4
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/routes/project.rb2
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb82
-rw-r--r--db/post_migrate/20161221140236_remove_unneeded_services.rb13
-rw-r--r--db/schema.rb4
-rw-r--r--lib/banzai/filter/external_link_filter.rb2
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb4
-rw-r--r--lib/gitlab/middleware/multipart.rb8
-rw-r--r--lib/gitlab/update_path_error.rb3
-rw-r--r--lib/mattermost/client.rb41
-rw-r--r--lib/mattermost/command.rb10
-rw-r--r--lib/mattermost/error.rb3
-rw-r--r--lib/mattermost/session.rb63
-rw-r--r--lib/mattermost/team.rb7
-rw-r--r--spec/controllers/groups_controller_spec.rb21
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb58
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb27
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb34
-rw-r--r--spec/javascripts/fixtures/issuable_filter.html.haml8
-rw-r--r--spec/javascripts/issuable_spec.js.es681
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb14
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb6
-rw-r--r--spec/lib/mattermost/client_spec.rb24
-rw-r--r--spec/lib/mattermost/command_spec.rb61
-rw-r--r--spec/lib/mattermost/session_spec.rb24
-rw-r--r--spec/lib/mattermost/team_spec.rb66
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb119
-rw-r--r--spec/services/groups/update_service_spec.rb51
75 files changed, 1458 insertions, 257 deletions
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 824febe3fd3..5e449170cd3 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -4,6 +4,7 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var AUTO_SCROLL_OFFSET = 75;
this.Build = (function() {
Build.interval = null;
@@ -19,6 +20,17 @@
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
+ this.$body = $('body');
+ this.$buildTrace = $('#build-trace');
+ this.$autoScrollContainer = $('.autoscroll-container');
+ this.$autoScrollStatus = $('#autoscroll-status');
+ this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
+ this.$upBuildTrace = $('#up-build-trace');
+ this.$downBuildTrace = $('#down-build-trace');
+ this.$scrollTopBtn = $('#scroll-top');
+ this.$scrollBottomBtn = $('#scroll-bottom');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
@@ -32,6 +44,7 @@
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
@@ -40,18 +53,6 @@
this.initScrollButtonAffix();
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
- // Bind autoscroll button to follow build output
- $('#autoscroll-button').on('click', function() {
- var state;
- state = $(this).data("state");
- if ("enabled" === state) {
- $(this).data("state", "disabled");
- return $(this).text("Enable autoscroll");
- } else {
- $(this).data("state", "enabled");
- return $(this).text("Disable autoscroll");
- }
- });
Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
@@ -91,9 +92,10 @@
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
- return $('.js-build-refresh').remove();
+ this.initScrollMonitor();
+ return this.$buildRefreshAnimation.remove();
}
- }
+ }.bind(this)
});
};
@@ -122,22 +124,95 @@
};
Build.prototype.checkAutoscroll = function() {
- if ("enabled" === $("#autoscroll-button").data("state")) {
- return $("html,body").scrollTop($("#build-trace").height());
+ if (this.$autoScrollStatus.data("state") === "enabled") {
+ return $("html,body").scrollTop(this.$buildTrace.height());
+ }
+
+ // Handle a situation where user started new build
+ // but never scrolled a page
+ if (!this.$scrollTopBtn.is(':visible') &&
+ !this.$scrollBottomBtn.is(':visible') &&
+ !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ this.$scrollBottomBtn.show();
}
};
Build.prototype.initScrollButtonAffix = function() {
- var $body, $buildTrace;
- $body = $('body');
- $buildTrace = $('#build-trace');
- return this.$buildScroll.affix({
- offset: {
- bottom: function() {
- return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
- }
+ // Hide everything initially
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+ this.$autoScrollContainer.hide();
+ }
+
+ // Page scroll listener to detect if user has scrolling page
+ // and handle following cases
+ // 1) User is at Top of Build Log;
+ // - Hide Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ // 2) User is at Bottom of Build Log;
+ // - Show Top Arrow button
+ // - Hide Bottom Arrow button
+ // - Enable Autoscroll and show indicator (when build is running)
+ // 3) User is somewhere in middle of Build Log;
+ // - Show Top Arrow button
+ // - Show Bottom Arrow button
+ // - Disable Autoscroll and hide indicator (when build is running)
+ Build.prototype.initScrollMonitor = function() {
+ if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is somewhere in middle of Build Log
+
+ this.$scrollTopBtn.show();
+
+ if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
+ this.$scrollBottomBtn.show();
+ } else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
+ this.$scrollBottomBtn.show();
+ } else {
+ this.$scrollBottomBtn.hide();
}
- });
+
+ // Hide Autoscroll Status Indicator
+ if (this.$scrollBottomBtn.is(':visible')) {
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else {
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ }
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // User is at Top of Build Log
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.show();
+
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ } else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
+ (this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
+ // User is at Bottom of Build Log
+
+ this.$scrollTopBtn.show();
+ this.$scrollBottomBtn.hide();
+
+ // Show and Reposition Autoscroll Status Indicator
+ this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
+ this.$autoScrollStatusText.addClass('animate');
+ } else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
+ // Build Log height is small
+
+ this.$scrollTopBtn.hide();
+ this.$scrollBottomBtn.hide();
+
+ // Hide Autoscroll Status Indicator
+ this.$autoScrollContainer.hide();
+ this.$autoScrollStatusText.removeClass('animate');
+ }
+
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
+ this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
+ }
};
Build.prototype.shouldHideSidebarForViewport = function() {
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 17d03c87bf5..cbd8ac4eddd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -112,7 +112,6 @@
return value.path != null ? this.Emoji.template : this.Loading.template;
}.bind(this),
insertTpl: ':${name}:',
- startWithSpace: false,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
callbacks: {
@@ -129,7 +128,6 @@
}.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
- startWithSpace: false,
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
@@ -172,7 +170,6 @@
}.bind(this),
data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
- startWithSpace: false,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
@@ -200,7 +197,6 @@
displayTpl: function(value) {
return value.title != null ? this.Milestones.template : this.Loading.template;
}.bind(this),
- startWithSpace: false,
data: this.defaultLoadingData,
callbacks: {
matcher: this.DefaultOptions.matcher,
@@ -225,7 +221,6 @@
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- startWithSpace: false,
displayTpl: function(value) {
return value.title != null ? this.Issues.template : this.Loading.template;
}.bind(this),
@@ -259,7 +254,6 @@
return this.isLoading(value) ? this.Loading.template : this.Labels.template;
}.bind(this),
insertTpl: '${atwho-at}${title}',
- startWithSpace: false,
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
@@ -379,14 +373,7 @@
togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
this.setting.tabSelectsMatch = !isPrevented;
this.setting.spaceSelectsMatch = !isPrevented;
- const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
- this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
},
- preventSpaceTabEnter(e) {
- const key = e.which || e.keyCode;
- const preventables = [9, 13, 32];
- if (preventables.indexOf(key) > -1) e.preventDefault();
- }
};
}).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 1c10a7445bb..9c3c96c20ed 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,13 +1,13 @@
-/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
+/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
/* global Issuable */
/* global Turbolinks */
-(function() {
+((global) => {
var issuable_created;
issuable_created = false;
- this.Issuable = {
+ global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
@@ -111,7 +111,11 @@
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
- formData = form.serialize();
+ formData = form.serializeArray();
+ formData = formData.filter(function(data) {
+ return data.value !== '';
+ });
+ formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
@@ -184,4 +188,4 @@
}
};
-}).call(this);
+})(window);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8fa80502d92..0a0e73e0ccc 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -93,6 +93,19 @@
}
};
+ // Check if element scrolled into viewport from above or below
+ // Courtesy http://stackoverflow.com/a/7557433/414749
+ w.gl.utils.isInViewport = function(el) {
+ var rect = el.getBoundingClientRect();
+
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= window.innerHeight &&
+ rect.right <= window.innerWidth
+ );
+ };
+
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 66f7e7f97c8..f9e8d297c05 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,3 +1,34 @@
+@keyframes fade-out-status {
+ 0%, 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+@keyframes blinking-dots {
+ 0% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 25% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+
+ 75% {
+ background-color: rgba($white-light, 0.4);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,1);
+ }
+
+ 100% {
+ background-color: rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light,0.2),
+ 24px 0 0 0 rgba($white-light,0.2);
+ }
+}
+
.build-page {
pre.trace {
background: $builds-trace-bg;
@@ -14,47 +45,101 @@
}
}
- .scroll-controls {
- .scroll-step {
- width: 31px;
- margin: 0 0 0 auto;
+ .environment-information {
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
}
+ }
+}
+
+.scroll-controls {
+ height: 100%;
+
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
+ }
+
+ .scroll-link,
+ .autoscroll-container {
+ right: 25px;
+ z-index: 1;
+ }
+
+ .scroll-link {
+ position: fixed;
+ display: block;
+ margin-bottom: 10px;
- &.affix-bottom {
- position: absolute;
- right: 25px;
+ &.scroll-top .gitlab-icon-scroll-up-hover,
+ &.scroll-top:hover .gitlab-icon-scroll-up,
+ &.scroll-bottom .gitlab-icon-scroll-down-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down {
+ display: none;
}
- &.affix {
- right: 25px;
- bottom: 15px;
- z-index: 1;
+ &.scroll-top:hover .gitlab-icon-scroll-up-hover,
+ &.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
+ display: inline-block;
}
- &.sidebar-expanded {
- right: #{$gutter_width + ($gl-padding * 2)};
+ &.scroll-top {
+ top: 110px;
}
- a {
- display: block;
- margin-bottom: 10px;
+ &.scroll-bottom {
+ bottom: -2px;
}
}
- .environment-information {
- background-color: $gray-light;
- border: 1px solid $border-color;
- padding: 12px $gl-padding;
- border-radius: $border-radius-default;
+ .autoscroll-container {
+ position: absolute;
+ }
- svg {
- position: relative;
- top: 1px;
- margin-right: 5px;
+ &.sidebar-expanded {
+
+ .scroll-link,
+ .autoscroll-container {
+ right: ($gutter_width + ($gl-padding * 2));
}
}
}
+.status-message {
+ display: inline-block;
+ color: $white-light;
+
+ .status-icon {
+ display: inline-block;
+ width: 16px;
+ height: 33px;
+ }
+
+ .status-text {
+ float: left;
+ opacity: 0;
+ margin-right: 10px;
+ font-weight: normal;
+ line-height: 1.8;
+ transition: opacity 1s ease-out;
+
+ &.animate {
+ animation: fade-out-status 2s ease;
+ }
+ }
+
+ &:hover .status-text {
+ opacity: 1;
+ }
+}
+
.build-header {
position: relative;
padding: 0;
@@ -109,6 +194,15 @@
.bash {
display: block;
}
+
+ .build-loader-animation {
+ position: relative;
+ width: 6px;
+ height: 6px;
+ margin: auto auto 12px 2px;
+ border-radius: 50%;
+ animation: blinking-dots 1s linear infinite;
+ }
}
.right-sidebar.build-sidebar {
@@ -248,6 +342,12 @@
}
}
+.build-sidebar {
+ .container-fluid.container-limited {
+ max-width: 100%;
+ }
+}
+
.build-detail-row {
margin-bottom: 5px;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index e779e65eca3..394980704ae 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -21,6 +21,14 @@
display: inline-block;
float: left;
+ .btn-success.dropdown-toggle .fa {
+ color: inherit;
+ }
+
+ .btn-success.dropdown-toggle:disabled {
+ background-color: $gl-success;
+ }
+
.accept_merge_request {
&.ci-pending,
&.ci-running {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 106c5d4d390..6ac4ec6ea0d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -43,7 +43,7 @@ ul.notes {
}
.system-note-message {
- display: inline-block;
+ display: inline;
&::first-letter {
text-transform: lowercase;
@@ -55,7 +55,7 @@ ul.notes {
}
p {
- display: inline-block;
+ display: inline;
margin: 0;
&::first-letter {
@@ -151,6 +151,10 @@ ul.notes {
}
}
}
+
+ .note-headline-light {
+ display: inline;
+ }
}
.discussion-body {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 566de8a4eba..f6164c8907e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -80,6 +80,10 @@
td {
padding: 10px 8px;
}
+
+ .commit-link {
+ padding: 9px 8px 10px;
+ }
}
tbody {
@@ -193,7 +197,7 @@
width: 8px;
position: absolute;
right: -7px;
- bottom: 9px;
+ bottom: 10px;
border-bottom: 2px solid $border-color;
}
}
@@ -499,15 +503,10 @@
> .ci-action-icon-container {
position: absolute;
- right: 4px;
+ right: 5px;
top: 5px;
}
- .ci-status-icon {
- position: relative;
- top: 1px;
- }
-
.ci-status-icon svg {
height: 20px;
width: 20px;
@@ -614,6 +613,10 @@
a {
display: inline-block;
+ }
+
+ .build-content {
+ width: 138px;
&:hover {
background-color: $stage-hover-bg;
@@ -623,15 +626,24 @@
ul {
max-height: 245px;
overflow: auto;
- margin: 5px 0;
+ margin: 3px 0;
li {
padding-top: 2px;
- margin: 0 5px;
+ margin: 4px 7px;
+ padding: 0 3px;
padding-left: 0;
padding-bottom: 0;
- margin-bottom: 0;
- line-height: 1.2;
+ line-height: 0;
+
+ .ci-action-icon-container:hover {
+ background-color: transparent;
+ }
+
+ .ci-status-icon {
+ position: relative;
+ top: 2px;
+ }
}
}
}
@@ -680,11 +692,15 @@
.dropdown-build {
color: $gl-text-color-light;
+ .build-content {
+ padding: 3px 7px 6px;
+ }
+
.ci-action-icon-container {
padding: 0;
font-size: 11px;
float: right;
- margin-top: 4px;
+ margin-top: 3px;
display: inline-block;
position: relative;
@@ -694,16 +710,10 @@
}
}
- &:hover {
- background-color: $stage-hover-bg;
- border-radius: 3px;
- color: $gl-text-color;
- }
-
.ci-action-icon-container {
i {
- width: 25px;
- height: 25px;
+ width: 24px;
+ height: 24px;
&::before {
top: 1px;
@@ -740,6 +750,10 @@
margin: 0;
}
+ .dropdown-build .build-content {
+ padding: 3px 7px 7px;
+ }
+
.builds-dropdown-loading {
margin: 10px auto;
width: 18px;
@@ -788,19 +802,25 @@
.mini-pipeline-graph-icon-container .ci-status-icon {
display: inline-block;
border: 1px solid;
- border-radius: 20px;
+ border-radius: 22px;
margin-right: 1px;
- width: 20px;
- height: 20px;
+ width: 22px;
+ height: 22px;
position: relative;
z-index: 2;
transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
svg {
top: -1px;
+ left: -1px;
}
}
+.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg {
+ width: 22px;
+ height: 22px;
+}
+
.builds-dropdown {
&:focus {
outline: none;
@@ -851,7 +871,7 @@
.mini-pipeline-graph-icon-container {
.ci-status-icon:hover,
.ci-status-icon:focus {
- width: 28px;
+ width: 32px;
padding: 0 8px 0 0;
+ .dropdown-caret {
@@ -863,7 +883,7 @@
font-size: 11px;
position: relative;
top: 3px;
- left: -11px;
+ left: -14px;
margin-right: -6px;
display: none;
z-index: 2;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index e16a553bcda..d6aa4c4c032 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -880,3 +880,23 @@ pre.light-well {
width: 30%;
}
}
+
+.services-installation-info .row {
+ margin-bottom: 10px;
+}
+
+.service-installation {
+ padding: 32px;
+ margin: 32px;
+ border-radius: 3px;
+ background-color: $white-light;
+
+ h3 {
+ margin-top: 0;
+ }
+
+ hr {
+ margin: 32px 0;
+ border-color: $border-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index a810ed32327..4acd17360c1 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,6 +1,6 @@
.container-fluid {
.ci-status {
- padding: 2px 7px;
+ padding: 2px 7px 4px;
margin-right: 10px;
border: 1px solid $gray-darker;
white-space: nowrap;
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index b83c3a872cf..efe9c001bcf 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
+ @group.reset_path!
+
render action: "edit"
end
end
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
new file mode 100644
index 00000000000..d87dff2a80e
--- /dev/null
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -0,0 +1,43 @@
+class Projects::MattermostsController < Projects::ApplicationController
+ include TriggersHelper
+ include ActionView::Helpers::AssetUrlHelper
+
+ layout 'project_settings'
+
+ before_action :authorize_admin_project!
+ before_action :service
+ before_action :teams, only: [:new]
+
+ def new
+ end
+
+ def create
+ result, message = @service.configure(current_user, configure_params)
+
+ if result
+ flash[:notice] = 'This service is now configured'
+ redirect_to edit_namespace_project_service_path(
+ @project.namespace, @project, service)
+ else
+ flash[:alert] = message || 'Failed to configure service'
+ redirect_to new_namespace_project_mattermost_path(
+ @project.namespace, @project)
+ end
+ end
+
+ private
+
+ def configure_params
+ params.require(:mattermost).permit(:trigger, :team_id).merge(
+ url: service_trigger_url(@service),
+ icon_url: asset_url('gitlab_logo.png'))
+ end
+
+ def teams
+ @teams ||= @service.list_teams(current_user)
+ end
+
+ def service
+ @service ||= @project.find_or_initialize_service('mattermost_slash_commands')
+ end
+end
diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb
new file mode 100644
index 00000000000..49ac12db832
--- /dev/null
+++ b/app/helpers/mattermost_helper.rb
@@ -0,0 +1,9 @@
+module MattermostHelper
+ def mattermost_teams_options(teams)
+ teams_options = teams.map do |id, options|
+ [options['display_name'] || options['name'], id]
+ end
+
+ teams_options.compact.unshift(['Select team...', '0'])
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index fd42f2328d8..b52f08c7081 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base
def move_dir
if any_project_has_container_registry_tags?
- raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
+ raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
# Move the namespace directory in all storages paths used by member projects
@@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Exception.new('namespace directory cannot be moved')
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
end
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 10740275669..6c78c0af71c 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -18,4 +18,34 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
def to_param
'mattermost_slash_commands'
end
+
+ def configure(user, params)
+ token = Mattermost::Command.new(user).
+ create(command(params))
+
+ update(active: true, token: token) if token
+ rescue Mattermost::Error => e
+ [false, e.message]
+ end
+
+ def list_teams(user)
+ Mattermost::Team.new(user).all
+ rescue Mattermost::Error => e
+ [[], e.message]
+ end
+
+ private
+
+ def command(params)
+ pretty_project_name = project.name_with_namespace
+
+ params.merge(
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{pretty_project_name}",
+ display_name: "GitLab / #{pretty_project_name}",
+ method: 'P',
+ user_name: 'GitLab')
+ end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index fff2273f402..4e878ec556a 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -14,7 +14,13 @@ module Groups
group.assign_attributes(params)
- group.save
+ begin
+ group.save
+ rescue Gitlab::UpdatePathError => e
+ group.errors.add(:base, e.message)
+
+ false
+ end
end
end
end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
index 927c67b65b0..79b2c99fd70 100644
--- a/app/validators/project_path_validator.rb
+++ b/app/validators/project_path_validator.rb
@@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
- RESERVED = (NamespaceValidator::RESERVED +
+ RESERVED = (NamespaceValidator::RESERVED -
+ %w[dashboard help ci admin search] +
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index cdeb81372ee..c69c53b656f 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -56,17 +56,22 @@
- else
#js-build-scroll.scroll-controls
.scroll-step
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ %a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top' }
+ = custom_icon('scroll_up')
+ = custom_icon('scroll_up_hover_active')
+ %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom' }
+ = custom_icon('scroll_down')
+ = custom_icon('scroll_down_hover_active')
- if @build.active?
.autoscroll-container
- %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
- Enable autoscroll
+ %span.status-message#autoscroll-status{ data: { state: 'disabled' } }
+ %span.status-text Autoscroll active
+ %i.status-icon
+ = custom_icon('scroll_down_hover_active')
+ #up-build-trace
%pre.build-trace#build-trace
%code.bash.js-build-output
- = icon("refresh spin", class: "js-build-refresh")
+ .build-loader-animation.js-build-refresh
#down-build-trace
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
new file mode 100644
index 00000000000..605c7f61dee
--- /dev/null
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -0,0 +1,12 @@
+%p
+ You aren’t a member of any team on the Mattermost instance at
+ %strong= Gitlab.config.mattermost.host
+%p
+ To install this service,
+ = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
+ join a team
+ = icon('external-link')
+ and try again.
+%hr
+.clearfix
+ = link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
new file mode 100644
index 00000000000..7980f7c9a72
--- /dev/null
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -0,0 +1,44 @@
+%p
+ This service will be installed on the Mattermost instance at
+ %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
+%hr
+= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
+ %h4 Team
+ %p
+ = @teams.one? ? 'The team' : 'Select the team'
+ where the slash commands will be used in
+ - selected_id = @teams.keys.first if @teams.one?
+ = f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? })
+ .help-block
+ - if @teams.one?
+ This is the only team where you are an administrator.
+ - else
+ The list shows teams where you are administrator
+ To create a team, ask your Mattermost system administrator.
+ To create a team,
+ = link_to "#{Gitlab.config.mattermost.host}/create_team" do
+ use Mattermost's interface
+ = icon('external-link')
+ %hr
+ %h4 Command trigger word
+ %p Choose the word that will trigger commands
+ = f.text_field(:trigger, value: @project.path, class: 'form-control')
+ .help-block
+ %p
+ Trigger word must be unique, and can't begin with a slash or contain any spaces.
+ Use the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+ %p
+ Reserved:
+ = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
+ see list of built-in slash commands
+ = icon('external-link')
+ %hr
+ .clearfix
+ .pull-right
+ = link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg'
+ = f.submit 'Install', class: 'btn btn-save btn-lg'
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
new file mode 100644
index 00000000000..96b1d2aee61
--- /dev/null
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -0,0 +1,8 @@
+.service-installation
+ .inline.pull-right
+ = custom_icon('mattermost_logo', size: 48)
+ %h3 Install Mattermost Command
+ - if @teams.empty?
+ = render 'no_teams'
+ - else
+ = render 'team_selection'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 9ab7971b56c..5bc417d1760 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -17,7 +17,7 @@
- # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
+ .ci_widget{class: "ci-#{status} ci-status-icon-#{status}", style: "display:none"}
= ci_icon_for_status(status)
%span
CI build
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index db51c4f8a4e..fc338dcf887 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -8,7 +8,6 @@
.col-lg-9
= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
= render 'shared/service_settings', form: form, subject: @service
-
.footer-block.row-content-block
= form.submit 'Save changes', class: 'btn btn-save'
&nbsp;
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
new file mode 100644
index 00000000000..8ca4c51a064
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -0,0 +1,91 @@
+- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+
+To setup this service:
+%ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ 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:
+
+%hr
+
+.help-form
+ .form-group
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#display_name')
+
+ .form-group
+ = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#description')
+
+ .form-group
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
+
+ .form-group
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#request_url')
+
+ .form-group
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block POST
+
+ .form-group
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_username')
+
+ .form-group
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#response_icon')
+
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.text-block Yes
+
+ .form-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_hint')
+
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#autocomplete_description')
+
+%hr
+
+%ul.list-unstyled
+ %li
+ 3. After adding the slash command, paste the
+
+ %strong token
+ into the field below
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 01a77a952d1..63b797cd391 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,4 +1,4 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- enabled = Gitlab.config.mattermost.enabled
.well
This service allows GitLab users to perform common operations on this
@@ -7,93 +7,9 @@
See list of available commands in Mattermost after setting up this service,
by entering
%code /&lt;command_trigger_word&gt; help
- %br
- %br
- To setup this service:
- %ul.list-unstyled
- %li
- 1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
- 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:
-
- %hr
-
- .help-form
- .form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#display_name')
-
- .form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#description')
-
- .form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.path_with_namespace
-
- .form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#request_url')
-
- .form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
-
- .form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#response_username')
-
- .form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#response_icon')
-
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Yes
-
- .form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_hint')
-
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(clipboard_target: '#autocomplete_description')
- %hr
+ - unless enabled
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- %ul.list-unstyled
- %li
- 3. After adding the slash command, paste the
- %strong token
- into the field below
+- if enabled
+ = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
new file mode 100644
index 00000000000..c929eee3bb9
--- /dev/null
+++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
@@ -0,0 +1,7 @@
+.services-installation-info
+ - unless @service.activated?
+ .row
+ .col-sm-9.col-sm-offset-3
+ = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
+ = custom_icon('mattermost_logo', size: 15)
+ = 'Add to Mattermost'
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 07d4927b6c9..e2033654018 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -10,10 +10,10 @@
.text-content
- if has_button && current_user
%h4
- The Issue Tracker is a good place to add things that need to be improved or solved in a project!
+ The Issue Tracker is the place to add things that need to be improved or solved in a project
%p
- An issue can be a bug, a todo or a feature request that needs to be discussed in a project.
- Besides, issues are searchable and filterable.
+ Issues can be bugs, tasks or ideas to be discussed.
+ Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- else
diff --git a/app/views/shared/icons/_mattermost_logo.svg.erb b/app/views/shared/icons/_mattermost_logo.svg.erb
new file mode 100644
index 00000000000..83fbd1a407d
--- /dev/null
+++ b/app/views/shared/icons/_mattermost_logo.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg>
diff --git a/app/views/shared/icons/_scroll_down.svg b/app/views/shared/icons/_scroll_down.svg
new file mode 100644
index 00000000000..acf22ac9314
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_down_hover_active.svg b/app/views/shared/icons/_scroll_down_hover_active.svg
new file mode 100644
index 00000000000..262576acf54
--- /dev/null
+++ b/app/views/shared/icons/_scroll_down_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up.svg b/app/views/shared/icons/_scroll_up.svg
new file mode 100644
index 00000000000..f11288fd59c
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/icons/_scroll_up_hover_active.svg b/app/views/shared/icons/_scroll_up_hover_active.svg
new file mode 100644
index 00000000000..4658dbb1bb7
--- /dev/null
+++ b/app/views/shared/icons/_scroll_up_hover_active.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
+ <path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
+</svg>
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 40fe53e6a8d..415361f8fbf 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -3,7 +3,7 @@
- show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
-- if selected.present?
+- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
diff --git a/changelogs/unreleased/19620-auto-scroll-log.yml b/changelogs/unreleased/19620-auto-scroll-log.yml
new file mode 100644
index 00000000000..cf38096683b
--- /dev/null
+++ b/changelogs/unreleased/19620-auto-scroll-log.yml
@@ -0,0 +1,4 @@
+---
+title: Improve Build Log scrolling experience
+merge_request: 7895
+author:
diff --git a/changelogs/unreleased/22742-filter-protocol-relative-urls.yml b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml
new file mode 100644
index 00000000000..b331f5a4eb5
--- /dev/null
+++ b/changelogs/unreleased/22742-filter-protocol-relative-urls.yml
@@ -0,0 +1,4 @@
+---
+title: 'Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742'
+merge_request: 6635
+author: Makoto Scott-Hinkle
diff --git a/changelogs/unreleased/25368-fix-left-align-system-note.yml b/changelogs/unreleased/25368-fix-left-align-system-note.yml
new file mode 100644
index 00000000000..81fd0888773
--- /dev/null
+++ b/changelogs/unreleased/25368-fix-left-align-system-note.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes left align issue for long system notes
+merge_request: 7982
+author:
diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml
new file mode 100644
index 00000000000..dd0f93e176f
--- /dev/null
+++ b/changelogs/unreleased/25898-ci-icon-color-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Adds CSS class to status icon on MR widget to prevent non-colored icon
+merge_request: 8219
+author:
diff --git a/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml
new file mode 100644
index 00000000000..39ce0b66768
--- /dev/null
+++ b/changelogs/unreleased/25905-mr-when-succeeds-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Adds background color for disabled state to merge when succeeds dropdown
+merge_request: 8222
+author:
diff --git a/changelogs/unreleased/25908-fix-grape-after-update.yml b/changelogs/unreleased/25908-fix-grape-after-update.yml
new file mode 100644
index 00000000000..026d5592441
--- /dev/null
+++ b/changelogs/unreleased/25908-fix-grape-after-update.yml
@@ -0,0 +1,4 @@
+---
+title: Use Grape's new Route methods
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-rename-invalid-groups.yml b/changelogs/unreleased/dz-rename-invalid-groups.yml
new file mode 100644
index 00000000000..90af42da01c
--- /dev/null
+++ b/changelogs/unreleased/dz-rename-invalid-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Rename groups with .git in the end of the path
+merge_request: 8199
+author:
diff --git a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml
new file mode 100644
index 00000000000..2787a5c57df
--- /dev/null
+++ b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml
@@ -0,0 +1,4 @@
+---
+title: Allow projects with 'dashboard' as path
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-whitelist-more-project-names.yml b/changelogs/unreleased/dz-whitelist-more-project-names.yml
new file mode 100644
index 00000000000..4a3f1511a0b
--- /dev/null
+++ b/changelogs/unreleased/dz-whitelist-more-project-names.yml
@@ -0,0 +1,4 @@
+---
+title: 'Whitelist next project names: help, ci, admin, search'
+merge_request: 8227
+author:
diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml
new file mode 100644
index 00000000000..a87b7612217
--- /dev/null
+++ b/changelogs/unreleased/fix-copy-issues-empty-state.yml
@@ -0,0 +1,4 @@
+---
+title: Improve copy in Issue Tracker empty state
+merge_request: 8202
+author:
diff --git a/changelogs/unreleased/fix-group-path-rename-error.yml b/changelogs/unreleased/fix-group-path-rename-error.yml
new file mode 100644
index 00000000000..e3d97ae3987
--- /dev/null
+++ b/changelogs/unreleased/fix-group-path-rename-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 500 error renaming group
+merge_request:
+author:
diff --git a/changelogs/unreleased/mattermost-slash-auto-config.yml b/changelogs/unreleased/mattermost-slash-auto-config.yml
new file mode 100644
index 00000000000..43014d38769
--- /dev/null
+++ b/changelogs/unreleased/mattermost-slash-auto-config.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to auto-configure Mattermost
+merge_request: 8070
+author:
diff --git a/changelogs/unreleased/zj-remove-unused-services.yml b/changelogs/unreleased/zj-remove-unused-services.yml
new file mode 100644
index 00000000000..8ede95f5faa
--- /dev/null
+++ b/changelogs/unreleased/zj-remove-unused-services.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unused and void services from the database
+merge_request:
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 2d1d48bf9da..42e5f105d46 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -586,4 +586,4 @@ test:
admin_group: ''
staging:
- <<: *base
+ <<: *base \ No newline at end of file
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 335fccb617b..baabd22b840 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -76,6 +76,8 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resource :mattermost, only: [:new, :create]
+
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
new file mode 100644
index 00000000000..bd0e4b2cc07
--- /dev/null
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -0,0 +1,82 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDotGitFromGroupNames < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::ShellAdapter
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ invalid_groups.each do |group|
+ path_was = group['path']
+ path_was_wildcard = quote_string("#{path_was}/%")
+ path = quote_string(rename_path(path_was))
+
+ move_namespace(group['id'], path_was, path)
+
+ execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}"
+ execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}"
+
+ select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
+ new_path = "#{path}/#{route['path'].split('/').last}"
+ execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
+ end
+ end
+ end
+
+ def down
+ # nothing to do here
+ end
+
+ private
+
+ def invalid_groups
+ select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'")
+ end
+
+ def route_exists?(path)
+ select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
+ end
+
+ # Accepts invalid path like test.git and returns test_git or
+ # test_git1 if test_git already taken
+ def rename_path(path)
+ # To stay closer with original name and reduce risk of duplicates
+ # we rename suffix instead of removing it
+ path = path.sub(/\.git\z/, '_git')
+
+ counter = 0
+ base = path
+
+ while route_exists?(path)
+ counter += 1
+ path = "#{base}#{counter}"
+ end
+
+ path
+ end
+
+ def move_namespace(group_id, path_was, path)
+ repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
+ Gitlab.config.repositories.storages[row['repository_storage']]
+ end
+
+ # Move the namespace directory in all storages paths used by member projects
+ repository_storage_paths.each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, path_was)
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Exception.new('namespace directory cannot be moved')
+ end
+ end
+
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ end
+end
diff --git a/db/post_migrate/20161221140236_remove_unneeded_services.rb b/db/post_migrate/20161221140236_remove_unneeded_services.rb
new file mode 100644
index 00000000000..a94ccc43a41
--- /dev/null
+++ b/db/post_migrate/20161221140236_remove_unneeded_services.rb
@@ -0,0 +1,13 @@
+class RemoveUnneededServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute("DELETE FROM services WHERE active = false AND properties = '{}';")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 14801b581e6..05b6c807660 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: 20161213172958) do
+ActiveRecord::Schema.define(version: 20161221140236) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -854,7 +854,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.datetime "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.string "scopes", default: "--- []\n", null: false
+ t.string "scopes", default: "--- []\n", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 2f19b59e725..d67d466bce8 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -10,7 +10,7 @@ module Banzai
node.set_attribute('href', href)
end
- if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+ if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 01c96a6fe96..91fb0bb317a 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -70,8 +70,8 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
- path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
- trans.action = "Grape##{endpoint.route.route_method} #{path}"
+ path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
+ trans.action = "Grape##{endpoint.route.request_method} #{path}"
end
private
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 65713e73a59..dd99f9bb7d7 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first
if value.nil?
- value = File.open(tmp_path)
+ value = open_file(tmp_path)
@open_files << value
else
value = decorate_params_value(value, @request.params[key], tmp_path)
@@ -68,7 +68,7 @@ module Gitlab
case path_value
when nil
- value_hash[path_key] = File.open(tmp_path)
+ value_hash[path_key] = open_file(tmp_path)
@open_files << value_hash[path_key]
value_hash
when Hash
@@ -78,6 +78,10 @@ module Gitlab
raise "unexpected path value: #{path_value.inspect}"
end
end
+
+ def open_file(path)
+ ::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
+ end
end
def initialize(app)
diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb
new file mode 100644
index 00000000000..ce14cc887d0
--- /dev/null
+++ b/lib/gitlab/update_path_error.rb
@@ -0,0 +1,3 @@
+module Gitlab
+ class UpdatePathError < StandardError; end
+end
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
new file mode 100644
index 00000000000..ec2903b7ec6
--- /dev/null
+++ b/lib/mattermost/client.rb
@@ -0,0 +1,41 @@
+module Mattermost
+ class ClientError < Mattermost::Error; end
+
+ class Client
+ attr_reader :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ private
+
+ def with_session(&blk)
+ Mattermost::Session.new(user).with_session(&blk)
+ end
+
+ def json_get(path, options = {})
+ with_session do |session|
+ json_response session.get(path, options)
+ end
+ end
+
+ def json_post(path, options = {})
+ with_session do |session|
+ json_response session.post(path, options)
+ end
+ end
+
+ def json_response(response)
+ json_response = JSON.parse(response.body)
+
+ unless response.success?
+ raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error')
+ end
+
+ json_response
+ rescue JSON::JSONError
+ raise Mattermost::ClientError.new('Cannot parse response')
+ end
+ end
+end
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
new file mode 100644
index 00000000000..d1e4bb0eccf
--- /dev/null
+++ b/lib/mattermost/command.rb
@@ -0,0 +1,10 @@
+module Mattermost
+ class Command < Client
+ def create(params)
+ response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ body: params.to_json)
+
+ response['token']
+ end
+ end
+end
diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb
new file mode 100644
index 00000000000..014df175be0
--- /dev/null
+++ b/lib/mattermost/error.rb
@@ -0,0 +1,3 @@
+module Mattermost
+ class Error < StandardError; end
+end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index fb8d7d97f8a..377cb7b1021 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -1,5 +1,12 @@
module Mattermost
- class NoSessionError < StandardError; end
+ class NoSessionError < Mattermost::Error
+ def message
+ 'No session could be set up, is Mattermost configured with Single Sign On?'
+ end
+ end
+
+ class ConnectionError < Mattermost::Error; end
+
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
#
@@ -17,6 +24,8 @@ module Mattermost
include Doorkeeper::Helpers::Controller
include HTTParty
+ LEASE_TIMEOUT = 60
+
base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token
@@ -26,12 +35,16 @@ module Mattermost
end
def with_session
- raise NoSessionError unless create
-
- begin
- yield self
- ensure
- destroy
+ with_lease do
+ raise Mattermost::NoSessionError unless create
+
+ begin
+ yield self
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::NoSessionError
+ ensure
+ destroy
+ end
end
end
@@ -58,11 +71,15 @@ module Mattermost
end
def get(path, options = {})
- self.class.get(path, options.merge(headers: @headers))
+ handle_exceptions do
+ self.class.get(path, options.merge(headers: @headers))
+ end
end
def post(path, options = {})
- self.class.post(path, options.merge(headers: @headers))
+ handle_exceptions do
+ self.class.post(path, options.merge(headers: @headers))
+ end
end
private
@@ -111,5 +128,33 @@ module Mattermost
response.headers['token']
end
end
+
+ def with_lease
+ lease_uuid = lease_try_obtain
+ raise NoSessionError unless lease_uuid
+
+ begin
+ yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
+ end
+ end
+
+ def lease_key
+ "mattermost:session"
+ end
+
+ def lease_try_obtain
+ lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+ lease.try_obtain
+ end
+
+ def handle_exceptions
+ yield
+ rescue HTTParty::Error => e
+ raise Mattermost::ConnectionError.new(e.message)
+ rescue Errno::ECONNREFUSED
+ raise Mattermost::ConnectionError.new(e.message)
+ end
end
end
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
new file mode 100644
index 00000000000..784eca6ab5a
--- /dev/null
+++ b/lib/mattermost/team.rb
@@ -0,0 +1,7 @@
+module Mattermost
+ class Team < Client
+ def all
+ json_get('/api/v3/teams/all')
+ end
+ end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a763e2c5ba8..98dfb3e5216 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -105,4 +105,25 @@ describe GroupsController do
end
end
end
+
+ describe 'PUT update' do
+ before do
+ sign_in(user)
+ end
+
+ it 'updates the path succesfully' do
+ post :update, id: group.to_param, group: { path: 'new_path' }
+
+ expect(response).to have_http_status(302)
+ expect(controller).to set_flash[:notice]
+ end
+
+ it 'does not update the path on error' do
+ allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
+ post :update, id: group.to_param, group: { path: 'new_path' }
+
+ expect(assigns(:group).errors).not_to be_empty
+ expect(assigns(:group).path).not_to eq('new_path')
+ end
+ end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
new file mode 100644
index 00000000000..2ae635a1244
--- /dev/null
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Projects::MattermostsController do
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET #new' do
+ before do
+ allow_any_instance_of(MattermostSlashCommandsService).
+ to receive(:list_teams).and_return([])
+
+ get(:new,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param)
+ end
+
+ it 'accepts the request' do
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ describe 'POST #create' do
+ let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } }
+
+ subject do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ mattermost: mattermost_params)
+ end
+
+ context 'no request can be made to mattermost' do
+ it 'shows the error' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"])
+
+ expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project))
+ end
+ end
+
+ context 'the request is succesull' do
+ before do
+ allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token')
+ end
+
+ it 'redirects to the new page' do
+ subject
+ service = project.services.last
+
+ expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service))
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index da64827b377..df3a467cbb7 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -39,7 +39,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}")
- sleep 1
note.click
end
@@ -53,7 +52,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
- sleep 1
note.click
end
@@ -67,7 +65,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys(":cartwheel")
- sleep 1
note.click
end
@@ -76,6 +73,22 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end
+ it 'doesn\'t open autocomplete after non-word character' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
+ it 'doesn\'t open autocomplete if there is no space before' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys("hello:#{user.username[0..2]}")
+ end
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
@@ -89,12 +102,4 @@ feature 'GFM autocomplete', feature: true, js: true do
end
end
end
-
- it 'doesnt open autocomplete after non-word character' do
- page.within '.timeline-content-form' do
- find('#note_note').native.send_keys("@#{user.username[0..2]}!")
- end
-
- expect(page).not_to have_selector('.atwho-view')
- end
end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index f474e7e891b..274d50e7ce4 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do
include WaitForAjax
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
+ let(:mattermost_enabled) { true }
before do
+ Settings.mattermost['enabled'] = mattermost_enabled
project.team << [user, :master]
login_as(user)
+ visit edit_namespace_project_service_path(project.namespace, project, service)
end
- describe 'user visites the mattermost slash command config page', js: true do
+ describe 'user visits the mattermost slash command config page', js: true do
it 'shows a help message' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
wait_for_ajax
expect(page).to have_content("This service allows GitLab users to perform common")
end
- end
-
- describe 'saving a token' do
- let(:token) { ('a'..'z').to_a.join }
it 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
click_on 'Save'
@@ -35,14 +32,21 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to eq(token)
end
- end
- describe 'the trigger url' do
- it 'shows the correct url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ describe 'mattermost service is enabled' do
+ it 'shows the add to mattermost button' do
+ expect(page).to have_link 'Add to Mattermost'
+ end
+ end
+
+ describe 'mattermost service is not enabled' do
+ let(:mattermost_enabled) { false }
+
+ it 'shows the correct trigger url' do
+ value = find_field('request_url').value
- value = find_field('request_url').value
- expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+ end
end
end
end
diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml
new file mode 100644
index 00000000000..ae745b292e6
--- /dev/null
+++ b/spec/javascripts/fixtures/issuable_filter.html.haml
@@ -0,0 +1,8 @@
+%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
+ %input{id: 'utf8', name: 'utf8', value: '✓'}
+ %input{id: 'check_all_issues', name: 'check_all_issues'}
+ %input{id: 'search', name: 'search'}
+ %input{id: 'author_id', name: 'author_id'}
+ %input{id: 'assignee_id', name: 'assignee_id'}
+ %input{id: 'milestone_title', name: 'milestone_title'}
+ %input{id: 'label_name', name: 'label_name'}
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6
new file mode 100644
index 00000000000..d61601ee4fb
--- /dev/null
+++ b/spec/javascripts/issuable_spec.js.es6
@@ -0,0 +1,81 @@
+/* global Issuable */
+/* global Turbolinks */
+
+//= require issuable
+//= require turbolinks
+
+(() => {
+ const BASE_URL = '/user/project/issues?scope=all&state=closed';
+ const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
+
+ function updateForm(formValues, form) {
+ $.each(formValues, (id, value) => {
+ $(`#${id}`, form).val(value);
+ });
+ }
+
+ function resetForm(form) {
+ $('input[name!="utf8"]', form).each((index, input) => {
+ input.setAttribute('value', '');
+ });
+ }
+
+ describe('Issuable', () => {
+ fixture.preload('issuable_filter');
+
+ beforeEach(() => {
+ fixture.load('issuable_filter');
+ Issuable.init();
+ });
+
+ it('should be defined', () => {
+ expect(window.Issuable).toBeDefined();
+ });
+
+ describe('filtering', () => {
+ let $filtersForm;
+
+ beforeEach(() => {
+ $filtersForm = $('.js-filter-form');
+ fixture.load('issuable_filter');
+ resetForm($filtersForm);
+ });
+
+ it('should contain only the default parameters', () => {
+ spyOn(Turbolinks, 'visit');
+
+ Issuable.filterResults($filtersForm);
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
+ });
+
+ it('should filter for the phrase "broken"', () => {
+ spyOn(Turbolinks, 'visit');
+
+ updateForm({ search: 'broken' }, $filtersForm);
+ Issuable.filterResults($filtersForm);
+ const params = `${DEFAULT_PARAMS}&search=broken`;
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ });
+
+ it('should keep query parameters after modifying filter', () => {
+ spyOn(Turbolinks, 'visit');
+
+ // initial filter
+ updateForm({ milestone_title: 'v1.0' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+
+ // update filter
+ updateForm({ label_name: 'Frontend' }, $filtersForm);
+
+ Issuable.filterResults($filtersForm);
+ params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
+ expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ });
+ });
+ });
+})();
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 167397c736b..d9e4525cb28 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq(exp)
end
end
+
+ context 'for protocol-relative links' do
+ let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index bcaffd27909..7371b578a48 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
it 'tags a transaction with the method and path of the route in the grape endpoint' do
- route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint
@@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) }
it 'tags a transaction with the method and path of the route in the grape endpount' do
- route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index ab1ab22795c..8d925460f01 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['file']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
@@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['user']['avatar']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
@@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['project']['milestone']['themesong']
- expect(file).to be_a(File)
+ expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path)
end
diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb
new file mode 100644
index 00000000000..dc11a414717
--- /dev/null
+++ b/spec/lib/mattermost/client_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Mattermost::Client do
+ let(:user) { build(:user) }
+
+ subject { described_class.new(user) }
+
+ context 'JSON parse error' do
+ before do
+ Struct.new("Request", :body, :success?)
+ end
+
+ it 'yields an error on malformed JSON' do
+ bad_json = Struct::Request.new("I'm not json", true)
+ expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
+ end
+
+ it 'shows a client error if the request was unsuccessful' do
+ bad_request = Struct::Request.new("true", false)
+
+ expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
+ end
+ end
+end
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
new file mode 100644
index 00000000000..5ccf1100898
--- /dev/null
+++ b/spec/lib/mattermost/command_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Mattermost::Command do
+ let(:params) { { 'token' => 'token', team_id: 'abc' } }
+
+ before do
+ Mattermost::Session.base_uri('http://mattermost.example.com')
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#create' do
+ let(:params) do
+ { team_id: 'abc',
+ trigger: 'gitlab'
+ }
+ end
+
+ subject { described_class.new(nil).create(params) }
+
+ context 'for valid trigger word' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ with(body: {
+ team_id: 'abc',
+ trigger: 'gitlab' }.to_json).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: { token: 'token' }.to_json
+ )
+ end
+
+ it 'returns a token' do
+ is_expected.to eq('token')
+ end
+ end
+
+ context 'for error message' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.command.duplicate_trigger.app_error',
+ message: 'This trigger word is already in use. Please choose another word.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'raises an error with message' do
+ expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
+ end
+ end
+ end
+end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 3c2eddbd221..74d12e37181 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do
end
end
end
+
+ context 'with lease' do
+ before do
+ allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
+ end
+
+ it 'tries to obtain a lease' do
+ expect(subject).to receive(:lease_try_obtain)
+ expect(Gitlab::ExclusiveLease).to receive(:cancel)
+
+ # Cannot setup a session, but we should still cancel the lease
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
+
+ context 'without lease' do
+ before do
+ allow(subject).to receive(:lease_try_obtain).and_return(nil)
+ end
+
+ it 'returns a NoSessionError error' do
+ expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ end
+ end
end
end
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
new file mode 100644
index 00000000000..2d14be6bcc2
--- /dev/null
+++ b/spec/lib/mattermost/team_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Mattermost::Team do
+ before do
+ Mattermost::Session.base_uri('http://mattermost.example.com')
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#all' do
+ subject { described_class.new(nil).all }
+
+ context 'for valid request' do
+ let(:response) do
+ [{
+ "id" => "xiyro8huptfhdndadpz8r3wnbo",
+ "create_at" => 1482174222155,
+ "update_at" => 1482174222155,
+ "delete_at" => 0,
+ "display_name" => "chatops",
+ "name" => "chatops",
+ "email" => "admin@example.com",
+ "type" => "O",
+ "company_name" => "",
+ "allowed_domains" => "",
+ "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
+ "allow_open_invite" => false }]
+ end
+
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: response.to_json
+ )
+ end
+
+ it 'returns a token' do
+ is_expected.to eq(response)
+ end
+ end
+
+ context 'for error message' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.team.list.app_error',
+ message: 'Cannot list teams.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'raises an error with message' do
+ expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 1ae1483e2a4..d6f4fbd7265 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -2,4 +2,123 @@ require 'spec_helper'
describe MattermostSlashCommandsService, :models do
it_behaves_like "chat slash commands service"
+
+ context 'Mattermost API' do
+ let(:project) { create(:empty_project) }
+ let(:service) { project.build_mattermost_slash_commands_service }
+ let(:user) { create(:user)}
+
+ before do
+ Mattermost::Session.base_uri("http://mattermost.example.com")
+
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session).
+ and_yield(Mattermost::Session.new(nil))
+ end
+
+ describe '#configure' do
+ subject do
+ service.configure(user, team_id: 'abc',
+ trigger: 'gitlab', url: 'http://trigger.url',
+ icon_url: 'http://icon.url/icon.png')
+ end
+
+ context 'the requests succeeds' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ with(body: {
+ team_id: 'abc',
+ trigger: 'gitlab',
+ url: 'http://trigger.url',
+ icon_url: 'http://icon.url/icon.png',
+ auto_complete: true,
+ auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}",
+ auto_complete_hint: '[help]',
+ description: "Perform common operations on: #{project.name_with_namespace}",
+ display_name: "GitLab / #{project.name_with_namespace}",
+ method: 'P',
+ user_name: 'GitLab' }.to_json).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: { token: 'token' }.to_json
+ )
+ end
+
+ it 'saves the service' do
+ expect { subject }.to change { project.services.count }.by(1)
+ end
+
+ it 'saves the token' do
+ subject
+
+ expect(service.reload.token).to eq('token')
+ end
+ end
+
+ context 'an error is received' do
+ before do
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ id: 'api.command.duplicate_trigger.app_error',
+ message: 'This trigger word is already in use. Please choose another word.',
+ detailed_error: '',
+ request_id: 'obc374man7bx5r3dbc1q5qhf3r',
+ status_code: 500
+ }.to_json
+ )
+ end
+
+ it 'shows error messages' do
+ succeeded, message = subject
+
+ expect(succeeded).to be(false)
+ expect(message).to eq('This trigger word is already in use. Please choose another word.')
+ end
+ end
+ end
+
+ describe '#list_teams' do
+ subject do
+ service.list_teams(user)
+ end
+
+ context 'the requests succeeds' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: ['list'].to_json
+ )
+ end
+
+ it 'returns a list of teams' do
+ expect(subject).not_to be_empty
+ end
+ end
+
+ context 'an error is received' do
+ before do
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
+ to_return(
+ status: 500,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ message: 'Failed to get team list.'
+ }.to_json
+ )
+ end
+
+ it 'shows error messages' do
+ teams, message = subject
+
+ expect(teams).to be_empty
+ expect(message).to eq('Failed to get team list.')
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 9c2331144a0..531180e48a1 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe Groups::UpdateService, services: true do
- let!(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
+ let!(:user) { create(:user) }
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
describe "#execute" do
context "project visibility_level validation" do
context "public group with public projects" do
- let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) }
+ let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
before do
public_group.add_user(user, Gitlab::Access::MASTER)
@@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do
end
context "internal group with internal project" do
- let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) }
+ let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
@@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do
end
context "unauthorized visibility_level validation" do
- let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) }
+ let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
end
@@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do
expect(internal_group.errors.count).to eq(1)
end
end
+
+ context 'rename group' do
+ let!(:service) { described_class.new(internal_group, user, path: 'new_path') }
+
+ before do
+ internal_group.add_user(user, Gitlab::Access::MASTER)
+ create(:project, :internal, group: internal_group)
+ end
+
+ it 'returns true' do
+ expect(service.execute).to eq(true)
+ end
+
+ context 'error moving group' do
+ before do
+ allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
+ end
+
+ it 'does not raise an error' do
+ expect { service.execute }.not_to raise_error
+ end
+
+ it 'returns false' do
+ expect(service.execute).to eq(false)
+ end
+
+ it 'has the right error' do
+ service.execute
+
+ expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError')
+ end
+
+ it "hasn't changed the path" do
+ expect { service.execute}.not_to change { internal_group.reload.path}
+ end
+ end
+ end
end