summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md20
-rw-r--r--app/assets/javascripts/application.js31
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es62
-rw-r--r--app/assets/javascripts/dispatcher.js.es614
-rw-r--r--app/assets/javascripts/extensions/element.js.es617
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6113
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js33
-rw-r--r--app/assets/javascripts/pipelines.js.es69
-rw-r--r--app/assets/javascripts/subscription.js52
-rw-r--r--app/assets/javascripts/subscription.js.es650
-rw-r--r--app/assets/javascripts/wikis.js38
-rw-r--r--app/assets/javascripts/wikis.js.es673
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/pagination.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss7
-rw-r--r--app/assets/stylesheets/framework/typography.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss1
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss8
-rw-r--r--app/assets/stylesheets/pages/environments.scss2
-rw-r--r--app/assets/stylesheets/pages/groups.scss6
-rw-r--r--app/assets/stylesheets/pages/milestone.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss125
-rw-r--r--app/controllers/help_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/pipelines_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb26
-rw-r--r--app/helpers/issuables_helper.rb10
-rw-r--r--app/helpers/nav_helper.rb5
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/merge_request.rb17
-rw-r--r--app/models/merge_request_diff.rb10
-rw-r--r--app/services/merge_requests/refresh_service.rb4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml1
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml12
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml17
-rw-r--r--app/views/projects/pipelines/show.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml16
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml23
-rw-r--r--app/views/projects/wikis/edit.html.haml32
-rw-r--r--app/views/projects/wikis/git_access.html.haml52
-rw-r--r--app/views/projects/wikis/history.html.haml17
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/projects/wikis/show.html.haml17
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml11
-rw-r--r--changelogs/unreleased/18546-update-wiki-page-design.yml4
-rw-r--r--changelogs/unreleased/22781-user-generated-permalinks.yml4
-rw-r--r--changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml4
-rw-r--r--changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml4
-rw-r--r--changelogs/unreleased/24726-remove-across-gitlab.yml4
-rw-r--r--changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml4
-rw-r--r--changelogs/unreleased/24814-pipeline-tabs.yml4
-rw-r--r--changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml4
-rw-r--r--changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml4
-rw-r--r--changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml4
-rw-r--r--changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml4
-rw-r--r--changelogs/unreleased/4269-public-api.yml4
-rw-r--r--changelogs/unreleased/boards-issue-sorting.yml4
-rw-r--r--changelogs/unreleased/clean-up-jira-service.yml4
-rw-r--r--changelogs/unreleased/comments-fixture.yml4
-rw-r--r--changelogs/unreleased/events-cache-invalidation.yml4
-rw-r--r--changelogs/unreleased/fix-ca-no-date.yml4
-rw-r--r--changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml4
-rw-r--r--changelogs/unreleased/fix-github-branch-formatter.yml4
-rw-r--r--changelogs/unreleased/fixed-commit-timeago.yml4
-rw-r--r--changelogs/unreleased/refresh-authorizations-with-lease.yml4
-rw-r--r--changelogs/unreleased/rephrase-system-notes.yml4
-rw-r--r--changelogs/unreleased/resolve-discussions-timeago.yml4
-rw-r--r--changelogs/unreleased/right-sidebar-fixture.yml4
-rw-r--r--changelogs/unreleased/sh-update-sidekiq-cron.yml4
-rw-r--r--changelogs/unreleased/shortcuts-issuable-fixture.yml4
-rw-r--r--changelogs/unreleased/timeout-merge-request-for-binary-file.yml4
-rw-r--r--changelogs/unreleased/use-st-commits-where-possible.yml5
-rw-r--r--changelogs/unreleased/workhorse-v1-0-1.yml4
-rw-r--r--config/initializers/ar_monkey_patch.rb17
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/routes/project.rb1
-rw-r--r--doc/administration/build_artifacts.md2
-rw-r--r--doc/ci/docker/using_docker_build.md32
-rw-r--r--doc/ci/triggers/README.md10
-rw-r--r--doc/development/gotchas.md48
-rw-r--r--doc/user/project/container_registry.md16
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md16
-rw-r--r--doc/web_hooks/ssl.pngbin23191 -> 27799 bytes
-rw-r--r--doc/web_hooks/web_hooks.md81
-rw-r--r--features/project/wiki.feature5
-rw-r--r--features/steps/project/source/markdown_render.rb6
-rw-r--r--features/steps/project/wiki.rb16
-rw-r--r--features/steps/shared/markdown.rb2
-rw-r--r--lib/api/helpers.rb5
-rw-r--r--lib/api/projects.rb28
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb8
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb2
-rw-r--r--spec/controllers/help_controller_spec.rb24
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/help_pages_spec.rb13
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb154
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb (renamed from spec/features/projects/pipelines_spec.rb)59
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb12
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb4
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js.es655
-rw-r--r--spec/javascripts/fixtures/comments.html.haml21
-rw-r--r--spec/javascripts/fixtures/issuable.html.haml2
-rw-r--r--spec/javascripts/fixtures/issue_note.html.haml12
-rw-r--r--spec/javascripts/fixtures/issues.rb7
-rw-r--r--spec/javascripts/fixtures/linked_tabs.html.haml13
-rw-r--r--spec/javascripts/fixtures/right_sidebar.html.haml17
-rw-r--r--spec/javascripts/notes_spec.js21
-rw-r--r--spec/javascripts/right_sidebar_spec.js14
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js6
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb21
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb12
-rw-r--r--spec/models/build_spec.rb4
-rw-r--r--spec/models/merge_request_diff_spec.rb34
-rw-r--r--spec/models/merge_request_spec.rb33
-rw-r--r--spec/requests/api/api_helpers_spec.rb54
-rw-r--r--spec/requests/api/projects_spec.rb376
-rw-r--r--spec/services/projects/destroy_service_spec.rb30
-rw-r--r--spec/support/matchers/have_issuable_counts.rb8
-rw-r--r--spec/support/matchers/markdown_matchers.rb6
136 files changed, 1551 insertions, 832 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12a3e63ed2e..86b30d2832d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.14.2 (2016-12-01)
+
+- Remove caching of events data. !6578
+- Rephrase some system notes to be compatible with new system note style. !7692
+- Pass tag SHA to post-receive hook when tag is created via UI. !7700
+- Prevent error when submitting a merge request and pipeline is not defined. !7707
+- Fixes system note style in commit discussion. !7721
+- Use a Redis lease for updating authorized projects. !7733
+- Refactor JiraService by moving code out of JiraService#execute method. !7756
+- Update GitLab Workhorse to v1.0.1. !7759
+- Fix pipelines info being hidden in merge request widget. !7808
+- Fixed commit timeago not rendering after initial page.
+- Fix for error thrown in cycle analytics events if build has not started.
+- Fixed issue boards issue sorting when dragging issue into list.
+- Allow access to the wiki with git when repository feature disabled.
+- Fixed timeago not rendering when resolving a discussion.
+- Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1.
+- Timeout creating and viewing merge request for binary file.
+- Gracefully recover from Redis connection failures in Sidekiq initializer.
+
## 8.14.1 (2016-11-28)
- Fix deselecting calendar days on contribution graph. !6453 (ClemMakesApps)
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 76f3c6506ed..cfab4721f4b 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -57,32 +57,11 @@
(function () {
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
- window.addEventListener('hashchange', gl.utils.shiftWindow);
-
- // automatically adjust scroll position for hash urls taking the height of the navbar into account
- // https://github.com/twitter/bootstrap/issues/1768
- window.adjustScroll = function() {
- var navbar = document.querySelector('.navbar-gitlab');
- var subnav = document.querySelector('.layout-nav');
- var fixedTabs = document.querySelector('.js-tabs-affix');
-
- adjustment = 0;
- if (navbar) adjustment -= navbar.offsetHeight;
- if (subnav) adjustment -= subnav.offsetHeight;
- if (fixedTabs) adjustment -= fixedTabs.offsetHeight;
-
- return scrollBy(0, adjustment);
- };
-
- window.addEventListener("hashchange", adjustScroll);
-
- window.onload = function () {
- // Scroll the window to avoid the topnav bar
- // https://github.com/twitter/bootstrap/issues/1768
- if (location.hash) {
- return setTimeout(adjustScroll, 100);
- }
- };
+ window.addEventListener('hashchange', gl.utils.handleLocationHash);
+ window.addEventListener('load', function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ gl.utils.handleLocationHash();
+ }, false);
$(function () {
var $body = $('body');
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index d5cb6164e0b..1644a772737 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -47,7 +47,7 @@
new gl.DueDateSelectors();
new LabelsSelect();
new Sidebar();
- new Subscription('.subscription');
+ gl.Subscription.bindAll('.subscription');
}
});
})();
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 16df4b0b005..ab521c6c1fc 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -135,8 +135,18 @@
new TreeView();
}
break;
+ case 'projects:pipelines:builds':
case 'projects:pipelines:show':
- new gl.Pipelines();
+ const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
+
+ new gl.Pipelines({
+ initTabs: true,
+ tabsOptions: {
+ action: controllerAction,
+ defaultAction: 'pipelines',
+ parentEl: '.pipelines-tabs',
+ },
+ });
break;
case 'groups:activity':
new gl.Activities();
@@ -262,7 +272,7 @@
new NotificationsDropdown();
break;
case 'wikis':
- new Wikis();
+ new gl.Wikis();
shortcut_handler = new ShortcutsNavigation();
new ZenMode();
new GLForm($('.wiki-form'));
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
index 6d9b0c4bc3e..3f12ad9ff9f 100644
--- a/app/assets/javascripts/extensions/element.js.es6
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -1,9 +1,20 @@
/* global Element */
-/* eslint-disable consistent-return, max-len */
-
-Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
+/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */
Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
if (!selectedElement) return;
return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
};
+
+Element.prototype.matches = Element.prototype.matches ||
+ Element.prototype.matchesSelector ||
+ Element.prototype.mozMatchesSelector ||
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.oMatchesSelector ||
+ Element.prototype.webkitMatchesSelector ||
+ function (s) {
+ const matches = (this.document || this.ownerDocument).querySelectorAll(s);
+ let i = matches.length;
+ while (--i >= 0 && matches.item(i) !== this) {}
+ return i > -1;
+ };
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
new file mode 100644
index 00000000000..e810ee85bd3
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
@@ -0,0 +1,113 @@
+/**
+ * Linked Tabs
+ *
+ * Handles persisting and restores the current tab selection and content.
+ * Reusable component for static content.
+ *
+ * ### Example Markup
+ *
+ * <ul class="nav-links tab-links">
+ * <li class="active">
+ * <a data-action="tab1" data-target="#tab1" data-toggle="tab" href="/path/tab1">
+ * Tab 1
+ * </a>
+ * </li>
+ * <li class="groups-tab">
+ * <a data-action="tab2" data-target="#tab2" data-toggle="tab" href="/path/tab2">
+ * Tab 2
+ * </a>
+ * </li>
+ *
+ *
+ * <div class="tab-content">
+ * <div class="tab-pane" id="tab1">
+ * Tab 1 Content
+ * </div>
+ * <div class="tab-pane" id="tab2">
+ * Tab 2 Content
+ * </div>
+ * </div>
+ *
+ *
+ * ### How to use
+ *
+ * new window.gl.LinkedTabs({
+ * action: "#{controller.action_name}",
+ * defaultAction: 'tab1',
+ * parentEl: '.tab-links'
+ * });
+ */
+
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.LinkedTabs = class LinkedTabs {
+ /**
+ * Binds the events and activates de default tab.
+ *
+ * @param {Object} options
+ */
+ constructor(options) {
+ this.options = options || {};
+
+ this.defaultAction = this.options.defaultAction;
+ this.action = this.options.action || this.defaultAction;
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.currentLocation = window.location;
+
+ const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
+
+ // since this is a custom event we need jQuery :(
+ $(document)
+ .off('shown.bs.tab', tabSelector)
+ .on('shown.bs.tab', tabSelector, e => this.tabShown(e));
+
+ this.activateTab(this.action);
+ }
+
+ /**
+ * Handles the `shown.bs.tab` event to set the currect url action.
+ *
+ * @param {type} evt
+ * @return {Function}
+ */
+ tabShown(evt) {
+ const source = evt.target.getAttribute('href');
+
+ return this.setCurrentAction(source);
+ }
+
+ /**
+ * Updates the URL with the path that matched the given action.
+ *
+ * @param {String} source
+ * @return {String}
+ */
+ setCurrentAction(source) {
+ const copySource = source;
+
+ copySource.replace(/\/+$/, '');
+
+ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
+
+ history.replaceState({
+ turbolinks: true,
+ url: newState,
+ }, document.title, newState);
+ return newState;
+ }
+
+ /**
+ * Given the current action activates the correct tab.
+ * http://getbootstrap.com/javascript/#tab-show
+ * Note: Will trigger `shown.bs.tab`
+ */
+ activateTab() {
+ return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
+ }
+ };
+})();
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index d83c41fae9d..c5846068b07 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */
(function() {
(function(w) {
var base;
@@ -94,10 +94,35 @@
return $(document).off('scroll');
};
- w.gl.utils.shiftWindow = function() {
- return w.scrollBy(0, -100);
- };
+ // automatically adjust scroll position for hash urls taking the height of the navbar into account
+ // https://github.com/twitter/bootstrap/issues/1768
+ w.gl.utils.handleLocationHash = function() {
+ var hash = w.gl.utils.getLocationHash();
+ if (!hash) return;
+
+ var navbar = document.querySelector('.navbar-gitlab');
+ var subnav = document.querySelector('.layout-nav');
+ var fixedTabs = document.querySelector('.js-tabs-affix');
+ var adjustment = 0;
+ if (navbar) adjustment -= navbar.offsetHeight;
+ if (subnav) adjustment -= subnav.offsetHeight;
+
+ // scroll to user-generated markdown anchor if we cannot find a match
+ if (document.getElementById(hash) === null) {
+ var target = document.getElementById('user-content-' + hash);
+ if (target && target.scrollIntoView) {
+ target.scrollIntoView(true);
+ window.scrollBy(0, adjustment);
+ }
+ } else {
+ // only adjust for fixedTabs when not targeting user-generated content
+ if (fixedTabs) {
+ adjustment -= fixedTabs.offsetHeight;
+ }
+ window.scrollBy(0, adjustment);
+ }
+ };
gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle');
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index a84db9c0233..72c6c4a1fcd 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,8 +1,15 @@
+//= require lib/utils/bootstrap_linked_tabs
+
/* eslint-disable */
((global) => {
class Pipelines {
- constructor() {
+ constructor(options) {
+
+ if (options.initTabs && options.tabsOptions) {
+ new global.LinkedTabs(options.tabsOptions);
+ }
+
this.addMarginToBuildColumns();
}
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index 6d75688deeb..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, one-var, one-var-declaration-per-line, camelcase, consistent-return, no-undef, padded-blocks, max-len */
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Subscription = (function() {
- function Subscription(container) {
- this.toggleSubscription = bind(this.toggleSubscription, this);
- var $container;
- this.$container = $(container);
- this.url = this.$container.attr('data-url');
- this.subscribe_button = this.$container.find('.js-subscribe-button');
- this.subscription_status = this.$container.find('.subscription-status');
- this.subscribe_button.unbind('click').click(this.toggleSubscription);
- }
-
- Subscription.prototype.toggleSubscription = function(event) {
- var action, btn, current_status;
- btn = $(event.currentTarget);
- action = btn.find('span').text();
- current_status = this.subscription_status.attr('data-status');
- btn.addClass('disabled');
-
- if ($('html').hasClass('issue-boards-page')) {
- this.url = this.$container.attr('data-url');
- }
-
- return $.post(this.url, (function(_this) {
- return function() {
- var status;
- btn.removeClass('disabled');
-
- if ($('html').hasClass('issue-boards-page')) {
- Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
- } else {
- status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
- _this.subscription_status.attr('data-status', status);
- action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
- btn.find('span').text(action);
- _this.subscription_status.find('>div').toggleClass('hidden');
- if (btn.attr('data-original-title')) {
- return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
- }
- }
- };
- })(this));
- };
-
- return Subscription;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/subscription.js.es6 b/app/assets/javascripts/subscription.js.es6
new file mode 100644
index 00000000000..62d1604fe9e
--- /dev/null
+++ b/app/assets/javascripts/subscription.js.es6
@@ -0,0 +1,50 @@
+/* global Vue */
+
+(() => {
+ class Subscription {
+ constructor(containerElm) {
+ this.containerElm = containerElm;
+
+ const subscribeButton = containerElm.querySelector('.js-subscribe-button');
+ if (subscribeButton) {
+ // remove class so we don't bind twice
+ subscribeButton.classList.remove('js-subscribe-button');
+ subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
+ }
+ }
+
+ toggleSubscription(event) {
+ const button = event.currentTarget;
+ const buttonSpan = button.querySelector('span');
+ if (!buttonSpan || button.classList.contains('disabled')) {
+ return;
+ }
+ button.classList.add('disabled');
+
+ const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
+ const toggleActionUrl = this.containerElm.dataset.url;
+
+ $.post(toggleActionUrl, () => {
+ button.classList.remove('disabled');
+
+ // hack to allow this to work with the issue boards Vue object
+ if (document.querySelector('html').classList.contains('issue-boards-page')) {
+ Vue.set(
+ gl.issueBoards.BoardsStore.detail.issue,
+ 'subscribed',
+ !gl.issueBoards.BoardsStore.detail.issue.subscribed,
+ );
+ } else {
+ buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
+ }
+ });
+ }
+
+ static bindAll(selector) {
+ [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
+ }
+ }
+
+ window.gl = window.gl || {};
+ window.gl.Subscription = Subscription;
+})();
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
deleted file mode 100644
index 5dd853389c2..00000000000
--- a/app/assets/javascripts/wikis.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, consistent-return, one-var, one-var-declaration-per-line, no-undef, prefer-template, padded-blocks, max-len */
-
-/*= require latinise */
-
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Wikis = (function() {
- function Wikis() {
- this.slugify = bind(this.slugify, this);
- $('.new-wiki-page').on('submit', (function(_this) {
- return function(e) {
- var field, path, slug;
- $('[data-error~=slug]').addClass('hidden');
- field = $('#new_wiki_path');
- slug = _this.slugify(field.val());
- if (slug.length > 0) {
- path = field.attr('data-wikis-path');
- location.href = path + '/' + slug;
- return e.preventDefault();
- }
- };
- })(this));
- }
-
- Wikis.prototype.dasherize = function(value) {
- return value.replace(/[_\s]+/g, '-');
- };
-
- Wikis.prototype.slugify = function(value) {
- return this.dasherize(value.trim().toLowerCase().latinise());
- };
-
- return Wikis;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
new file mode 100644
index 00000000000..ecff5fd5bf4
--- /dev/null
+++ b/app/assets/javascripts/wikis.js.es6
@@ -0,0 +1,73 @@
+/* eslint-disable no-param-reassign */
+/* global Breakpoints */
+
+/*= require latinise */
+/*= require breakpoints */
+/*= require jquery.nicescroll */
+
+((global) => {
+ const dasherize = str => str.replace(/[_\s]+/g, '-');
+ const slugify = str => dasherize(str.trim().toLowerCase().latinise());
+
+ class Wikis {
+ constructor() {
+ this.bp = Breakpoints.get();
+ this.sidebarEl = document.querySelector('.js-wiki-sidebar');
+ this.sidebarExpanded = false;
+ $(this.sidebarEl).niceScroll();
+
+ const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
+ for (let i = 0; i < sidebarToggles.length; i += 1) {
+ sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
+ }
+
+ this.newWikiForm = document.querySelector('form.new-wiki-page');
+ if (this.newWikiForm) {
+ this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ }
+
+ window.addEventListener('resize', () => this.renderSidebar());
+ this.renderSidebar();
+ }
+
+ handleNewWikiSubmit(e) {
+ if (!this.newWikiForm) return;
+
+ const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
+ const slug = slugify(slugInput.value);
+
+ if (slug.length > 0) {
+ const wikisPath = slugInput.getAttribute('data-wikis-path');
+ window.location.href = `${wikisPath}/${slug}`;
+ e.preventDefault();
+ }
+ }
+
+ handleToggleSidebar(e) {
+ e.preventDefault();
+ this.sidebarExpanded = !this.sidebarExpanded;
+ this.renderSidebar();
+ }
+
+ sidebarCanCollapse() {
+ const bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ }
+
+ renderSidebar() {
+ if (!this.sidebarEl) return;
+ const { classList } = this.sidebarEl;
+ if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
+ if (!classList.contains('right-sidebar-expanded')) {
+ classList.remove('right-sidebar-collapsed');
+ classList.add('right-sidebar-expanded');
+ }
+ } else if (classList.contains('right-sidebar-expanded')) {
+ classList.add('right-sidebar-collapsed');
+ classList.remove('right-sidebar-expanded');
+ }
+ }
+ }
+
+ global.Wikis = Wikis;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 8642b7530e2..81852158b94 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -2,7 +2,7 @@
padding-left: 0;
padding-right: 0;
- @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
overflow-x: scroll;
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index a9006de6d3e..eadb9409fee 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -38,7 +38,7 @@
}
}
-@media (max-width: $screen-md-min) {
+@media (max-width: $screen-sm-max) {
ul.notes {
.flash-container.timeline-content {
margin-left: 0;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index e83a1f7ad68..a01899ccbd2 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -98,7 +98,7 @@ label {
}
}
- @media(max-width: $screen-sm-min) {
+ @media(max-width: $screen-xs-max) {
padding: 0 $gl-padding;
.control-label,
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 91ab1503439..c5e5dad574d 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -21,7 +21,6 @@
background: $color-darker;
}
- .sidebar-header,
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 16ecf466931..f9bcbbf2ca5 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -228,7 +228,7 @@ header {
}
.page-sidebar-pinned.right-sidebar-expanded {
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
.header-content .title {
width: 300px;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 1839ffa0976..98f72e58c23 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -123,7 +123,7 @@
line-height: 28px;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index cb2c351c368..b37c1d0d670 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -43,7 +43,7 @@
/**
* Small screen pagination
*/
-@media (max-width: $screen-xs) {
+@media (max-width: $screen-xs-min) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
@@ -62,7 +62,7 @@
/**
* Medium screen pagination
*/
-@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
+@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) {
.gl-pagination {
.page {
display: none;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 44c445c0543..4269d365578 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -59,11 +59,6 @@
padding: 0 !important;
}
- .sidebar-header {
- padding: 11px 22px 12px;
- font-size: 20px;
- }
-
li {
&.separate-item {
padding-top: 10px;
@@ -220,7 +215,7 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- &:not(.build-sidebar) {
+ &:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width;
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 070e42d63d2..aa604b1cd19 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -182,6 +182,7 @@
left: -16px;
position: absolute;
text-decoration: none;
+ outline: none;
&::after {
content: image-url('icon_anchor.svg');
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 4327f8bf640..82f36f24867 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -325,7 +325,6 @@
}
.issuable-header-text {
- width: 100%;
padding-right: 35px;
> strong {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 498a8f68e49..0b4930ec98f 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -53,7 +53,7 @@
border-bottom: none;
position: relative;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 6px 0 24px;
}
}
@@ -61,7 +61,7 @@
.column {
text-align: center;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 15px 0;
}
@@ -78,7 +78,7 @@
}
&:last-child {
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
text-align: center;
}
}
@@ -156,7 +156,7 @@
}
.inner-content {
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
padding: 0 28px;
text-align: center;
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 4b382e8adaf..de3d2ba549f 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -8,7 +8,7 @@
font-size: 34px;
}
-@media (max-width: $screen-sm-min) {
+@media (max-width: $screen-xs-max) {
.environments-container {
width: 100%;
overflow: auto;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 57d028cec8c..a9af7af59e2 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -49,14 +49,14 @@
padding: 50px 100px;
overflow: hidden;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
padding: 50px 0;
}
svg {
float: right;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
float: none;
display: block;
width: 250px;
@@ -71,7 +71,7 @@
width: 460px;
margin-top: 120px;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
float: none;
margin-top: 60px;
width: auto;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 8843d1463db..dfc6079bd15 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -123,7 +123,7 @@
padding: 20px 0;
}
-@media (max-width: $screen-sm-min) {
+@media (max-width: $screen-xs-max) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 16ddef481bd..65775c45e5b 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -111,7 +111,7 @@
text-align: center;
font-size: 13px;
- @media (max-width: $screen-md-min) {
+ @media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 15ec8be831e..56a798a7b6d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -253,7 +253,7 @@ ul.notes {
}
.page-sidebar-pinned.right-sidebar-expanded {
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
.note-header {
.note-headline-light {
display: block;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 6fab97a71aa..f8677f93fe0 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -180,7 +180,7 @@
.modal-dialog {
width: 380px;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: auto;
}
@@ -261,4 +261,4 @@ table.u2f-registrations {
td:not(:last-child) {
border-right: solid 1px transparent;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0562ee7b178..1cf7587dbb4 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -189,7 +189,7 @@
}
.download-button {
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
margin-left: 0;
}
}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 69288b31cc4..779db77da60 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -37,7 +37,7 @@
@include make-md-column(6);
margin-top: 10px;
- @media (max-width: $screen-sm-min) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 2b836fa1f4a..20ad63be045 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -31,7 +31,7 @@
.last-commit {
@include str-truncated(506px);
- @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+ @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index dfaeba41cf6..b9f81533150 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,3 +4,128 @@
margin-right: auto;
padding-right: 7px;
}
+
+.wiki-page-header {
+ @extend .top-area;
+ position: relative;
+
+ .wiki-page-title {
+ margin: 0;
+ font-size: 22px;
+ }
+
+ .wiki-last-edit-by {
+ color: $gl-gray-light;
+
+ strong {
+ color: $gl-text-color;
+ }
+ }
+
+ .light {
+ font-weight: normal;
+ color: $gl-gray-light;
+ }
+
+ .git-access-header {
+ padding: 16px 40px 11px 0;
+ line-height: 28px;
+ font-size: 18px;
+ }
+
+ .git-clone-holder {
+ width: 100%;
+ padding-bottom: 40px;
+ }
+
+ button.sidebar-toggle {
+ position: absolute;
+ right: 0;
+ top: 11px;
+ display: block;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ &.has-sidebar-toggle {
+ padding-right: 40px;
+ }
+
+ .git-clone-holder {
+ width: 480px;
+ }
+
+ .nav-controls {
+ width: auto;
+ min-width: 50%;
+ white-space: nowrap;
+ }
+ }
+
+ @media (min-width: $screen-md-min) {
+ &.has-sidebar-toggle {
+ padding-right: 0;
+ }
+
+ button.sidebar-toggle {
+ display: none;
+ }
+ }
+}
+
+.wiki-git-access {
+ margin: $gl-padding 0;
+
+ h3 {
+ font-size: 22px;
+ font-weight: normal;
+ margin-top: 1.4em;
+ }
+}
+
+.right-sidebar.wiki-sidebar {
+ padding: $gl-padding 0;
+
+ &.right-sidebar-collapsed {
+ display: none;
+ }
+
+ .blocks-container {
+ padding: 0 $gl-padding;
+ }
+
+ .block {
+ width: 100%;
+ }
+
+ a {
+ color: $layout-link-gray;
+
+ &:hover,
+ &.active {
+ color: $black;
+ }
+ }
+
+ .active > a {
+ color: $black;
+ }
+
+ ul.wiki-pages,
+ ul.wiki-pages li {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ ul.wiki-pages li {
+ margin: 5px 0 10px;
+ }
+
+ .wiki-sidebar-header {
+ padding: 0 $gl-padding $gl-padding;
+
+ .gutter-toggle {
+ margin-top: 0;
+ }
+ }
+}
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index a10cdcce72b..37feff79999 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -7,8 +7,10 @@ class HelpController < ApplicationController
@help_index = File.read(Rails.root.join('doc', 'README.md'))
# Prefix Markdown links with `help/` unless they are external links
- # See http://rubular.com/r/MioSrVLK3S
- @help_index.gsub!(%r{(\]\()(?!.+://)([^\)\(]+\))}, '\1/help/\2')
+ # See http://rubular.com/r/X3baHTbPO2
+ @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do
+ "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}"
+ end
end
def show
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f47df8b623b..d2cef52842c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -492,7 +492,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
- if @merge_request.commits.blank?
+ if @merge_request.has_no_commits?
# and if target branch doesn't exist
return invalid_mr unless @merge_request.target_branch_exists?
end
@@ -500,7 +500,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars
@noteable = @merge_request
- @commits_count = @merge_request.commits.count
+ @commits_count = @merge_request.commits_count
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 533af80aee0..85188cfdd4c 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -1,6 +1,6 @@
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
- before_action :commit, only: [:show]
+ before_action :commit, only: [:show, :builds]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
@@ -32,6 +32,14 @@ class Projects::PipelinesController < Projects::ApplicationController
def show
end
+ def builds
+ respond_to do |format|
+ format.html do
+ render 'show'
+ end
+ end
+ end
+
def retry
pipeline.retry_failed(current_user)
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 177ccf5eec9..c3353446fd1 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -115,6 +115,8 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
+
+ @sidebar_wiki_pages = @project_wiki.pages.first(15)
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9a74e36870b..001c83ccb4b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -49,6 +49,32 @@ class IssuableFinder
execute.find_by(*params)
end
+ # We often get counts for each state by running a query per state, and
+ # counting those results. This is typically slower than running one query
+ # (even if that query is slower than any of the individual state queries) and
+ # grouping and counting within that query.
+ #
+ def count_by_state
+ count_params = params.merge(state: nil, sort: nil)
+ labels_count = label_names.any? ? label_names.count : 1
+ finder = self.class.new(current_user, count_params)
+ counts = Hash.new(0)
+
+ # Searching by label includes a GROUP BY in the query, but ours will be last
+ # because it is added last. Searching by multiple labels also includes a row
+ # per issuable, so we have to count those in Ruby - which is bad, but still
+ # better than performing multiple queries.
+ #
+ finder.execute.reorder(nil).group(:state).count.each do |key, value|
+ counts[Array(key).last.to_sym] += value / labels_count
+ end
+
+ counts[:all] = counts.values.sum
+ counts[:opened] += counts[:reopened]
+
+ counts
+ end
+
def group
return @group if defined?(@group)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 6584aa3edd5..8231f8fa334 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -180,12 +180,9 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- issuables_finder = public_send("#{issuable_type}_finder")
-
- params = issuables_finder.params.merge(state: state)
- finder = issuables_finder.class.new(issuables_finder.current_user, params)
-
- finder.execute.page(1).total_count
+ @counts ||= {}
+ @counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state
+ @counts[issuable_type][state]
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
@@ -195,6 +192,7 @@ module IssuablesHelper
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+ opts.delete_if { |_, value| value.blank? }
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index df87fac132d..a3331dc80cb 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -20,6 +20,11 @@ module NavHelper
end
elsif current_path?('builds#show')
"page-gutter build-sidebar right-sidebar-expanded"
+ elsif current_path?('wikis#show') ||
+ current_path?('wikis#edit') ||
+ current_path?('wikis#history') ||
+ current_path?('wikis#git_access')
+ "page-gutter wiki-sidebar right-sidebar-expanded"
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e7d33bd26db..88c46076df6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -203,7 +203,7 @@ module Ci
.reorder(iid: :asc)
merge_requests.find do |merge_request|
- merge_request.commits.any? { |ci| ci.id == pipeline.sha }
+ merge_request.commits_sha.include?(pipeline.sha)
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 64990f8134e..bfb016df46d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -22,7 +22,8 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
- delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
+ delegate :commits, :real_size, :commits_sha, :commits_count,
+ to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -662,7 +663,7 @@ class MergeRequest < ActiveRecord::Base
end
def broken?
- self.commits.blank? || branch_missing? || cannot_be_merged?
+ has_no_commits? || branch_missing? || cannot_be_merged?
end
def can_be_merged_by?(user)
@@ -770,10 +771,6 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
- def commits_sha
- commits.map(&:sha)
- end
-
def head_pipeline
return unless diff_head_sha && source_project
@@ -871,4 +868,12 @@ class MergeRequest < ActiveRecord::Base
@conflicts_can_be_resolved_in_ui = false
end
end
+
+ def has_commits?
+ commits_count > 0
+ end
+
+ def has_no_commits?
+ !has_commits?
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 58a24eb84cb..b8f36a2c958 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -127,11 +127,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits_sha
- if @commits
- commits.map(&:sha)
- else
- st_commits.map { |commit| commit[:id] }
- end
+ st_commits.map { |commit| commit[:id] }
end
def diff_refs
@@ -176,6 +172,10 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
end
+ def commits_count
+ st_commits.count
+ end
+
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 22596b4014a..e4056306bc4 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -63,7 +63,7 @@ module MergeRequests
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
- mr_commit_ids = merge_request.commits.map(&:id)
+ mr_commit_ids = merge_request.commits_sha
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
@@ -123,7 +123,7 @@ module MergeRequests
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
- mr_commit_ids = Set.new(merge_request.commits.map(&:id))
+ mr_commit_ids = Set.new(merge_request.commits_sha)
new_commits, existing_commits = @commits.partition do |commit|
mr_commit_ids.include?(commit.id)
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 2a6d9cda379..817e4bebb05 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,5 +1,4 @@
.nav-sidebar
- .sidebar-header Across GitLab
%ul.nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
index 21c9563e9db..a08c7f2af09 100644
--- a/app/views/projects/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -1,11 +1,7 @@
- if current_user
.block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
- .title
+ %span.issuable-header-text.hide-collapsed.pull-left
Notifications
- %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
- .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
- .unsubscribed{ "v-show" => "!issue.subscribed" }
- You're not receiving notifications from this thread.
- .subscribed{ "v-show" => "issue.subscribed" }
- You're receiving notifications because you're subscribed to this thread.
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %span
+ {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index eab48b78cb3..5cc92595fe0 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -27,7 +27,7 @@
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 20c93930abc..eee711dc5af 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -11,7 +11,7 @@
= render 'projects/merge_requests/widget/open/archived'
- elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch'
- - elsif @merge_request.commits.blank?
+ - elsif @merge_request.has_no_commits?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check'
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 718314701f9..3464e155a1b 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,14 +1,17 @@
.tabs-holder
- %ul.nav-links.no-top.no-bottom
- %li.active
- = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' }, class: 'pipeline-tab'
- %li
- = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' }, class: 'builds-tab' do
+ %ul.pipelines-tabs.nav-links.no-top.no-bottom
+ %li.js-pipeline-tab-link
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ Pipeline
+ %li.js-builds-tab-link
+ = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
Builds
- %span.badge= pipeline.statuses.count
+ %span.badge.js-builds-counter= pipeline.statuses.count
+
+
.tab-content
- #js-tab-pipeline.tab-pane.active
+ #js-tab-pipeline.tab-pane
.build-content.middle-block.pipeline-graph
.pipeline-visualization
%ul.stage-column-list
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 8c6652a5f90..29a41bc664b 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -2,7 +2,7 @@
- page_title "Pipeline"
= render "projects/pipelines/head"
-%div{ class: container_class }
+%div.js-pipeline-container{ class: container_class, data: { controller_action: "#{controller.action_name}" } }
- if @commit
= render "projects/pipelines/info"
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 4e41a15d9f4..c52527332bc 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -34,9 +34,6 @@
- if @page && @page.persisted?
= f.submit 'Save changes', class: "btn-save btn"
.pull-right
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do
- Delete
= link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped"
- else
= f.submit 'Create page', class: "btn-create btn"
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
deleted file mode 100644
index afdef70e1cf..00000000000
--- a/app/views/projects/wikis/_nav.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-= content_for :sub_nav do
- .scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
-
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wikis_pages_path(@project.namespace, @project)
-
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
-
- = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
new file mode 100644
index 00000000000..cad9c15a49e
--- /dev/null
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar
+ .block.wiki-sidebar-header.append-bottom-default
+ %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
+ = icon('angle-double-right')
+
+ - git_access_url = namespace_project_wikis_git_access_path(@project.namespace, @project)
+ = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
+ = succeed '&nbsp;' do
+ = icon('cloud-download')
+ Clone repository
+
+ .blocks-container
+ .block.block-first
+ %ul.wiki-pages
+ - @sidebar_wiki_pages.each do |wiki_page|
+ %li{ class: params[:id] == wiki_page.slug ? 'active' : '' }
+ = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
+ = wiki_page.title.capitalize
+ .block
+ = link_to namespace_project_wikis_pages_path(@project.namespace, @project), class: 'btn btn-block' do
+ More Pages
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 679d6018bef..8cf018da1b7 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,23 +1,35 @@
- @no_container = true
- page_title "Edit", @page.title.capitalize, "Wiki"
-= render 'nav'
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong
+ %h2.wiki-page-title
- if @page.persisted?
= link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- else
= @page.title.capitalize
- %span.light
- &middot;
- Edit Page
+ %span.light
+ &middot;
+ - if @page.persisted?
+ Edit Page
+ - else
+ Create Page
.nav-controls
- - if !(@page && @page.persisted?)
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New Page
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ - if @page.persisted?
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
+ Page History
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do
+ Delete
= render 'form'
+
+= render 'sidebar'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index b8811a28dd6..e25d6a48573 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,34 +1,36 @@
- @no_container = true
- page_title "Git Access", "Wiki"
-= render 'nav'
%div{ class: container_class }
- .sub-header-block
- %span.oneline
- Git access for
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
+ .git-access-header
+ Clone repository
%strong= @project_wiki.path_with_namespace
- .pull-right
- = render "shared/clone_panel", project: @project_wiki
+ = render "shared/clone_panel", project: @project_wiki
+
+ .wiki-git-access
+ %h3 Install Gollum
+ %pre.dark
+ :preserve
+ gem install gollum
- .prepend-top-default
- %fieldset
- %legend Install Gollum:
- %pre.dark
- :preserve
- gem install gollum
+ %h3 Clone your wiki
+ %pre.dark
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ cd #{h @project_wiki.path}
- %legend Clone Your Wiki:
- %pre.dark
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{h @project_wiki.path}
+ %h3 Start Gollum and edit locally
+ %pre.dark
+ :preserve
+ gollum
+ == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
+ >> Thin web server (v1.5.0 codename Knife)
+ >> Maximum connections set to 1024
+ >> Listening on 0.0.0.0:4567, CTRL+C to stop
- %legend Start Gollum And Edit Locally:
- %pre.dark
- :preserve
- gollum
- == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
- >> Thin web server (v1.5.0 codename Knife)
- >> Maximum connections set to 1024
- >> Listening on 0.0.0.0:4567, CTRL+C to stop
+= render 'sidebar'
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 4c0b14e2c42..dd7213622c1 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,13 +1,16 @@
- page_title "History", @page.title.capitalize, "Wiki"
-= render 'nav'
+
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong
+ %h2.wiki-page-title
= link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- %span.light
- &middot;
- History
+ %span.light
+ &middot;
+ History
.table-holder
%table.table
@@ -35,3 +38,5 @@
%td
%strong
= @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+
+= render 'sidebar'
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 9c10acd4cb6..e1eaffc6884 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,9 +1,18 @@
- @no_container = true
- page_title "Pages", "Wiki"
-= render 'nav'
-
%div{ class: container_class }
+ .wiki-page-header
+
+ .nav-text
+ %h2.wiki-page-title
+ Wiki Pages
+
+ .nav-controls
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project), class: 'btn' do
+ = icon('cloud-download')
+ Clone repository
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 5cebb538cf5..1b6dceee241 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,15 +1,19 @@
- @no_container = true
- page_title @page.title.capitalize, "Wiki"
-= render 'nav'
%div{ class: container_class }
- .top-area
+ .wiki-page-header.has-sidebar-toggle
+ %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ = icon('angle-double-left')
+
.nav-text
- %strong= @page.title.capitalize
+ %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- &middot;
- last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ Last edited by
+ %strong
+ #{@page.commit.author.name}
+ #{time_ago_with_tooltip(@page.commit.authored_date)}
.nav-controls
= render 'main_links'
@@ -19,8 +23,9 @@
This is an old version of this page.
You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
= preserve do
= render_wiki_content(@page)
+
+= render 'sidebar'
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index baa6d5f8206..26b349e8a62 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,4 +1,4 @@
-- if @issues.reorder(nil).any?
+- if @issues.to_a.any?
- @issues.group_by(&:project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index ca3178395c1..2f3605b4d27 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-- if @merge_requests.reorder(nil).any?
+- if @merge_requests.to_a.any?
- @merge_requests.group_by(&:target_project).each do |group|
.panel.panel-default.panel-small
- project = group[0]
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 02427650219..958f8413e1d 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -144,16 +144,11 @@
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
- .title.hide-collapsed
+ %span.issuable-header-text.hide-collapsed.pull-left
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
- .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
- .unsubscribed{class: ( 'hidden' if subscribed )}
- You're not receiving notifications from this thread.
- .subscribed{class: ( 'hidden' unless subscribed )}
- You're receiving notifications because you're subscribed to this thread.
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
@@ -170,6 +165,6 @@
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
- new Subscription('.subscription')
+ gl.Subscription.bindAll('.subscription');
new gl.DueDateSelectors();
sidebar = new Sidebar();
diff --git a/changelogs/unreleased/18546-update-wiki-page-design.yml b/changelogs/unreleased/18546-update-wiki-page-design.yml
new file mode 100644
index 00000000000..c76e17340f2
--- /dev/null
+++ b/changelogs/unreleased/18546-update-wiki-page-design.yml
@@ -0,0 +1,4 @@
+---
+title: Update wiki page design
+merge_request: 7429
+author:
diff --git a/changelogs/unreleased/22781-user-generated-permalinks.yml b/changelogs/unreleased/22781-user-generated-permalinks.yml
new file mode 100644
index 00000000000..e46739e48e3
--- /dev/null
+++ b/changelogs/unreleased/22781-user-generated-permalinks.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent DOM ID collisions resulting from user-generated content anchors
+merge_request: 7631
+author:
diff --git a/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml
new file mode 100644
index 00000000000..2227c81bd34
--- /dev/null
+++ b/changelogs/unreleased/24281-issue-merge-request-sidebar-subscribe-button-style-improvement.yml
@@ -0,0 +1,4 @@
+---
+title: Remove the help text under the sidebar subscribe button and style it inline
+merge_request: 7389
+author:
diff --git a/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml
new file mode 100644
index 00000000000..01b19a47ecd
--- /dev/null
+++ b/changelogs/unreleased/24669-merge-request-dashboard-page-takes-over-a-minute-to-load.yml
@@ -0,0 +1,4 @@
+---
+title: Speed up issuable dashboards
+merge_request:
+author:
diff --git a/changelogs/unreleased/24726-remove-across-gitlab.yml b/changelogs/unreleased/24726-remove-across-gitlab.yml
new file mode 100644
index 00000000000..6436e4b688f
--- /dev/null
+++ b/changelogs/unreleased/24726-remove-across-gitlab.yml
@@ -0,0 +1,4 @@
+---
+title: 24726 Remove Across GitLab from side navigation
+merge_request:
+author:
diff --git a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml b/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml
deleted file mode 100644
index 9254db40742..00000000000
--- a/changelogs/unreleased/24813-project-members-with-developer-access-can-no-longer-create-tags.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Pass tag SHA to post-receive hook when tag is created via UI
-merge_request: 7700
-author:
diff --git a/changelogs/unreleased/24814-pipeline-tabs.yml b/changelogs/unreleased/24814-pipeline-tabs.yml
new file mode 100644
index 00000000000..f85e7576905
--- /dev/null
+++ b/changelogs/unreleased/24814-pipeline-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Cicking on tabs on pipeline page should set URL
+merge_request: 7709
+author:
diff --git a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml b/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml
deleted file mode 100644
index 4b4aea79380..00000000000
--- a/changelogs/unreleased/24860-actionview-template-error-undefined-method-size-for-nil-nilclass.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent error when submitting a merge request and pipeline is not defined
-merge_request: 7707
-author:
diff --git a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml b/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml
deleted file mode 100644
index 7ddf0b46d4c..00000000000
--- a/changelogs/unreleased/24894-style-system-note-in-commit-discussion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes system note style in commit discussion
-merge_request: 7721
-author:
diff --git a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml b/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml
deleted file mode 100644
index dad9db0ffef..00000000000
--- a/changelogs/unreleased/25055-pipelines-info-missing-from-mr-widget.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipelines info being hidden in merge request widget
-merge_request: 7808
-author:
diff --git a/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml
new file mode 100644
index 00000000000..58efd9113f2
--- /dev/null
+++ b/changelogs/unreleased/25199-fix-broken-urls-in-help-page.yml
@@ -0,0 +1,4 @@
+---
+title: Don't change relative URLs to absolute URLs in the Help page
+merge_request:
+author:
diff --git a/changelogs/unreleased/4269-public-api.yml b/changelogs/unreleased/4269-public-api.yml
new file mode 100644
index 00000000000..560bc6a4f13
--- /dev/null
+++ b/changelogs/unreleased/4269-public-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow public access to some Project API endpoints
+merge_request: 7843
+author:
diff --git a/changelogs/unreleased/boards-issue-sorting.yml b/changelogs/unreleased/boards-issue-sorting.yml
deleted file mode 100644
index fb7dc2f9190..00000000000
--- a/changelogs/unreleased/boards-issue-sorting.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed issue boards issue sorting when dragging issue into list
-merge_request:
-author:
diff --git a/changelogs/unreleased/clean-up-jira-service.yml b/changelogs/unreleased/clean-up-jira-service.yml
deleted file mode 100644
index a309cb57532..00000000000
--- a/changelogs/unreleased/clean-up-jira-service.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Refactor JiraService by moving code out of JiraService#execute method
-merge_request: 7756
-author:
diff --git a/changelogs/unreleased/comments-fixture.yml b/changelogs/unreleased/comments-fixture.yml
new file mode 100644
index 00000000000..824c1c88a60
--- /dev/null
+++ b/changelogs/unreleased/comments-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for notes_spec
+merge_request: 7683
+author: winniehell
diff --git a/changelogs/unreleased/events-cache-invalidation.yml b/changelogs/unreleased/events-cache-invalidation.yml
deleted file mode 100644
index 2b30f4dcbce..00000000000
--- a/changelogs/unreleased/events-cache-invalidation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove caching of events data
-merge_request: 6578
-author:
diff --git a/changelogs/unreleased/fix-ca-no-date.yml b/changelogs/unreleased/fix-ca-no-date.yml
deleted file mode 100644
index 6de4a56ac0d..00000000000
--- a/changelogs/unreleased/fix-ca-no-date.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix for error thrown in cycle analytics events if build has not started
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml b/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml
deleted file mode 100644
index 82ca6316876..00000000000
--- a/changelogs/unreleased/fix-git-access-wiki-when-repository-feature-disabled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow access to the wiki with git when repository feature disabled
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-github-branch-formatter.yml b/changelogs/unreleased/fix-github-branch-formatter.yml
new file mode 100644
index 00000000000..c8698f507de
--- /dev/null
+++ b/changelogs/unreleased/fix-github-branch-formatter.yml
@@ -0,0 +1,4 @@
+---
+title: Fix branch validation for GitHub PR where repo/fork was renamed/deleted
+merge_request:
+author:
diff --git a/changelogs/unreleased/fixed-commit-timeago.yml b/changelogs/unreleased/fixed-commit-timeago.yml
deleted file mode 100644
index 295d8db63d0..00000000000
--- a/changelogs/unreleased/fixed-commit-timeago.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed commit timeago not rendering after initial page
-merge_request:
-author:
diff --git a/changelogs/unreleased/refresh-authorizations-with-lease.yml b/changelogs/unreleased/refresh-authorizations-with-lease.yml
deleted file mode 100644
index bb9b77018e3..00000000000
--- a/changelogs/unreleased/refresh-authorizations-with-lease.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use a Redis lease for updating authorized projects
-merge_request: 7733
-author:
diff --git a/changelogs/unreleased/rephrase-system-notes.yml b/changelogs/unreleased/rephrase-system-notes.yml
deleted file mode 100644
index e77c3a31cb4..00000000000
--- a/changelogs/unreleased/rephrase-system-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rephrase some system notes to be compatible with new system note style
-merge_request: 7692
-author:
diff --git a/changelogs/unreleased/resolve-discussions-timeago.yml b/changelogs/unreleased/resolve-discussions-timeago.yml
deleted file mode 100644
index ffedeb93f1d..00000000000
--- a/changelogs/unreleased/resolve-discussions-timeago.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed timeago not rendering when resolving a discussion
-merge_request:
-author:
diff --git a/changelogs/unreleased/right-sidebar-fixture.yml b/changelogs/unreleased/right-sidebar-fixture.yml
new file mode 100644
index 00000000000..46a3e459fef
--- /dev/null
+++ b/changelogs/unreleased/right-sidebar-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for right_sidebar_spec
+merge_request: 7687
+author: winniehell
diff --git a/changelogs/unreleased/sh-update-sidekiq-cron.yml b/changelogs/unreleased/sh-update-sidekiq-cron.yml
deleted file mode 100644
index d79ba817a18..00000000000
--- a/changelogs/unreleased/sh-update-sidekiq-cron.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update Sidekiq-cron to fix compatibility issues with Sidekiq 4.2.1
-merge_request:
-author:
diff --git a/changelogs/unreleased/shortcuts-issuable-fixture.yml b/changelogs/unreleased/shortcuts-issuable-fixture.yml
new file mode 100644
index 00000000000..88945600886
--- /dev/null
+++ b/changelogs/unreleased/shortcuts-issuable-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for shortcuts_issuable_spec
+merge_request: 7685
+author: winniehell
diff --git a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml b/changelogs/unreleased/timeout-merge-request-for-binary-file.yml
deleted file mode 100644
index 5161265d1bd..00000000000
--- a/changelogs/unreleased/timeout-merge-request-for-binary-file.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Timeout creating and viewing merge request for binary file
-merge_request:
-author:
diff --git a/changelogs/unreleased/use-st-commits-where-possible.yml b/changelogs/unreleased/use-st-commits-where-possible.yml
new file mode 100644
index 00000000000..e4395461560
--- /dev/null
+++ b/changelogs/unreleased/use-st-commits-where-possible.yml
@@ -0,0 +1,5 @@
+---
+title: Replace references to MergeRequestDiff#commits with st_commits when we care
+ only about the number of commits
+merge_request: 7668
+author:
diff --git a/changelogs/unreleased/workhorse-v1-0-1.yml b/changelogs/unreleased/workhorse-v1-0-1.yml
deleted file mode 100644
index c26c2d45b1d..00000000000
--- a/changelogs/unreleased/workhorse-v1-0-1.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update GitLab Workhorse to v1.0.1
-merge_request: 7759
-author:
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb
index 0da584626ee..6979f4641b0 100644
--- a/config/initializers/ar_monkey_patch.rb
+++ b/config/initializers/ar_monkey_patch.rb
@@ -52,6 +52,23 @@ module ActiveRecord
raise
end
end
+
+ # This is patched because we need it to query `lock_version IS NULL`
+ # rather than `lock_version = 0` whenever lock_version is NULL.
+ def relation_for_destroy
+ return super unless locking_enabled?
+
+ column_name = self.class.locking_column
+ super.where(self.class.arel_table[column_name].eq(self[column_name]))
+ end
+ end
+
+ # This is patched because we want `lock_version` default to `NULL`
+ # rather than `0`
+ class LockingType < SimpleDelegator
+ def type_cast_from_database(value)
+ super
+ end
end
end
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index b87b31d9697..1d7a3f03ace 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -61,5 +61,5 @@ begin
end
end
end
-rescue Redis::BaseError, SocketError
+rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 1336484a399..0754f0ec3b0 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -129,6 +129,7 @@ constraints(ProjectUrlConstrainer.new) do
member do
post :cancel
post :retry
+ get :builds
end
end
diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md
index 64353f7282b..3ba8387c7f0 100644
--- a/doc/administration/build_artifacts.md
+++ b/doc/administration/build_artifacts.md
@@ -84,7 +84,7 @@ _The artifacts are stored by default in
## Set the maximum file size of the artifacts
Provided the artifacts are enabled, you can change the maximum file size of the
-artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration#maximum-artifacts-size).
+artifacts through the [Admin area settings](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size).
[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
[restart gitlab]: restart_gitlab.md "How to restart GitLab"
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 89088cf9b83..28141cced3b 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -270,12 +270,16 @@ which can be avoided if a different driver is used, for example `overlay`.
## Using the GitLab Container Registry
-> **Note:**
-This feature requires GitLab 8.8 and GitLab Runner 1.2.
-
-Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, if you're using
-docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
+> **Notes:**
+- This feature requires GitLab 8.8 and GitLab Runner 1.2.
+- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
+ to pass a personal access token instead of your password in order to login to
+ GitLab's Container Registry.
+Once you've built a Docker image, you can push it up to the built-in
+[GitLab Container Registry](../../user/project/container_registry.md). For example,
+if you're using docker-in-docker on your runners, this is how your `.gitlab-ci.yml`
+could look like:
```yaml
build:
@@ -354,10 +358,20 @@ deploy:
```
Some things you should be aware of when using the Container Registry:
-* You must log in to the container registry before running commands. Putting this in `before_script` will run it before each build job.
-* Using `docker build --pull` makes sure that Docker fetches any changes to base images before building just in case your cache is stale. It takes slightly longer, but means you don’t get stuck without security patches to base images.
-* Doing an explicit `docker pull` before each `docker run` makes sure to fetch the latest image that was just built. This is especially important if you are using multiple runners that cache images locally. Using the git SHA in your image tag makes this less necessary since each build will be unique and you shouldn't ever have a stale image, but it's still possible if you re-build a given commit after a dependency has changed.
-* You don't want to build directly to `latest` in case there are multiple builds happening simultaneously.
+
+- You must log in to the container registry before running commands. Putting
+ this in `before_script` will run it before each build job.
+- Using `docker build --pull` makes sure that Docker fetches any changes to base
+ images before building just in case your cache is stale. It takes slightly
+ longer, but means you don’t get stuck without security patches to base images.
+- Doing an explicit `docker pull` before each `docker run` makes sure to fetch
+ the latest image that was just built. This is especially important if you are
+ using multiple runners that cache images locally. Using the git SHA in your
+ image tag makes this less necessary since each build will be unique and you
+ shouldn't ever have a stale image, but it's still possible if you re-build a
+ given commit after a dependency has changed.
+- You don't want to build directly to `latest` in case there are multiple builds
+ happening simultaneously.
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index cf7c55f75f2..efca05af7b8 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -6,7 +6,7 @@
GitLab 8.12 has a completely redesigned build permissions system.
Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
-Triggers can be used to force a rebuild of a specific branch, tag or commit,
+Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call.
## Add a trigger
@@ -29,6 +29,10 @@ irreversible.
## Trigger a build
+> **Note**:
+Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
+it will not trigger a build.
+
To trigger a build you need to send a `POST` request to GitLab's API endpoint:
```
@@ -36,8 +40,8 @@ POST /projects/:id/trigger/builds
```
The required parameters are the trigger's `token` and the Git `ref` on which
-the trigger will be performed. Valid refs are the branch, the tag or the commit
-SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md)
+the trigger will be performed. Valid refs are the branch and the tag. The `:id`
+of a project can be found by [querying the API](../../api/projects.md)
or by visiting the **Triggers** page which provides self-explanatory examples.
When a rebuild is triggered, the information is exposed in GitLab's UI under
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 7bfc9cb361f..0f78e8238af 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -141,51 +141,3 @@ in an initializer._
### Further reading
- Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
-
-## ID-based CSS selectors need to be a bit more specific
-
-Normally, because HTML `id` attributes need to be unique to the page, it's
-perfectly fine to write some JavaScript like the following:
-
-```javascript
-$('#js-my-selector').hide();
-```
-
-However, there's a feature of GitLab's Markdown processing that [automatically
-adds anchors to header elements][ToC Processing], with the `id` attribute being
-automatically generated based on the content of the header.
-
-Unfortunately, this feature makes it possible for user-generated content to
-create a header element with the same `id` attribute we're using in our
-selector, potentially breaking the JavaScript behavior. A user could break the
-above example with the following Markdown:
-
-```markdown
-## JS My Selector
-```
-
-Which gets converted to the following HTML:
-
-```html
-<h2>
- <a id="js-my-selector" class="anchor" href="#js-my-selector" aria-hidden="true"></a>
- JS My Selector
-</h2>
-```
-
-[ToC Processing]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/lib/banzai/filter/table_of_contents_filter.rb#L31-37
-
-### Solution
-
-The current recommended fix for this is to make our selectors slightly more
-specific:
-
-```javascript
-$('div#js-my-selector').hide();
-```
-
-### Further reading
-
-- Issue: [Merge request ToC anchor conflicts with tabs](https://gitlab.com/gitlab-org/gitlab-ce/issues/3908)
-- Merge Request: [Make tab target selectors less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2023)
-- Merge Request: [Make cross-project reference's clipboard target less naive](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2024)
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index b205fea2c40..47a4a3f85d0 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -4,13 +4,15 @@
---
-> **Note**
-Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
-versions earlier than 1.10.
->
-This document is about the user guide. To learn how to enable GitLab Container
-Registry across your GitLab instance, visit the
-[administrator documentation](../../administration/container_registry.md).
+>**Notes:**
+- Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
+ versions earlier than 1.10.
+- This document is about the user guide. To learn how to enable GitLab Container
+ Registry across your GitLab instance, visit the
+ [administrator documentation](../../administration/container_registry.md).
+- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need
+ to pass a personal access token instead of your password in order to login to
+ GitLab's Container Registry.
With the Docker Container Registry integrated into GitLab, every project can
have its own space to store its Docker images.
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 4f12acb8398..320faff65c5 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -187,11 +187,17 @@ To properly configure submodules with GitLab CI, read the
With the update permission model we also extended the support for accessing
Container Registries for private projects.
-> **Note:**
-As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for
-permissions, this makes the `image:` directive to not work with private projects
-automatically. The manual configuration by an Administrator is required to use
-private images. We plan to remove that limitation in one of the upcoming releases.
+> **Notes:**
+- GitLab Runner versions prior to 1.8 don't incorporate the introduced changes
+ for permissions. This makes the `image:` directive to not work with private
+ projects automatically and it needs to be configured manually on Runner's host
+ with a predefined account (for example administrator's personal account with
+ access token created explicitly for this purpose). This issue is resolved with
+ latest changes in GitLab Runner 1.8 which receives GitLab credentials with
+ build data.
+- Starting with GitLab 8.12, if you have 2FA enabled in your account, you need
+ to pass a personal access token instead of your password in order to login to
+ GitLab's Container Registry.
Your builds can access all container images that you would normally have access
to. The only implication is that you can push to the Container Registry of the
diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png
index a552888ed96..21ddec4ebdf 100644
--- a/doc/web_hooks/ssl.png
+++ b/doc/web_hooks/ssl.png
Binary files differ
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index cd37189fdd2..1659dd1f6cb 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1,17 +1,21 @@
# Webhooks
-_**Note:**
-Starting from GitLab 8.5:_
+>**Note:**
+Starting from GitLab 8.5:
+- the `repository` key is deprecated in favor of the `project` key
+- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
+- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
-- _the `repository` key is deprecated in favor of the `project` key_
-- _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_
-- _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_
+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
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
-Project webhooks allow you to trigger an URL if new code is pushed or a new issue is created.
+Webhooks can be used to update an external issue tracker, trigger CI builds,
+update a backup mirror, or even deploy to your production server.
-You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data 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.
## Webhook endpoint tips
@@ -26,21 +30,27 @@ GitLab webhooks keep in mind the following things:
you are writing a low-level hook this is important to remember.
- GitLab ignores the HTTP status code returned by your endpoint.
-## Secret Token
+## Secret token
-If you specify a secret token, it will be sent with the hook request in the `X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify that the request is legitimate.
+If you specify a secret token, it will be sent with the hook request in the
+`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
+that the request is legitimate.
-## SSL Verification
+## SSL verification
By default, the SSL certificate of the webhook endpoint is verified based on
-an internal list of Certificate Authorities,
-which means the certificate cannot be self-signed.
+an internal list of Certificate Authorities, which means the certificate cannot
+be self-signed.
You can turn this off in the webhook settings in your GitLab projects.
![SSL Verification](ssl.png)
-## Push events
+## Events
+
+Below are described the supported events.
+
+### Push events
Triggered when you push to the repository except when pushing tags.
@@ -121,7 +131,7 @@ X-Gitlab-Event: Push Hook
}
```
-## Tag events
+### Tag events
Triggered when you create (or delete) tags to the repository.
@@ -174,7 +184,7 @@ X-Gitlab-Event: Tag Push Hook
}
```
-## Issues events
+### Issues events
Triggered when a new issue is created or an existing issue was updated/closed/reopened.
@@ -240,7 +250,7 @@ X-Gitlab-Event: Issue Hook
}
}
```
-## Comment events
+### Comment events
Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
@@ -253,7 +263,7 @@ Valid target types:
3. `issue`
4. `snippet`
-### Comment on commit
+#### Comment on commit
**Request header**:
@@ -332,7 +342,7 @@ X-Gitlab-Event: Note Hook
}
```
-### Comment on merge request
+#### Comment on merge request
**Request header**:
@@ -459,7 +469,7 @@ X-Gitlab-Event: Note Hook
}
```
-### Comment on issue
+#### Comment on issue
**Request header**:
@@ -534,7 +544,7 @@ X-Gitlab-Event: Note Hook
}
```
-### Comment on code snippet
+#### Comment on code snippet
**Request header**:
@@ -607,7 +617,7 @@ X-Gitlab-Event: Note Hook
}
```
-## Merge request events
+### Merge request events
Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
@@ -699,7 +709,7 @@ X-Gitlab-Event: Merge Request Hook
}
```
-## Wiki Page events
+### Wiki Page events
Triggered when a wiki page is created or edited.
@@ -737,9 +747,9 @@ X-Gitlab-Event: Wiki Page Hook
},
"wiki": {
"web_url": "http://example.com/root/awesome-project/wikis/home",
- "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
- "git_http_url": "http://example.com/root/awesome-project.wiki.git",
- "path_with_namespace": "root/awesome-project.wiki",
+ "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
+ "git_http_url": "http://example.com/root/awesome-project.wiki.git",
+ "path_with_namespace": "root/awesome-project.wiki",
"default_branch": "master"
},
"object_attributes": {
@@ -754,7 +764,7 @@ X-Gitlab-Event: Wiki Page Hook
}
```
-## Pipeline events
+### Pipeline events
Triggered on status change of Pipeline.
@@ -922,8 +932,7 @@ X-Gitlab-Event: Pipeline Hook
}
```
-
-## Build events
+### Build events
Triggered on status change of a Build.
@@ -935,7 +944,7 @@ X-Gitlab-Event: Build Hook
**Request Body**:
-```
+```json
{
"object_kind": "build",
"ref": "gitlab-script-trigger",
@@ -980,12 +989,13 @@ X-Gitlab-Event: Build Hook
}
```
-#### Example webhook receiver
+## Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
-a simple echo script running in a console session.
+a simple echo script running in a console session. For the following script to
+work you need to have Ruby installed.
-Save the following file as `print_http_body.rb`.
+Save the following file as `print_http_body.rb`:
```ruby
require 'webrick'
@@ -1005,7 +1015,8 @@ Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
8000`. Then add your server as a webhook receiver in GitLab as
`http://my.host:8000/`.
-When you press 'Test Hook' in GitLab, you should see something like this in the console.
+When you press 'Test Hook' in GitLab, you should see something like this in the
+console:
```
{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index 63ce3ccb536..a04228de03b 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -49,7 +49,6 @@ Feature: Project Wiki
Scenario: View all pages
Given I have an existing wiki page
And I browse to that Wiki page
- And I click on the "Pages" button
Then I should see the existing page in the pages list
Scenario: File exists in wiki repo
@@ -72,13 +71,11 @@ Feature: Project Wiki
@javascript
Scenario: New Wiki page that has a path
Given I create a New page with paths
- And I click on the "Pages" button
Then I should see non-escaped link in the pages list
@javascript
Scenario: Edit Wiki page that has a path
Given I create a New page with paths
- And I click on the "Pages" button
And I edit the Wiki page with a path
Then I should see a non-escaped path
And I should see the Editing page
@@ -88,7 +85,6 @@ Feature: Project Wiki
@javascript
Scenario: View the page history of a Wiki page that has a path
Given I create a New page with paths
- And I click on the "Pages" button
And I view the page history of a Wiki page that has a path
Then I should see a non-escaped path
And I should see the page history
@@ -96,7 +92,6 @@ Feature: Project Wiki
@javascript
Scenario: View an old page version of a Wiki page
Given I create a New page with paths
- And I click on the "Pages" button
And I edit the Wiki page with a path
Then I should see a non-escaped path
And I should see the Editing page
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 2134dae168a..dee6a8a5558 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -241,7 +241,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
page.within(:css, ".nav-text") do
expect(page).to have_content "Test"
- expect(page).to have_content "Edit Page"
+ expect(page).to have_content "Create Page"
end
end
@@ -258,7 +258,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api")
page.within(:css, ".nav-text") do
- expect(page).to have_content "Edit"
+ expect(page).to have_content "Create"
expect(page).to have_content "Api"
end
end
@@ -271,7 +271,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks")
page.within(:css, ".nav-text") do
- expect(page).to have_content "Edit"
+ expect(page).to have_content "Create"
expect(page).to have_content "Rake"
end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 07a955b1a14..4cb0a21fbb4 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -29,7 +29,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(page).to have_content "link test"
click_link "link test"
- expect(page).to have_content "Edit Page"
+ expect(page).to have_content "Create Page"
end
step 'I have an existing Wiki page' do
@@ -80,13 +80,9 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(page).to have_content "Page was successfully deleted"
end
- step 'I click on the "Pages" button' do
- click_on "Pages"
- end
-
step 'I should see the existing page in the pages list' do
expect(page).to have_content current_user.name
- expect(page).to have_content @page.title
+ expect(find('.wiki-pages')).to have_content @page.title.capitalize
end
step 'I have an existing Wiki page with images linked on page' do
@@ -125,7 +121,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I should see the new wiki page form' do
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Edit Page')
+ expect(page).to have_content('Create Page')
end
step 'I create a New page with paths' do
@@ -142,8 +138,8 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I edit the Wiki page with a path' do
- expect(page).to have_content('three')
- click_on 'three'
+ expect(find('.wiki-pages')).to have_content('Three')
+ click_on 'Three'
expect(find('.nav-text')).to have_content('Three')
click_on 'Edit'
end
@@ -157,7 +153,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I view the page history of a Wiki page that has a path' do
- click_on 'three'
+ click_on 'Three'
click_on 'Page History'
end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index 56b36f7c46c..a036d9b884f 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -2,7 +2,7 @@ module SharedMarkdown
include Spinach::DSL
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
- node = find("#{parent} h#{level} a##{id}")
+ node = find("#{parent} h#{level} a#user-content-#{id}")
expect(node[:href]).to eq "##{id}"
# Work around a weird Capybara behavior where calling `parent` on a node
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index cbafa952ef6..7f94ede7940 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -141,6 +141,10 @@ module API
unauthorized! unless current_user
end
+ def authenticate_non_get!
+ authenticate! unless %w[GET HEAD].include?(route.route_method)
+ end
+
def authenticate_by_gitlab_shell_token!
input = params['secret_token'].try(:chomp)
unless Devise.secure_compare(secret_token, input)
@@ -149,6 +153,7 @@ module API
end
def authenticated_as_admin!
+ authenticate!
forbidden! unless current_user.is_admin?
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 8975b1a751c..2929d2157dc 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,7 +3,7 @@ module API
class Projects < Grape::API
include PaginationParams
- before { authenticate! }
+ before { authenticate_non_get! }
helpers do
params :optional_params do
@@ -61,7 +61,7 @@ module API
end
end
- desc 'Get a projects list for authenticated user' do
+ desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails
end
params do
@@ -70,15 +70,15 @@ module API
use :filter_params
use :pagination
end
- get do
- projects = current_user.authorized_projects
+ get '/visible' do
+ projects = ProjectsFinder.new.execute(current_user)
projects = filter_projects(projects)
- entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+ entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
present paginate(projects), with: entity, user: current_user
end
- desc 'Get a list of visible projects for authenticated user' do
+ desc 'Get a projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
@@ -87,8 +87,10 @@ module API
use :filter_params
use :pagination
end
- get '/visible' do
- projects = ProjectsFinder.new.execute(current_user)
+ get do
+ authenticate!
+
+ projects = current_user.authorized_projects
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
@@ -103,6 +105,8 @@ module API
use :pagination
end
get '/owned' do
+ authenticate!
+
projects = current_user.owned_projects
projects = filter_projects(projects)
@@ -117,6 +121,8 @@ module API
use :pagination
end
get '/starred' do
+ authenticate!
+
projects = current_user.viewable_starred_projects
projects = filter_projects(projects)
@@ -132,6 +138,7 @@ module API
end
get '/all' do
authenticated_as_admin!
+
projects = Project.all
projects = filter_projects(projects)
@@ -213,7 +220,8 @@ module API
success Entities::ProjectWithAccess
end
get ":id" do
- present user_project, with: Entities::ProjectWithAccess, user: current_user,
+ entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
+ present user_project, with: entity, user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project)
end
@@ -433,7 +441,7 @@ module API
use :pagination
end
get ':id/users' do
- users = User.where(id: user_project.team.users.map(&:id))
+ users = user_project.team.users
users = users.search(params[:search]) if params[:search].present?
present paginate(users), with: Entities::UserBasic
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index a4eda6fdf76..8e7084f2543 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -35,9 +35,11 @@ module Banzai
headers[id] += 1
if header_content = node.children.first
+ # namespace detection will be automatically handled via javascript (see issue #22781)
+ namespace = "user-content-"
href = "#{id}#{uniq}"
push_toc(href, text)
- header_content.add_previous_sibling(anchor_tag(href))
+ header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href))
end
end
@@ -48,8 +50,8 @@ module Banzai
private
- def anchor_tag(href)
- %Q{<a id="#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>}
+ def anchor_tag(id, href)
+ %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>}
end
def push_toc(href, text)
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 4750675ae9d..0a8d05b5fe1 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def valid?
- repo.present?
+ sha.present? && ref.present?
end
private
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index cffed987f6b..d3489324a9c 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -8,26 +8,32 @@ describe HelpController do
end
describe 'GET #index' do
- context 'when url prefixed without /help/' do
- it 'has correct url prefix' do
- stub_readme("[API](api/README.md)")
+ context 'with absolute url' do
+ it 'keeps the URL absolute' do
+ stub_readme("[API](/api/README.md)")
+
get :index
- expect(assigns[:help_index]).to eq '[API](/help/api/README.md)'
+
+ expect(assigns[:help_index]).to eq '[API](/api/README.md)'
end
end
- context 'when url prefixed with help' do
- it 'will be an absolute path' do
- stub_readme("[API](helpful_hints/README.md)")
+ context 'with relative url' do
+ it 'prefixes it with /help/' do
+ stub_readme("[API](api/README.md)")
+
get :index
- expect(assigns[:help_index]).to eq '[API](/help/helpful_hints/README.md)'
+
+ expect(assigns[:help_index]).to eq '[API](/help/api/README.md)'
end
end
context 'when url is an external link' do
- it 'will not be changed' do
+ it 'does not change it' do
stub_readme("[external](https://some.external.link)")
+
get :index
+
expect(assigns[:help_index]).to eq '[external](https://some.external.link)'
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index f160052a844..c16aafa1470 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -304,8 +304,8 @@ describe 'Issue Boards', feature: true, js: true do
page.within('.subscription') do
click_button 'Subscribe'
-
- expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
+ wait_for_ajax
+ expect(page).to have_content("Unsubscribe")
end
end
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 73d03837144..4319d6db0d2 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -12,9 +12,9 @@ describe 'Help Pages', feature: true do
end
describe 'Get the main help page' do
- shared_examples_for 'help page' do
+ shared_examples_for 'help page' do |prefix: ''|
it 'prefixes links correctly' do
- expect(page).to have_selector('div.documentation-index > ul a[href="/help/api/README.md"]')
+ expect(page).to have_selector(%(div.documentation-index > ul a[href="#{prefix}/help/api/README.md"]))
end
end
@@ -33,5 +33,14 @@ describe 'Help Pages', feature: true do
it_behaves_like 'help page'
end
+
+ context 'with a relative installation' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab')
+ visit help_path
+ end
+
+ it_behaves_like 'help page', prefix: '/gitlab'
+ end
end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
new file mode 100644
index 00000000000..3350a3aeefc
--- /dev/null
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe "Pipelines", feature: true, js: true do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ describe 'GET /:project/pipelines/:id' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
+ @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
+ @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ end
+
+ before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it 'shows the pipeline graph' do
+ expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_content('Build')
+ expect(page).to have_content('Test')
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Cancel running')
+ end
+
+ it 'shows Pipeline tab pane as active' do
+ expect(page).to have_css('#js-tab-pipeline.active')
+ end
+
+ context 'page tabs' do
+ it 'shows Pipeline and Builds tabs with link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Builds')
+ end
+
+ it 'shows counter in Builds tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
+ end
+
+ it 'shows Pipeline tab as active' do
+ expect(page).to have_css('.js-pipeline-tab-link.active')
+ end
+ end
+
+ context 'retrying builds' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before { click_on 'Retry failed' }
+
+ it { expect(page).not_to have_content('Retry failed') }
+ end
+ end
+
+ context 'canceling builds' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before { click_on 'Cancel running' }
+
+ it { expect(page).not_to have_content('Cancel running') }
+ end
+ end
+ end
+
+ describe 'GET /:project/pipelines/:id/builds' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
+ @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
+ @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ end
+
+ before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)}
+
+ it 'shows a list of builds' do
+ expect(page).to have_content('Test')
+ expect(page).to have_content(@success.id)
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content(@failed.id)
+ expect(page).to have_content(@running.id)
+ expect(page).to have_content(@external.id)
+ expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Cancel running')
+ expect(page).to have_link('Play')
+ end
+
+ it 'shows Builds tab pane as active' do
+ expect(page).to have_css('#js-tab-builds.active')
+ end
+
+ context 'page tabs' do
+ it 'shows Pipeline and Builds tabs with link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Builds')
+ end
+
+ it 'shows counter in Builds tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
+ end
+
+ it 'shows Builds tab as active' do
+ expect(page).to have_css('li.js-builds-tab-link.active')
+ end
+ end
+
+ context 'retrying builds' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before { click_on 'Retry failed' }
+
+ it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).to have_selector('.retried') }
+ end
+ end
+
+ context 'canceling builds' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before { click_on 'Cancel running' }
+
+ it { expect(page).not_to have_content('Cancel running') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+
+ context 'playing manual build' do
+ before do
+ within '.pipeline-holder' do
+ click_link('Play')
+ end
+ end
+
+ it { expect(@manual.reload).to be_pending }
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 10e5466fc85..f3731698a18 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -152,65 +152,6 @@ describe "Pipelines" do
end
end
- describe 'GET /:project/pipelines/:id' do
- let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
-
- before do
- @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
- @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
- @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
- @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
- end
-
- before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
-
- it 'shows a list of builds' do
- expect(page).to have_content('Test')
- expect(page).to have_content(@success.id)
- expect(page).to have_content('Deploy')
- expect(page).to have_content(@failed.id)
- expect(page).to have_content(@running.id)
- expect(page).to have_content(@external.id)
- expect(page).to have_content('Retry failed')
- expect(page).to have_content('Cancel running')
- expect(page).to have_link('Play')
- end
-
- context 'retrying builds' do
- it { expect(page).not_to have_content('retried') }
-
- context 'when retrying' do
- before { click_on 'Retry failed' }
-
- it { expect(page).not_to have_content('Retry failed') }
- it { expect(page).to have_selector('.retried') }
- end
- end
-
- context 'canceling builds' do
- it { expect(page).not_to have_selector('.ci-canceled') }
-
- context 'when canceling' do
- before { click_on 'Cancel running' }
-
- it { expect(page).not_to have_content('Cancel running') }
- it { expect(page).to have_selector('.ci-canceled') }
- end
- end
-
- context 'playing manual build' do
- before do
- within '.pipeline-holder' do
- click_link('Play')
- end
- end
-
- it { expect(@manual.reload).to be_pending }
- end
- end
-
describe 'POST /:project/pipelines' do
let(:project) { create(:project) }
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 7afd83b7250..fff8b9f3447 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -20,7 +20,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -41,7 +41,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Foo')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
@@ -55,7 +55,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Spaces in the name')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
@@ -69,7 +69,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Hyphens in the name')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -85,7 +85,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -105,7 +105,7 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
click_button 'Create page'
expect(page).to have_content('Foo')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index ef82d2375dd..f842d14fa96 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -22,7 +22,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
click_button 'Save changes'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
@@ -37,7 +37,7 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
click_button 'Save changes'
expect(page).to have_content('Home')
- expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!')
end
end
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
new file mode 100644
index 00000000000..9aa3c50611d
--- /dev/null
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
@@ -0,0 +1,55 @@
+//= require lib/utils/bootstrap_linked_tabs
+
+(() => {
+ describe('Linked Tabs', () => {
+ fixture.preload('linked_tabs');
+
+ beforeEach(() => {
+ fixture.load('linked_tabs');
+ });
+
+ describe('when is initialized', () => {
+ it('should activate the tab correspondent to the given action', () => {
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'tab1',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+
+ it('should active the default tab action when the action is show', () => {
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+ });
+
+ describe('on click', () => {
+ it('should change the url according to the clicked tab', () => {
+ const historySpy = spyOn(history, 'replaceState').and.callFake(() => {});
+
+ const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a');
+ const newState = secondTab.getAttribute('href') + linkedTabs.currentLocation.search + linkedTabs.currentLocation.hash;
+
+ secondTab.click();
+
+ expect(historySpy).toHaveBeenCalledWith({
+ turbolinks: true,
+ url: newState,
+ }, document.title, newState);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml
deleted file mode 100644
index cc1f8f15c21..00000000000
--- a/spec/javascripts/fixtures/comments.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-.flash-container.timeline-content
-.timeline-icon.hidden-xs.hidden-sm
- %a.author_link
- %img
-.timeline-content.timeline-content-form
- %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form
- .md-area
- .md-header
- .md-write-holder
- .zen-backdrop.div-dropzone-wrapper
- .div-dropzone-wrapper
- .div-dropzone.dz-clickable
- %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area
- .note-form-actions.clearfix
- %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' }
- %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen
- Reopen issue
- %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close
- Close issue
- %a.btn.btn-cancel.js-note-discard
- Discard draft \ No newline at end of file
diff --git a/spec/javascripts/fixtures/issuable.html.haml b/spec/javascripts/fixtures/issuable.html.haml
deleted file mode 100644
index 42ab4aa68b1..00000000000
--- a/spec/javascripts/fixtures/issuable.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%form.js-main-target-form
- %textarea#note_note
diff --git a/spec/javascripts/fixtures/issue_note.html.haml b/spec/javascripts/fixtures/issue_note.html.haml
deleted file mode 100644
index 0aecc7334fd..00000000000
--- a/spec/javascripts/fixtures/issue_note.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-%ul
- %li.note
- .js-task-list-container
- .note-text
- %ul.task-list
- %li.task-list-item
- %input.task-list-item-checkbox{type: 'checkbox'}
- Task List Item
- .note-edit-form
- %form
- %textarea.js-task-list-field
- \- [ ] Task List Item
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index c10784fe5ae..06f708f9e15 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -26,8 +26,13 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
end
it 'issues/issue-with-task-list.html.raw' do |example|
+ issue = create(:issue, project: project, description: '- [ ] Task List Item')
+ render_issue(example.description, issue)
+ end
+
+ it 'issues/issue_with_comment.html.raw' do |example|
issue = create(:issue, project: project)
- issue.update(description: '- [ ] Task List Item')
+ create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save
render_issue(example.description, issue)
end
diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml
new file mode 100644
index 00000000000..93c0cf97ff0
--- /dev/null
+++ b/spec/javascripts/fixtures/linked_tabs.html.haml
@@ -0,0 +1,13 @@
+%ul.nav.nav-tabs.linked-tabs
+ %li
+ %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
+ Tab 1
+ %li
+ %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
+ Tab 2
+
+.tab-content
+ #tab1.tab-pane
+ Tab 1 Content
+ #tab2.tab-pane
+ Tab 2 Content
diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml
deleted file mode 100644
index d259b58f235..00000000000
--- a/spec/javascripts/fixtures/right_sidebar.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-%div
- %div.page-gutter.page-with-sidebar
-
- %aside.right-sidebar
- %div.block.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle
- %i.fa.fa-angle-double-left
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: "1", issuable_type: "issue", url: "/todos" }}
- %span.js-issuable-todo-text
- Add todo
- %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden
-
- %form.issuable-context-form
- %div.block.labels
- %div.sidebar-collapsed-icon
- %i.fa.fa-tags
- %span 1
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 51f2ae8bcbd..2db182d702b 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -6,17 +6,21 @@
(function() {
window.gon || (window.gon = {});
-
- window.disableButtonIfEmptyField = function() {
- return null;
- };
+ window.gl = window.gl || {};
+ gl.utils = gl.utils || {};
describe('Notes', function() {
- describe('task lists', function() {
- fixture.preload('issue_note.html');
+ var commentsTemplate = 'issues/issue_with_comment.raw';
+ fixture.preload(commentsTemplate);
+ beforeEach(function () {
+ fixture.load(commentsTemplate);
+ gl.utils.disableButtonIfEmptyField = _.noop;
+ window.project_uploads_path = 'http://test.host/uploads';
+ });
+
+ describe('task lists', function() {
beforeEach(function() {
- fixture.load('issue_note.html');
$('form').on('submit', function(e) {
e.preventDefault();
});
@@ -41,12 +45,9 @@
});
describe('comments', function() {
- var commentsTemplate = 'comments.html';
var textarea = '.js-note-text';
- fixture.preload(commentsTemplate);
beforeEach(function() {
- fixture.load(commentsTemplate);
this.notes = new Notes();
this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 83ebbd63f3a..0a9bc546144 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -34,9 +34,10 @@
};
describe('RightSidebar', function() {
- fixture.preload('right_sidebar.html');
+ var fixtureName = 'issues/open-issue.html.raw';
+ fixture.preload(fixtureName);
beforeEach(function() {
- fixture.load('right_sidebar.html');
+ fixture.load(fixtureName);
this.sidebar = new Sidebar;
$aside = $('.right-sidebar');
$page = $('.page-with-sidebar');
@@ -44,15 +45,12 @@
$toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
});
- it('should expand the sidebar when arrow is clicked', function() {
+ it('should expand/collapse the sidebar when arrow is clicked', function() {
+ assertSidebarState('expanded');
$toggle.click();
- return assertSidebarState('expanded');
- });
- it('should collapse the sidebar when arrow is clicked', function() {
+ assertSidebarState('collapsed');
$toggle.click();
assertSidebarState('expanded');
- $toggle.click();
- return assertSidebarState('collapsed');
});
it('should float over the page and when sidebar icons clicked', function() {
$labelsIcon.click();
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 7d36d79b687..e37816b0a8c 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -4,9 +4,11 @@
(function() {
describe('ShortcutsIssuable', function() {
- fixture.preload('issuable.html');
+ var fixtureName = 'issues/open-issue.html.raw';
+ fixture.preload(fixtureName);
beforeEach(function() {
- fixture.load('issuable.html');
+ fixture.load(fixtureName);
+ document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
return this.shortcut = new ShortcutsIssuable();
});
return describe('#replyWithSelectedText', function() {
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 356dd01a03a..70b31f3a880 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -22,7 +22,7 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do
html = header(i, "Header #{i}")
doc = filter(html)
- expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
+ expect(doc.css("h#{i} a").first.attr('id')).to eq "user-content-header-#{i}"
end
end
@@ -32,7 +32,12 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do
expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
end
- it 'links to the id' do
+ it 'has a namespaced id' do
+ doc = filter(header(1, 'Header'))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-header'
+ end
+
+ it 'links to the non-namespaced id' do
doc = filter(header(1, 'Header'))
expect(doc.css('h1 a').first.attr('href')).to eq '#header'
end
@@ -40,29 +45,29 @@ describe Banzai::Filter::TableOfContentsFilter, lib: true do
describe 'generated IDs' do
it 'translates spaces to dashes' do
doc = filter(header(1, 'This header has spaces in it'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-has-spaces-in-it'
end
it 'squeezes multiple spaces and dashes' do
doc = filter(header(1, 'This---header is poorly-formatted'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-poorly-formatted'
end
it 'removes punctuation' do
doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation'
end
it 'appends a unique number to duplicates' do
doc = filter(header(1, 'One') + header(2, 'One'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'one'
- expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#one'
+ expect(doc.css('h2 a').first.attr('href')).to eq '#one-1'
end
it 'supports Unicode' do
doc = filter(header(1, '한글'))
- expect(doc.css('h1 a').first.attr('id')).to eq '한글'
+ expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
end
end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index e5300dbba1e..462caa5b5fe 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -49,14 +49,20 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
end
describe '#valid?' do
- it 'returns true when raw repo is present' do
+ it 'returns true when raw sha and ref are present' do
branch = described_class.new(project, double(raw))
expect(branch.valid?).to eq true
end
- it 'returns false when raw repo is blank' do
- branch = described_class.new(project, double(raw.merge(repo: nil)))
+ it 'returns false when raw sha is blank' do
+ branch = described_class.new(project, double(raw.merge(sha: nil)))
+
+ expect(branch.valid?).to eq false
+ end
+
+ it 'returns false when raw ref is blank' do
+ branch = described_class.new(project, double(raw.merge(ref: nil)))
expect(branch.valid?).to eq false
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ef07f2275b1..d4970e38f7c 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -730,8 +730,8 @@ describe Ci::Build, models: true do
pipeline2 = create(:ci_pipeline, project: project)
@build2 = create(:ci_build, pipeline: pipeline2)
- commits = [double(id: pipeline.sha), double(id: pipeline2.sha)]
- allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(@merge_request).to receive(:commits_sha).
+ and_return([pipeline.sha, pipeline2.sha])
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e5007424041..eb876d105da 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -77,24 +77,13 @@ describe MergeRequestDiff, models: true do
end
describe '#commits_sha' do
- shared_examples 'returning all commits SHA' do
- it 'returns all commits SHA' do
- commits_sha = subject.commits_sha
+ it 'returns all commits SHA using serialized commits' do
+ subject.st_commits = [
+ { id: 'sha1' },
+ { id: 'sha2' }
+ ]
- expect(commits_sha).to eq(subject.commits.map(&:sha))
- end
- end
-
- context 'when commits were loaded' do
- before do
- subject.commits
- end
-
- it_behaves_like 'returning all commits SHA'
- end
-
- context 'when commits were not loaded' do
- it_behaves_like 'returning all commits SHA'
+ expect(subject.commits_sha).to eq(['sha1', 'sha2'])
end
end
@@ -113,4 +102,15 @@ describe MergeRequestDiff, models: true do
expect(diffs.size).to eq(3)
end
end
+
+ describe '#commits_count' do
+ it 'returns number of commits using serialized commits' do
+ subject.st_commits = [
+ { id: 'sha1' },
+ { id: 'sha2' }
+ ]
+
+ expect(subject.commits_count).to eq 2
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 26034cb1c7b..ec22ef93465 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -557,16 +557,13 @@ describe MergeRequest, models: true do
end
describe '#commits_sha' do
- let(:commit0) { double('commit0', sha: 'sha1') }
- let(:commit1) { double('commit1', sha: 'sha2') }
- let(:commit2) { double('commit2', sha: 'sha3') }
-
before do
- allow(subject.merge_request_diff).to receive(:commits).and_return([commit0, commit1, commit2])
+ allow(subject.merge_request_diff).to receive(:commits_sha).
+ and_return(['sha1'])
end
- it 'returns sha of commits' do
- expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
+ it 'delegates to merge request diff' do
+ expect(subject.commits_sha).to eq ['sha1']
end
end
@@ -1440,4 +1437,26 @@ describe MergeRequest, models: true do
end
end
end
+
+ describe '#has_commits?' do
+ before do
+ allow(subject.merge_request_diff).to receive(:commits_count).
+ and_return(2)
+ end
+
+ it 'returns true when merge request diff has commits' do
+ expect(subject.has_commits?).to be_truthy
+ end
+ end
+
+ describe '#has_no_commits?' do
+ before do
+ allow(subject.merge_request_diff).to receive(:commits_count).
+ and_return(0)
+ end
+
+ it 'returns true when merge request diff has 0 commits' do
+ expect(subject.has_no_commits?).to be_truthy
+ end
+ end
end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 01bb9e955e0..36517ad0f8c 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -47,7 +47,7 @@ describe API::Helpers, api: true do
end
def error!(message, status)
- raise Exception
+ raise Exception.new("#{status} - #{message}")
end
describe ".current_user" do
@@ -290,4 +290,56 @@ describe API::Helpers, api: true do
handle_api_exception(exception)
end
end
+
+ describe '.authenticate_non_get!' do
+ %w[HEAD GET].each do |method_name|
+ context "method is #{method_name}" do
+ before do
+ expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name))
+ end
+
+ it 'does not raise an error' do
+ expect_any_instance_of(self.class).not_to receive(:authenticate!)
+
+ expect { authenticate_non_get! }.not_to raise_error
+ end
+ end
+ end
+
+ %w[POST PUT PATCH DELETE].each do |method_name|
+ context "method is #{method_name}" do
+ before do
+ expect_any_instance_of(self.class).to receive(:route).and_return(double(route_method: method_name))
+ end
+
+ it 'calls authenticate!' do
+ expect_any_instance_of(self.class).to receive(:authenticate!)
+
+ authenticate_non_get!
+ end
+ end
+ end
+ end
+
+ describe '.authenticate!' do
+ context 'current_user is nil' do
+ before do
+ expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
+ end
+
+ it 'returns a 401 response' do
+ expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
+ end
+ end
+
+ context 'current_user is present' do
+ before do
+ expect_any_instance_of(self.class).to receive(:current_user).and_return(true)
+ end
+
+ it 'does not raise an error' do
+ expect { authenticate! }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 482e81b29a6..5b3427e66e8 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -200,32 +200,43 @@ describe API::API, api: true do
end
describe 'GET /projects/visible' do
- let(:public_project) { create(:project, :public) }
+ shared_examples_for 'visible projects response' do
+ it 'returns the visible projects' do
+ get api('/projects/visible', current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
+ end
+ end
+ let!(:public_project) { create(:project, :public) }
before do
- public_project
project
project2
project3
project4
end
- it 'returns the projects viewable by the user' do
- get api('/projects/visible', user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.map { |project| project['id'] }).
- to contain_exactly(public_project.id, project.id, project2.id, project3.id)
+ context 'when unauthenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { nil }
+ let(:projects) { [public_project] }
+ end
end
- it 'shows only public projects when the user only has access to those' do
- get api('/projects/visible', user2)
+ context 'when authenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+ end
+ end
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.map { |project| project['id'] }).
- to contain_exactly(public_project.id)
+ context 'when authenticated as a different user' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user2 }
+ let(:projects) { [public_project] }
+ end
end
end
@@ -528,135 +539,150 @@ describe API::API, api: true do
end
describe 'GET /projects/:id' do
- before { project }
- before { project_member }
-
- it 'returns a project by id' do
- group = create(:group)
- link = create(:project_group_link, project: project, group: group)
+ context 'when unauthenticated' do
+ it 'returns the public projects' do
+ public_project = create(:project, :public)
- get api("/projects/#{project.id}", user)
+ get api("/projects/#{public_project.id}")
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(project.id)
- expect(json_response['description']).to eq(project.description)
- expect(json_response['default_branch']).to eq(project.default_branch)
- expect(json_response['tag_list']).to be_an Array
- expect(json_response['public']).to be_falsey
- expect(json_response['archived']).to be_falsey
- expect(json_response['visibility_level']).to be_present
- expect(json_response['ssh_url_to_repo']).to be_present
- expect(json_response['http_url_to_repo']).to be_present
- expect(json_response['web_url']).to be_present
- expect(json_response['owner']).to be_a Hash
- expect(json_response['owner']).to be_a Hash
- expect(json_response['name']).to eq(project.name)
- expect(json_response['path']).to be_present
- expect(json_response['issues_enabled']).to be_present
- expect(json_response['merge_requests_enabled']).to be_present
- expect(json_response['wiki_enabled']).to be_present
- expect(json_response['builds_enabled']).to be_present
- expect(json_response['snippets_enabled']).to be_present
- expect(json_response['container_registry_enabled']).to be_present
- expect(json_response['created_at']).to be_present
- expect(json_response['last_activity_at']).to be_present
- expect(json_response['shared_runners_enabled']).to be_present
- expect(json_response['creator_id']).to be_present
- expect(json_response['namespace']).to be_present
- expect(json_response['avatar_url']).to be_nil
- expect(json_response['star_count']).to be_present
- expect(json_response['forks_count']).to be_present
- expect(json_response['public_builds']).to be_present
- expect(json_response['shared_with_groups']).to be_an Array
- expect(json_response['shared_with_groups'].length).to eq(1)
- expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
- expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
- expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
- expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
- expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
- end
-
- it 'returns a project by path name' do
- get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(project.name)
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(public_project.id)
+ expect(json_response['description']).to eq(public_project.description)
+ expect(json_response.keys).not_to include('permissions')
+ end
end
- it 'returns a 404 error if not found' do
- get api('/projects/42', user)
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
+ context 'when authenticated' do
+ before do
+ project
+ project_member
+ end
- it 'returns a 404 error if user is not a member' do
- other_user = create(:user)
- get api("/projects/#{project.id}", other_user)
- expect(response).to have_http_status(404)
- end
+ it 'returns a project by id' do
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
- it 'handles users with dots' do
- dot_user = create(:user, username: 'dot.user')
- project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+ get api("/projects/#{project.id}", user)
- get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(project.name)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['tag_list']).to be_an Array
+ expect(json_response['public']).to be_falsey
+ expect(json_response['archived']).to be_falsey
+ expect(json_response['visibility_level']).to be_present
+ expect(json_response['ssh_url_to_repo']).to be_present
+ expect(json_response['http_url_to_repo']).to be_present
+ expect(json_response['web_url']).to be_present
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to be_present
+ expect(json_response['issues_enabled']).to be_present
+ expect(json_response['merge_requests_enabled']).to be_present
+ expect(json_response['wiki_enabled']).to be_present
+ expect(json_response['builds_enabled']).to be_present
+ expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ expect(json_response['avatar_url']).to be_nil
+ expect(json_response['star_count']).to be_present
+ expect(json_response['forks_count']).to be_present
+ expect(json_response['public_builds']).to be_present
+ expect(json_response['shared_with_groups']).to be_an Array
+ expect(json_response['shared_with_groups'].length).to eq(1)
+ expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
+ expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
+ expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ end
+
+ it 'returns a project by path name' do
+ get api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
- describe 'permissions' do
- context 'all projects' do
- before { project.team << [user, :master] }
+ it 'returns a 404 error if not found' do
+ get api('/projects/42', user)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
- it 'contains permission information' do
- get api("/projects", user)
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+ get api("/projects/#{project.id}", other_user)
+ expect(response).to have_http_status(404)
+ end
- expect(response).to have_http_status(200)
- expect(json_response.first['permissions']['project_access']['access_level']).
- to eq(Gitlab::Access::MASTER)
- expect(json_response.first['permissions']['group_access']).to be_nil
- end
+ it 'handles users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
end
- context 'personal project' do
- it 'sets project access and returns 200' do
- project.team << [user, :master]
- get api("/projects/#{project.id}", user)
+ describe 'permissions' do
+ context 'all projects' do
+ before { project.team << [user, :master] }
- expect(response).to have_http_status(200)
- expect(json_response['permissions']['project_access']['access_level']).
+ it 'contains permission information' do
+ get api("/projects", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['permissions']['project_access']['access_level']).
to eq(Gitlab::Access::MASTER)
- expect(json_response['permissions']['group_access']).to be_nil
+ expect(json_response.first['permissions']['group_access']).to be_nil
+ end
end
- end
- context 'group project' do
- let(:project2) { create(:project, group: create(:group)) }
+ context 'personal project' do
+ it 'sets project access and returns 200' do
+ project.team << [user, :master]
+ get api("/projects/#{project.id}", user)
- before { project2.group.add_owner(user) }
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response['permissions']['group_access']).to be_nil
+ end
+ end
- it 'sets the owner and return 200' do
- get api("/projects/#{project2.id}", user)
+ context 'group project' do
+ let(:project2) { create(:project, group: create(:group)) }
- expect(response).to have_http_status(200)
- expect(json_response['permissions']['project_access']).to be_nil
- expect(json_response['permissions']['group_access']['access_level']).
+ before { project2.group.add_owner(user) }
+
+ it 'sets the owner and return 200' do
+ get api("/projects/#{project2.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']).to be_nil
+ expect(json_response['permissions']['group_access']['access_level']).
to eq(Gitlab::Access::OWNER)
+ end
end
end
end
end
describe 'GET /projects/:id/events' do
- before { project_member2 }
-
- context 'valid request' do
- before do
+ shared_examples_for 'project events response' do
+ it 'returns the project events' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
note = create(:note_on_issue, note: 'What an awesome day!', project: project)
EventCreateService.new.leave_note(note, note.author)
- end
- it 'returns all events' do
- get api("/projects/#{project.id}/events", user)
+ get api("/projects/#{project.id}/events", current_user)
expect(response).to have_http_status(200)
@@ -669,24 +695,90 @@ describe API::API, api: true do
expect(last_event['action_name']).to eq('joined')
expect(last_event['project_id'].to_i).to eq(project.id)
- expect(last_event['author_username']).to eq(user3.username)
- expect(last_event['author']['name']).to eq(user3.name)
+ expect(last_event['author_username']).to eq(member.username)
+ expect(last_event['author']['name']).to eq(member.name)
end
end
- it 'returns a 404 error if not found' do
- get api('/projects/42/events', user)
+ context 'when unauthenticated' do
+ it_behaves_like 'project events response' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project events response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get api('/projects/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get api("/projects/#{project.id}/events", other_user)
+
+ expect(response).to have_http_status(404)
+ end
end
+ end
- it 'returns a 404 error if user is not a member' do
- other_user = create(:user)
+ describe 'GET /projects/:id/users' do
+ shared_examples_for 'project users response' do
+ it 'returns the project users' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
- get api("/projects/#{project.id}/events", other_user)
+ get api("/projects/#{project.id}/users", current_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+
+ first_user = json_response.first
+
+ expect(first_user['username']).to eq(member.username)
+ expect(first_user['name']).to eq(member.name)
+ expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project users response' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project users response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get api('/projects/42/users', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get api("/projects/#{project.id}/users", other_user)
+
+ expect(response).to have_http_status(404)
+ end
end
end
@@ -950,35 +1042,37 @@ describe API::API, api: true do
let!(:public) { create(:empty_project, :public, name: "public #{query}") }
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
+ shared_examples_for 'project search response' do |args = {}|
+ it 'returns project search responses' do
+ get api("/projects/search/#{query}", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(args[:results])
+ json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) }
+ end
+ end
+
context 'when unauthenticated' do
- it 'returns authentication error' do
- get api("/projects/search/#{query}")
- expect(response).to have_http_status(401)
+ it_behaves_like 'project search response', results: 1 do
+ let(:current_user) { nil }
end
end
context 'when authenticated' do
- it 'returns an array of projects' do
- get api("/projects/search/#{query}", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(6)
- json_response.each {|project| expect(project['name']).to match(/.*query.*/)}
+ it_behaves_like 'project search response', results: 6 do
+ let(:current_user) { user }
end
end
context 'when authenticated as a different user' do
- it 'returns matching public projects' do
- get api("/projects/search/#{query}", user2)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(2)
- json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)}
+ it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do
+ let(:current_user) { user2 }
end
end
end
- describe 'PUT /projects/:id̈́' do
+ describe 'PUT /projects/:id' do
before { project }
before { user }
before { user3 }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 7dcd03496bb..90771825f5c 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -7,15 +7,21 @@ describe Projects::DestroyService, services: true do
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
+ shared_examples 'deleting the project' do
+ it 'deletes the project' do
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_falsey
+ expect(Dir.exist?(remove_path)).to be_falsey
+ end
+ end
+
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed repository will be removed
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
- it { expect(Project.all).not_to include(project) }
- it { expect(Dir.exist?(path)).to be_falsey }
- it { expect(Dir.exist?(remove_path)).to be_falsey }
+ it_behaves_like 'deleting the project'
end
context 'Sidekiq fake' do
@@ -38,11 +44,21 @@ describe Projects::DestroyService, services: true do
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
- it 'deletes the project' do
- expect(Project.all).not_to include(project)
- expect(Dir.exist?(path)).to be_falsey
- expect(Dir.exist?(remove_path)).to be_falsey
+ it_behaves_like 'deleting the project'
+ end
+
+ context 'delete with pipeline' do # which has optimistic locking
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+
+ before do
+ expect(project).to receive(:destroy!).and_call_original
+
+ perform_enqueued_jobs do
+ destroy_project(project, user, {})
+ end
end
+
+ it_behaves_like 'deleting the project'
end
context 'container registry' do
diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb
index 02605d6b70e..92cf3de5448 100644
--- a/spec/support/matchers/have_issuable_counts.rb
+++ b/spec/support/matchers/have_issuable_counts.rb
@@ -1,9 +1,9 @@
RSpec::Matchers.define :have_issuable_counts do |opts|
- match do |actual|
- expected_counts = opts.map do |state, count|
- "#{state.to_s.humanize} #{count}"
- end
+ expected_counts = opts.map do |state, count|
+ "#{state.to_s.humanize} #{count}"
+ end
+ match do |actual|
actual.within '.issues-state-filters' do
expected_counts.each do |expected_count|
expect(actual).to have_content(expected_count)
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 8c98b1f988c..97b8b342eb2 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -38,9 +38,9 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('h1 a#gitlab-markdown')
- expect(actual).to have_selector('h2 a#markdown')
- expect(actual).to have_selector('h3 a#autolinkfilter')
+ expect(actual).to have_selector('h1 a#user-content-gitlab-markdown')
+ expect(actual).to have_selector('h2 a#user-content-markdown')
+ expect(actual).to have_selector('h3 a#user-content-autolinkfilter')
end
end