summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Knox <psimyn@gmail.com>2018-01-15 14:59:11 +1100
committerSimon Knox <psimyn@gmail.com>2018-01-15 14:59:11 +1100
commit2a341eb03fc7d2ece4a9fd95ec63995efa30b219 (patch)
treeb2cf7d6a6eb13c56bc59819cd7927196bbc6d93c
parent564cdddb5f4c8a690a937b61dba2e0427c3eb565 (diff)
parent74f2f9b30fb1972a26481072486b358eb943309f (diff)
downloadgitlab-ce-2a341eb03fc7d2ece4a9fd95ec63995efa30b219.tar.gz
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into dispatcher-projects-c
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Gemfile.lock7
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue3
-rw-r--r--app/assets/javascripts/commit_merge_requests.js73
-rw-r--r--app/assets/javascripts/create_item_dropdown.js (renamed from app/assets/javascripts/protected_tags/protected_tag_dropdown.js)57
-rw-r--r--app/assets/javascripts/dispatcher.js66
-rw-r--r--app/assets/javascripts/init_labels.js18
-rw-r--r--app/assets/javascripts/jobs/components/header.vue6
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue1
-rw-r--r--app/assets/javascripts/pages/projects/blame/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js33
-rw-r--r--app/assets/javascripts/pages/projects/labels/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/labels/new/index.js3
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js7
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js12
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js90
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js11
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue6
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss10
-rw-r--r--app/assets/stylesheets/framework/header.scss5
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss11
-rw-r--r--app/controllers/projects/commit_controller.rb14
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/models/commit.rb9
-rw-r--r--app/models/concerns/resolvable_discussion.rb25
-rw-r--r--app/models/merge_request.rb15
-rw-r--r--app/models/merge_request_diff.rb3
-rw-r--r--app/models/project.rb11
-rw-r--r--app/models/repository.rb9
-rw-r--r--app/models/route.rb2
-rw-r--r--app/views/admin/dashboard/index.html.haml56
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml6
-rw-r--r--app/views/projects/jobs/_table.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_dropdown.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_dropdown.html.haml2
-rw-r--r--changelogs/unreleased/40492-update-admin-dashboard-content-order.yml5
-rw-r--r--changelogs/unreleased/41731-predicate-memoization.yml5
-rw-r--r--changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml5
-rw-r--r--changelogs/unreleased/41807-15665-consistently-502s-because-it-fetches-every-commit.yml6
-rw-r--r--changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml5
-rw-r--r--changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml5
-rw-r--r--changelogs/unreleased/disable-pages-on-jobs.yml6
-rw-r--r--changelogs/unreleased/display-mr-in-commit-page.yml5
-rw-r--r--changelogs/unreleased/fix_gitlab-ce-41891.yml5
-rw-r--r--changelogs/unreleased/mk-fix-permanent-redirect-validation.yml5
-rw-r--r--config/routes/project.rb1
-rw-r--r--db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb17
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/articles/index.md19
-rw-r--r--doc/articles/runner_autoscale_aws/index.md411
-rw-r--r--doc/ci/README.md151
-rw-r--r--doc/ci/examples/README.md98
-rw-r--r--features/project/issues/issues.feature2
-rw-r--r--features/steps/project/issues/issues.rb8
-rw-r--r--lib/banzai/filter/relative_link_filter.rb15
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb10
-rw-r--r--lib/gitlab/bare_repository_import/repository.rb10
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.rb6
-rw-r--r--lib/gitlab/ci/stage/seed.rb6
-rw-r--r--lib/gitlab/github_import/client.rb6
-rw-r--r--lib/gitlab/user_access.rb10
-rw-r--r--qa/README.md11
-rw-r--r--rubocop/cop/gitlab/predicate_memoization.rb39
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--spec/factories/redirect_routes.rb15
-rw-r--r--spec/features/boards/sidebar_spec.rb10
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb20
-rw-r--r--spec/javascripts/commit_merge_requests_spec.js60
-rw-r--r--spec/javascripts/jobs/header_spec.js34
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js10
-rw-r--r--spec/javascripts/notes/components/note_form_spec.js9
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js12
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb48
-rw-r--r--spec/models/commit_range_spec.rb6
-rw-r--r--spec/models/commit_spec.rb13
-rw-r--r--spec/models/merge_request_diff_spec.rb22
-rw-r--r--spec/models/merge_request_spec.rb110
-rw-r--r--spec/models/route_spec.rb60
-rw-r--r--spec/rubocop/cop/gitlab/predicate_memoization_spec.rb100
92 files changed, 1180 insertions, 919 deletions
diff --git a/.gitignore b/.gitignore
index 4933575332b..2004c2a09b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.swp
*.mo
*.edit.po
+*.rej
.DS_Store
.bundle
.chef
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f038ce72aeb..80ba8e5c1a1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -61,6 +61,9 @@ stages:
.use-pg: &use-pg
services:
+ # As of Jan 2018, we don't have a strong reason to upgrade to 9.6 for CI yet,
+ # so using the least common denominator ensures backwards compatibility
+ # (as many users are still using 9.2).
- postgres:9.2
- redis:alpine
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 057e2d6e0dc..b366ae6f069 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -171,7 +171,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
-~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
+~Geo, ~Gitaly, ~Platform, ~Monitoring, ~Release, and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
diff --git a/Gemfile.lock b/Gemfile.lock
index cb6b0ebb3bc..8e31ac1f993 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -340,6 +340,8 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.4.1.1)
+ googleapis-common-protos-types (1.0.1)
+ google-protobuf (~> 3.0)
googleauth (0.5.3)
faraday (~> 0.12)
jwt (~> 1.4)
@@ -366,9 +368,10 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.4.5)
+ grpc (1.8.3)
google-protobuf (~> 3.1)
- googleauth (~> 0.5.1)
+ googleapis-common-protos-types (~> 1.0.0)
+ googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
tilt
haml_lint (0.26.0)
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 57457ebd0a3..ff2e0768a87 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -81,8 +81,7 @@
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
- ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
- </a>`,
+ ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
);
diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js
new file mode 100644
index 00000000000..f76c9b7e690
--- /dev/null
+++ b/app/assets/javascripts/commit_merge_requests.js
@@ -0,0 +1,73 @@
+/* global Flash */
+
+import axios from './lib/utils/axios_utils';
+import { n__, s__ } from './locale';
+
+export function getHeaderText(childElementCount, mergeRequestCount) {
+ if (childElementCount === 0) {
+ return `${mergeRequestCount} ${n__('merge request', 'merge requests', mergeRequestCount)}`;
+ }
+ return ',';
+}
+
+export function createHeader(childElementCount, mergeRequestCount) {
+ const headerText = getHeaderText(childElementCount, mergeRequestCount);
+
+ return $('<span />', {
+ class: 'append-right-5',
+ text: headerText,
+ });
+}
+
+export function createLink(mergeRequest) {
+ return $('<a />', {
+ class: 'append-right-5',
+ href: mergeRequest.path,
+ text: `!${mergeRequest.iid}`,
+ });
+}
+
+export function createTitle(mergeRequest) {
+ return $('<span />', {
+ text: mergeRequest.title,
+ });
+}
+
+export function createItem(mergeRequest) {
+ const $item = $('<span />');
+ const $link = createLink(mergeRequest);
+ const $title = createTitle(mergeRequest);
+ $item.append($link);
+ $item.append($title);
+
+ return $item;
+}
+
+export function createContent(mergeRequests) {
+ const $content = $('<span />');
+
+ if (mergeRequests.length === 0) {
+ $content.text(s__('Commits|No related merge requests found'));
+ } else {
+ mergeRequests.forEach((mergeRequest) => {
+ const $header = createHeader($content.children().length, mergeRequests.length);
+ const $item = createItem(mergeRequest);
+ $content.append($header);
+ $content.append($item);
+ });
+ }
+
+ return $content;
+}
+
+export function fetchCommitMergeRequests() {
+ const $container = $('.merge-requests');
+
+ axios.get($container.data('projectCommitPath'))
+ .then((response) => {
+ const $content = createContent(response.data);
+
+ $container.html($content);
+ })
+ .catch(() => Flash(s__('Commits|An error occurred while fetching merge requests data.')));
+}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index a0224213aa0..c3eceb285f5 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
-export default class ProtectedTagDropdown {
+export default class CreateItemDropdown {
/**
* @param {Object} options containing
* `$dropdown` target element
@@ -8,11 +8,14 @@ export default class ProtectedTagDropdown {
* $dropdown must be an element created using `dropdown_tag()` rails helper
*/
constructor(options) {
- this.onSelect = options.onSelect;
+ this.defaultToggleLabel = options.defaultToggleLabel;
+ this.fieldName = options.fieldName;
+ this.onSelect = options.onSelect || (() => {});
+ this.getDataOption = options.getData;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
- this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag');
+ this.$createButton = this.$dropdownContainer.find('.js-dropdown-create-new-item');
this.buildDropdown();
this.bindEvents();
@@ -23,7 +26,7 @@ export default class ProtectedTagDropdown {
buildDropdown() {
this.$dropdown.glDropdown({
- data: this.getProtectedTags.bind(this),
+ data: this.getData.bind(this),
filterable: true,
remote: false,
search: {
@@ -31,14 +34,14 @@ export default class ProtectedTagDropdown {
},
selectable: true,
toggleLabel(selected) {
- return (selected && 'id' in selected) ? selected.title : 'Protected Tag';
+ return (selected && 'id' in selected) ? selected.title : this.defaultToggleLabel;
},
- fieldName: 'protected_tag[name]',
- text(protectedTag) {
- return _.escape(protectedTag.title);
+ fieldName: this.fieldName,
+ text(item) {
+ return _.escape(item.title);
},
- id(protectedTag) {
- return _.escape(protectedTag.id);
+ id(item) {
+ return _.escape(item.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
@@ -49,37 +52,37 @@ export default class ProtectedTagDropdown {
}
bindEvents() {
- this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this));
+ this.$createButton.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
+ e.preventDefault();
+
+ // Refresh the dropdown's data, which ends up calling `getData`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
- e.preventDefault();
}
- getProtectedTags(term, callback) {
- if (this.selectedTag) {
- callback(gon.open_tags.concat(this.selectedTag));
- } else {
- callback(gon.open_tags);
- }
+ getData(term, callback) {
+ this.getDataOption(term, (data = []) => {
+ callback(data.concat(this.selectedItem || []));
+ });
}
- toggleCreateNewButton(tagName) {
- if (tagName) {
- this.selectedTag = {
- title: tagName,
- id: tagName,
- text: tagName,
+ toggleCreateNewButton(item) {
+ if (item) {
+ this.selectedItem = {
+ title: item,
+ id: item,
+ text: item,
};
this.$dropdownContainer
- .find('.js-create-new-protected-tag code')
- .text(tagName);
+ .find('.js-dropdown-create-new-item code')
+ .text(item);
}
- this.toggleFooter(!tagName);
+ this.toggleFooter(!item);
}
toggleFooter(toggleState) {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 111da7bbbd4..892719c4e70 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -28,12 +28,9 @@ import Flash from './flash';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import SecretValues from './behaviors/secret_values';
-import DeleteModal from './branches/branches_delete_modal';
import Group from './group';
import ProjectsList from './projects_list';
import setupProjectEdit from './project_edit';
-import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
-import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import ShortcutsWiki from './shortcuts_wiki';
import BlobViewer from './blob/viewer/index';
@@ -41,7 +38,6 @@ import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
-import ShortcutsBlob from './shortcuts_blob';
import Star from './star';
import TreeView from './tree';
import Wikis from './wikis';
@@ -53,7 +49,6 @@ import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility';
import NewGroupChild from './groups/new_group_child';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
-import AjaxLoadingSpinner from './ajax_loading_spinner';
import GlFieldErrors from './gl_field_errors';
import GLForm from './gl_form';
import Shortcuts from './shortcuts';
@@ -80,7 +75,7 @@ import Activities from './activities';
}
Dispatcher.prototype.initPageScripts = function() {
- var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
+ var path, shortcut_handler;
const page = $('body').attr('data-page');
if (!page) {
return false;
@@ -105,33 +100,6 @@ import Activities from './activities';
});
});
- function initBlob() {
- new LineHighlighter();
-
- new BlobLinePermalinkUpdater(
- document.querySelector('#blob-content-holder'),
- '.diff-line-num[data-line-number]',
- document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
- );
-
- shortcut_handler = new ShortcutsNavigation();
- fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
- fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
- new ShortcutsBlob({
- skipResetBindings: true,
- fileBlobPermalinkUrl,
- });
-
- new BlobForkSuggestion({
- openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
- forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
- cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
- suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
- actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
- })
- .init();
- }
-
const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search');
switch (page) {
@@ -243,8 +211,9 @@ import Activities from './activities';
new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML));
break;
case 'projects:branches:index':
- AjaxLoadingSpinner.init();
- new DeleteModal();
+ import('./pages/projects/branches/index')
+ .then(callDefault)
+ .catch(fail);
break;
case 'projects:issues:new':
case 'projects:issues:edit':
@@ -449,20 +418,37 @@ import Activities from './activities';
shortcut_handler = true;
break;
case 'projects:blob:show':
- new BlobViewer();
- initBlob();
+ import('./pages/projects/blob/show')
+ .then(callDefault)
+ .catch(fail);
+ shortcut_handler = true;
break;
case 'projects:blame:show':
- initBlob();
+ import('./pages/projects/blame/show')
+ .then(callDefault)
+ .catch(fail);
+ shortcut_handler = true;
break;
case 'groups:labels:new':
case 'groups:labels:edit':
+ new Labels();
+ break;
case 'projects:labels:new':
+ import('./pages/projects/labels/new')
+ .then(callDefault)
+ .catch(fail);
+ break;
case 'projects:labels:edit':
- new Labels();
+ import('./pages/projects/labels/edit')
+ .then(callDefault)
+ .catch(fail);
break;
- case 'groups:labels:index':
case 'projects:labels:index':
+ import('./pages/projects/labels/index')
+ .then(callDefault)
+ .catch(fail);
+ break;
+ case 'groups:labels:index':
if ($('.prioritized-labels').length) {
new LabelManager();
}
diff --git a/app/assets/javascripts/init_labels.js b/app/assets/javascripts/init_labels.js
new file mode 100644
index 00000000000..5f20055510f
--- /dev/null
+++ b/app/assets/javascripts/init_labels.js
@@ -0,0 +1,18 @@
+import LabelManager from './label_manager';
+import GroupLabelSubscription from './group_label_subscription';
+import ProjectLabelSubscription from './project_label_subscription';
+
+export default () => {
+ if ($('.prioritized-labels').length) {
+ new LabelManager(); // eslint-disable-line no-new
+ }
+ $('.label-subscription').each((i, el) => {
+ const $el = $(el);
+
+ if ($el.find('.dropdown-group-label').length) {
+ new GroupLabelSubscription($el); // eslint-disable-line no-new
+ } else {
+ new ProjectLabelSubscription($el); // eslint-disable-line no-new
+ }
+ });
+};
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 9e3f659db5f..321a4872ccc 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -30,8 +30,12 @@
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
+ /**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
jobStarted() {
- return this.job.started;
+ return !this.job.started === false;
},
},
watch: {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 1f18c196137..3c8452ac808 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -271,7 +271,7 @@ Please check your network connection and try again.`;
<div class="timeline-content timeline-content-form">
<form
ref="commentForm"
- class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
+ class="new-note common-note-form gfm-form js-main-target-form"
>
<div class="error-alert"></div>
@@ -301,7 +301,8 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
:disabled="isSubmitting"
placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()"
- @keydown.meta.enter="handleSave()">
+ @keydown.meta.enter="handleSave()"
+ @keydown.ctrl.enter="handleSave()">
</textarea>
</markdown-field>
<div class="note-form-actions">
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index aeda3497715..d382a9bb642 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -155,6 +155,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@keydown.meta.enter="handleUpdate()"
+ @keydown.ctrl.enter="handleUpdate()"
@keydown.up="editMyLastNote()"
@keydown.esc="cancelHandler(true)">
</textarea>
diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js
new file mode 100644
index 00000000000..480357a309c
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/blame/show/index.js
@@ -0,0 +1,3 @@
+import initBlob from '~/pages/projects/init_blob';
+
+export default initBlob;
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
new file mode 100644
index 00000000000..a3eeb1cefb6
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -0,0 +1,7 @@
+import BlobViewer from '~/blob/viewer/index';
+import initBlob from '~/pages/projects/init_blob';
+
+export default () => {
+ new BlobViewer(); // eslint-disable-line no-new
+ initBlob();
+};
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
new file mode 100644
index 00000000000..cee0f19bf2a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -0,0 +1,7 @@
+import AjaxLoadingSpinner from '~/ajax_loading_spinner';
+import DeleteModal from '~/branches/branches_delete_modal';
+
+export default () => {
+ AjaxLoadingSpinner.init();
+ new DeleteModal(); // eslint-disable-line no-new
+};
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index fee4be87e4a..24581460d06 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
+import { fetchCommitMergeRequests } from './commit_merge_requests';
export default () => {
new Diff();
@@ -17,4 +18,5 @@ export default () => {
const stickyBarPaddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
+ fetchCommitMergeRequests();
};
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
new file mode 100644
index 00000000000..26f0ad46114
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -0,0 +1,33 @@
+import LineHighlighter from '~/line_highlighter';
+import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
+import ShortcutsNavigation from '~/shortcuts_navigation';
+import ShortcutsBlob from '~/shortcuts_blob';
+import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
+
+export default () => {
+ new LineHighlighter(); // eslint-disable-line no-new
+
+ new BlobLinePermalinkUpdater( // eslint-disable-line no-new
+ document.querySelector('#blob-content-holder'),
+ '.diff-line-num[data-line-number]',
+ document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
+ );
+
+ const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+
+ new ShortcutsNavigation(); // eslint-disable-line no-new
+
+ new ShortcutsBlob({ // eslint-disable-line no-new
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
+
+ new BlobForkSuggestion({ // eslint-disable-line no-new
+ openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
+ forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
+ cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
+ suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
+ actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
+ }).init();
+};
diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js
new file mode 100644
index 00000000000..72c5e4744ac
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/labels/edit/index.js
@@ -0,0 +1,3 @@
+import Labels from '~/labels';
+
+export default () => new Labels();
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
new file mode 100644
index 00000000000..018345fa112
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -0,0 +1,3 @@
+import initLabels from '~/init_labels';
+
+export default initLabels;
diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js
new file mode 100644
index 00000000000..72c5e4744ac
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/labels/new/index.js
@@ -0,0 +1,3 @@
+import Labels from '~/labels';
+
+export default () => new Labels();
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
index 2660da3c558..e78ebce2923 100644
--- a/app/assets/javascripts/projects_dropdown/index.js
+++ b/app/assets/javascripts/projects_dropdown/index.js
@@ -19,11 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
- $(navEl).on('show.bs.dropdown', (e) => {
- const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu');
- dropdownEl.one('transitionend', () => {
- eventHub.$emit('dropdownOpen');
- });
+ $(navEl).on('shown.bs.dropdown', () => {
+ eventHub.$emit('dropdownOpen');
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 0a9fdb074e5..2948baeab11 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
-import ProtectedBranchDropdown from './protected_branch_dropdown';
+import CreateItemDropdown from '../create_item_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
@@ -35,10 +35,12 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback,
});
- // Protected branch dropdown
- this.protectedBranchDropdown = new ProtectedBranchDropdown({
+ this.createItemDropdown = new CreateItemDropdown({
$dropdown: $protectedBranchDropdown,
+ defaultToggleLabel: 'Protected Branch',
+ fieldName: 'protected_branch[name]',
onSelect: this.onSelectCallback,
+ getData: ProtectedBranchCreate.getProtectedBranches,
});
this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
@@ -60,6 +62,10 @@ export default class ProtectedBranchCreate {
this.$form.find('input[type="submit"]').attr('disabled', completedForm);
}
+ static getProtectedBranches(term, callback) {
+ callback(gon.open_branches);
+ }
+
loadPreviousSelection(mergeDropdown, pushDropdown) {
let mergeIndex = 0;
let pushIndex = 0;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
deleted file mode 100644
index 678882a8d2c..00000000000
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import _ from 'underscore';
-
-export default class ProtectedBranchDropdown {
- /**
- * @param {Object} options containing
- * `$dropdown` target element
- * `onSelect` event callback
- * $dropdown must be an element created using `dropdown_branch()` rails helper
- */
- constructor(options) {
- this.onSelect = options.onSelect;
- this.$dropdown = options.$dropdown;
- this.$dropdownContainer = this.$dropdown.parent();
- this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
- this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
-
- this.buildDropdown();
- this.bindEvents();
-
- // Hide footer
- this.toggleFooter(true);
- }
-
- buildDropdown() {
- this.$dropdown.glDropdown({
- data: this.getProtectedBranches.bind(this),
- filterable: true,
- remote: false,
- search: {
- fields: ['title'],
- },
- selectable: true,
- toggleLabel(selected) {
- return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
- },
- fieldName: 'protected_branch[name]',
- text(protectedBranch) {
- return _.escape(protectedBranch.title);
- },
- id(protectedBranch) {
- return _.escape(protectedBranch.id);
- },
- onFilter: this.toggleCreateNewButton.bind(this),
- clicked: (options) => {
- options.e.preventDefault();
- this.onSelect();
- },
- });
- }
-
- bindEvents() {
- this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
- }
-
- onClickCreateWildcard(e) {
- e.preventDefault();
-
- // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
- this.$dropdown.data('glDropdown').remote.execute();
- this.$dropdown.data('glDropdown').selectRowAtIndex();
- }
-
- getProtectedBranches(term, callback) {
- if (this.selectedBranch) {
- callback(gon.open_branches.concat(this.selectedBranch));
- } else {
- callback(gon.open_branches);
- }
- }
-
- toggleCreateNewButton(branchName) {
- if (branchName) {
- this.selectedBranch = {
- title: branchName,
- id: branchName,
- text: branchName,
- };
-
- this.$dropdownContainer
- .find('.js-create-new-protected-branch code')
- .text(branchName);
- }
-
- this.toggleFooter(!branchName);
- }
-
- toggleFooter(toggleState) {
- this.$dropdownFooter.toggleClass('hidden', toggleState);
- }
-}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 91bd140bd12..d1e4a75c17b 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -1,5 +1,5 @@
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
-import ProtectedTagDropdown from './protected_tag_dropdown';
+import CreateItemDropdown from '../create_item_dropdown';
export default class ProtectedTagCreate {
constructor() {
@@ -24,9 +24,12 @@ export default class ProtectedTagCreate {
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown
- this.protectedTagDropdown = new ProtectedTagDropdown({
+ this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
+ defaultToggleLabel: 'Protected Tag',
+ fieldName: 'protected_tag[name]',
onSelect: this.onSelectCallback,
+ getData: ProtectedTagCreate.getProtectedTags,
});
}
@@ -38,4 +41,8 @@ export default class ProtectedTagCreate {
this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
+
+ static getProtectedTags(term, callback) {
+ callback(gon.open_tags);
+ }
}
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 7226076a8fc..d69d100a26c 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,12 +1,22 @@
<script>
- /* eslint-disable vue/require-default-prop */
- import { __ } from '../../../locale';
+ import { __ } from '~/locale';
+ import icon from '~/vue_shared/components/icon.vue';
+ import toggleButton from '~/vue_shared/components/toggle_button.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
- import loadingButton from '../../../vue_shared/components/loading_button.vue';
+
+ const ICON_ON = 'notifications';
+ const ICON_OFF = 'notifications-off';
+ const LABEL_ON = __('Notifications on');
+ const LABEL_OFF = __('Notifications off');
export default {
+ directives: {
+ tooltip,
+ },
components: {
- loadingButton,
+ icon,
+ toggleButton,
},
props: {
loading: {
@@ -17,22 +27,23 @@
subscribed: {
type: Boolean,
required: false,
+ default: null,
},
id: {
type: Number,
required: false,
+ default: null,
},
},
computed: {
- buttonLabel() {
- let label;
- if (this.subscribed === false) {
- label = __('Subscribe');
- } else if (this.subscribed === true) {
- label = __('Unsubscribe');
- }
-
- return label;
+ showLoadingState() {
+ return this.subscribed === null;
+ },
+ notificationIcon() {
+ return this.subscribed ? ICON_ON : ICON_OFF;
+ },
+ notificationTooltip() {
+ return this.subscribed ? LABEL_ON : LABEL_OFF;
},
},
methods: {
@@ -46,21 +57,29 @@
<template>
<div>
<div class="sidebar-collapsed-icon">
- <i
- class="fa fa-rss"
- aria-hidden="true"
+ <span
+ v-tooltip
+ :title="notificationTooltip"
+ data-container="body"
+ data-placement="left"
>
- </i>
+ <icon
+ :name="notificationIcon"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon is-active"
+ />
+ </span>
</div>
<span class="issuable-header-text hide-collapsed pull-left">
{{ __('Notifications') }}
</span>
- <loading-button
- ref="loadingButton"
- class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
- :loading="loading"
- :label="buttonLabel"
- @click="toggleSubscription"
+ <toggle-button
+ ref="toggleButton"
+ class="pull-right hide-collapsed js-issuable-subscribe-button"
+ :is-loading="showLoadingState"
+ :value="subscribed"
+ @change="toggleSubscription"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 2b12718ae96..09031d3ffa1 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -23,11 +23,12 @@
name: {
type: String,
required: false,
- default: '',
+ default: null,
},
value: {
type: Boolean,
- required: true,
+ required: false,
+ default: null,
},
disabledInput: {
type: Boolean,
@@ -61,6 +62,7 @@
<template>
<label class="toggle-wrapper">
<input
+ v-if="name"
type="hidden"
:name="name"
:value="value"
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d1b3754d4ef..1d2303a3a2b 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -666,6 +666,16 @@
}
}
+.dropdown-create-new-item-button {
+ @include dropdown-link;
+
+ width: 100%;
+ background-color: transparent;
+ border: 0;
+ text-align: left;
+ text-overflow: ellipsis;
+}
+
.dropdown-loading {
position: absolute;
top: 0;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index ad160f37641..3b7256f3000 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -104,7 +104,10 @@
img {
height: 28px;
- margin-right: 8px;
+
+ + .logo-text {
+ margin-left: 8px;
+ }
}
&.wrap {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ae9a8b0182c..759719a72da 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -162,10 +162,6 @@
border: 0;
}
- span {
- display: inline-block;
- }
-
.select2-container span {
margin-top: 0;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 61a76d0387a..bf41005b6d5 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -895,17 +895,6 @@ pre.light-well {
}
}
-.create-new-protected-branch-button,
-.create-new-protected-tag-button {
- @include dropdown-link;
-
- width: 100%;
- background-color: transparent;
- border: 0;
- text-align: left;
- text-overflow: ellipsis;
-}
-
.protected-branches-list,
.protected-tags-list {
margin-bottom: 30px;
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 2e7344b1cad..effb484ef0f 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines, :merge_requests]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -52,6 +52,18 @@ class Projects::CommitController < Projects::ApplicationController
end
end
+ def merge_requests
+ @merge_requests = @commit.merge_requests.map do |mr|
+ { iid: mr.iid, path: merge_request_path(mr), title: mr.title }
+ end
+
+ respond_to do |format|
+ format.json do
+ render json: @merge_requests.to_json
+ end
+ end
+ end
+
def branches
# branch_names_contains/tag_names_contains can take a long time when there are thousands of
# branches/tags - each `git branch --contains xxx` request can consume a cpu core.
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 4865ec3dfe5..8b54ba3ad7c 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -29,7 +29,7 @@ class Projects::JobsController < Projects::ApplicationController
:project,
:tags
])
- @builds = @builds.page(params[:page]).per(30)
+ @builds = @builds.page(params[:page]).per(30).without_count
end
def cancel_all
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 39d7f5b159d..21904c87f01 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -238,6 +238,10 @@ class Commit
notes.includes(:author)
end
+ def merge_requests
+ @merge_requests ||= project.merge_requests.by_commit_sha(sha)
+ end
+
def method_missing(method, *args, &block)
@raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -342,10 +346,11 @@ class Commit
@merged_merge_request_hash[current_user]
end
- def has_been_reverted?(current_user, noteable = self)
+ def has_been_reverted?(current_user, notes_association = nil)
ext = all_references(current_user)
+ notes_association ||= notes_with_associations
- noteable.notes_with_associations.system.each do |note|
+ notes_association.system.each do |note|
note.all_references(current_user, extractor: ext)
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index b6c7b6735b9..7c236369793 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -1,5 +1,6 @@
module ResolvableDiscussion
extend ActiveSupport::Concern
+ include ::Gitlab::Utils::StrongMemoize
included do
# A number of properties of this `Discussion`, like `first_note` and `resolvable?`, are memoized.
@@ -31,27 +32,37 @@ module ResolvableDiscussion
end
def resolvable?
- @resolvable ||= potentially_resolvable? && notes.any?(&:resolvable?)
+ strong_memoize(:resolvable) do
+ potentially_resolvable? && notes.any?(&:resolvable?)
+ end
end
def resolved?
- @resolved ||= resolvable? && notes.none?(&:to_be_resolved?)
+ strong_memoize(:resolved) do
+ resolvable? && notes.none?(&:to_be_resolved?)
+ end
end
def first_note
- @first_note ||= notes.first
+ strong_memoize(:first_note) do
+ notes.first
+ end
end
def first_note_to_resolve
return unless resolvable?
- @first_note_to_resolve ||= notes.find(&:to_be_resolved?) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ strong_memoize(:first_note_to_resolve) do
+ notes.find(&:to_be_resolved?)
+ end
end
def last_resolved_note
return unless resolved?
- @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ strong_memoize(:last_resolved_note) do
+ resolved_notes.sort_by(&:resolved_at).last
+ end
end
def resolved_notes
@@ -93,8 +104,8 @@ module ResolvableDiscussion
# Set the notes array to the updated notes
@notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
- self.class.memoized_values.each do |var|
- instance_variable_set(:"@#{var}", nil)
+ self.class.memoized_values.each do |name|
+ clear_memoization(name)
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2669d2a6ff3..8028ff3875b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -140,7 +140,9 @@ class MergeRequest < ActiveRecord::Base
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
-
+ scope :by_commit_sha, ->(sha) do
+ where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
+ end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
@@ -982,7 +984,16 @@ class MergeRequest < ActiveRecord::Base
end
def can_be_reverted?(current_user)
- merge_commit && !merge_commit.has_been_reverted?(current_user, self)
+ return false unless merge_commit
+
+ merged_at = metrics&.merged_at
+ notes_association = notes_with_associations
+
+ if merged_at
+ notes_association = notes_association.where('created_at > ?', merged_at)
+ end
+
+ !merge_commit.has_been_reverted?(current_user, notes_association)
end
def can_be_cherry_picked?
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index afab72930c1..69a846da9be 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -28,6 +28,9 @@ class MergeRequestDiff < ActiveRecord::Base
end
scope :viewable, -> { without_state(:empty) }
+ scope :by_commit_sha, ->(sha) do
+ joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
+ end
scope :recent, -> { order(id: :desc).limit(100) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 029f2da2e4e..7ab7df4fdcd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -20,6 +20,7 @@ class Project < ActiveRecord::Base
include GroupDescendant
include Gitlab::SQL::Pattern
include DeploymentPlatform
+ include ::Gitlab::Utils::StrongMemoize
extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
@@ -993,9 +994,13 @@ class Project < ActiveRecord::Base
end
def repo_exists?
- @repo_exists ||= repository.exists?
- rescue
- @repo_exists = false
+ strong_memoize(:repo_exists) do
+ begin
+ repository.exists?
+ rescue
+ false
+ end
+ end
end
def root_ref?(branch)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d27212b2058..8e9f33c174c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -895,15 +895,18 @@ class Repository
branch = Gitlab::Git::Branch.find(self, branch_or_name)
if branch
- @root_ref_sha ||= commit(root_ref).sha
- same_head = branch.target == @root_ref_sha
- merged = ancestor?(branch.target, @root_ref_sha)
+ same_head = branch.target == root_ref_sha
+ merged = ancestor?(branch.target, root_ref_sha)
!same_head && merged
else
nil
end
end
+ def root_ref_sha
+ @root_ref_sha ||= commit(root_ref).sha
+ end
+
delegate :merged_branch_names, to: :raw_repository
def merge_base(first_commit_id, second_commit_id)
diff --git a/app/models/route.rb b/app/models/route.rb
index 7ba3ec06041..412f5fb45a5 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,7 +8,7 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- validate :ensure_permanent_paths
+ validate :ensure_permanent_paths, if: :path_changed?
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index a24516355bf..509f559c120 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -4,6 +4,34 @@
%div{ class: container_class }
.admin-dashboard.prepend-top-default
.row
+ .col-sm-4
+ .info-well.dark-well
+ .well-segment.well-centered
+ = link_to admin_projects_path do
+ %h3.text-center
+ Projects:
+ = number_with_delimiter(Project.cached_count)
+ %hr
+ = link_to('New project', new_project_path, class: "btn btn-new")
+ .col-sm-4
+ .info-well.dark-well
+ .well-segment.well-centered
+ = link_to admin_users_path do
+ %h3.text-center
+ Users:
+ = number_with_delimiter(User.count)
+ %hr
+ = link_to 'New user', new_admin_user_path, class: "btn btn-new"
+ .col-sm-4
+ .info-well.dark-well
+ .well-segment.well-centered
+ = link_to admin_groups_path do
+ %h3.text-center
+ Groups:
+ = number_with_delimiter(Group.count)
+ %hr
+ = link_to 'New group', new_admin_group_path, class: "btn btn-new"
+ .row
.col-md-4
.info-well
.well-segment.admin-well.admin-well-statistics
@@ -136,34 +164,6 @@
%span.pull-right
= Gitlab::Database.version
.row
- .col-sm-4
- .info-well.dark-well
- .well-segment.well-centered
- = link_to admin_projects_path do
- %h3.text-center
- Projects:
- = number_with_delimiter(Project.cached_count)
- %hr
- = link_to('New project', new_project_path, class: "btn btn-new")
- .col-sm-4
- .info-well.dark-well
- .well-segment.well-centered
- = link_to admin_users_path do
- %h3.text-center
- Users:
- = number_with_delimiter(User.count)
- %hr
- = link_to 'New user', new_admin_user_path, class: "btn btn-new"
- .col-sm-4
- .info-well.dark-well
- .well-segment.well-centered
- = link_to admin_groups_path do
- %h3.text-center
- Groups:
- = number_with_delimiter(Group.count)
- %hr
- = link_to 'New group', new_admin_group_path, class: "btn btn-new"
- .row
.col-md-4
.info-well
.well-segment.admin-well
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 42941acc508..3e85535dae0 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -7,7 +7,7 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
+ = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 46727811be4..e7fc83a8d04 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,8 +6,10 @@
%h1.title
= link_to root_path, title: 'Dashboard', id: 'logo' do
= brand_header_logo
- %span.logo-text.hidden-xs
- = brand_header_logo_type
+ - logo_text = brand_header_logo_type
+ - if logo_text.present?
+ %span.logo-text.hidden-xs
+ = logo_text
- if current_user
= render "layouts/nav/dashboard"
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index c5b1897c492..e759c87bda7 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -30,7 +30,7 @@
%li CI variables
%li Any encrypted tokens
%p
- Once the exported file is ready, you will receive a notification email with a download link.
+ Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- if project.export_project_path
= link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 09934c09865..461129a3e0e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -64,6 +64,12 @@
.commit-info.branches
%i.fa.fa-spinner.fa-spin
+ .well-segment.merge-request-info
+ .icon-container
+ = custom_icon('mr_bold')
+ %span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
+ = icon('spinner spin')
+
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
diff --git a/app/views/projects/jobs/_table.html.haml b/app/views/projects/jobs/_table.html.haml
index 82806f022ee..d124d3ebfc1 100644
--- a/app/views/projects/jobs/_table.html.haml
+++ b/app/views/projects/jobs/_table.html.haml
@@ -22,4 +22,4 @@
= render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, admin: admin }
- = paginate builds, theme: 'gitlab'
+ = paginate_collection(builds)
diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/projects/protected_branches/shared/_dropdown.html.haml
index 6e9c473494e..74435236808 100644
--- a/app/views/projects/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/shared/_dropdown.html.haml
@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%li
- %button{ class: "create-new-protected-branch-button js-create-new-protected-branch", title: "New Protected Branch" }
+ %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: "New Protected Branch" }
Create wildcard
%code
diff --git a/app/views/projects/protected_tags/shared/_dropdown.html.haml b/app/views/projects/protected_tags/shared/_dropdown.html.haml
index 9b6923210f7..f0d7dcccd36 100644
--- a/app/views/projects/protected_tags/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_tags/shared/_dropdown.html.haml
@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%li
- %button{ class: "create-new-protected-tag-button js-create-new-protected-tag", title: "New Protected Tag" }
+ %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: "New Protected Tag" }
Create wildcard
%code
diff --git a/changelogs/unreleased/40492-update-admin-dashboard-content-order.yml b/changelogs/unreleased/40492-update-admin-dashboard-content-order.yml
new file mode 100644
index 00000000000..2416b15b6d5
--- /dev/null
+++ b/changelogs/unreleased/40492-update-admin-dashboard-content-order.yml
@@ -0,0 +1,5 @@
+---
+title: Move row containing Projects, Users and Groups count to the top in admin dashboard
+merge_request: 16421
+author:
+type: changed
diff --git a/changelogs/unreleased/41731-predicate-memoization.yml b/changelogs/unreleased/41731-predicate-memoization.yml
new file mode 100644
index 00000000000..110f78063f4
--- /dev/null
+++ b/changelogs/unreleased/41731-predicate-memoization.yml
@@ -0,0 +1,5 @@
+---
+title: Properly memoize some predicate methods
+merge_request: 16329
+author:
+type: performance
diff --git a/changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml b/changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml
new file mode 100644
index 00000000000..2a3d00f8e5f
--- /dev/null
+++ b/changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml
@@ -0,0 +1,5 @@
+---
+title: Add reason to keep postgresql 9.2 for CI
+merge_request: 16277
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/41807-15665-consistently-502s-because-it-fetches-every-commit.yml b/changelogs/unreleased/41807-15665-consistently-502s-because-it-fetches-every-commit.yml
new file mode 100644
index 00000000000..146ae12afbd
--- /dev/null
+++ b/changelogs/unreleased/41807-15665-consistently-502s-because-it-fetches-every-commit.yml
@@ -0,0 +1,6 @@
+---
+title: Speed up loading merged merge requests when they contained a lot of commits
+ before merging
+merge_request: 16320
+author:
+type: performance
diff --git a/changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml b/changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml
new file mode 100644
index 00000000000..d4b7ec6a3b5
--- /dev/null
+++ b/changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure that emails contain absolute, rather than relative, links to user uploads
+merge_request: 16364
+author:
+type: fixed
diff --git a/changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml b/changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml
new file mode 100644
index 00000000000..32a6f87d98e
--- /dev/null
+++ b/changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Ctrl+Enter keyboard shortcut saving comment/note edit
+merge_request: 16415
+author:
+type: fixed
diff --git a/changelogs/unreleased/disable-pages-on-jobs.yml b/changelogs/unreleased/disable-pages-on-jobs.yml
new file mode 100644
index 00000000000..629768efce1
--- /dev/null
+++ b/changelogs/unreleased/disable-pages-on-jobs.yml
@@ -0,0 +1,6 @@
+---
+title: Use simple Next/Prev paging for jobs to avoid large count queries on arbitrarily
+ large sets of historical jobs
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/display-mr-in-commit-page.yml b/changelogs/unreleased/display-mr-in-commit-page.yml
new file mode 100644
index 00000000000..a9224c00b66
--- /dev/null
+++ b/changelogs/unreleased/display-mr-in-commit-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add link on commit page to merge request that introduced that commit
+merge_request: 13713
+author: Hiroyuki Sato
+type: added
diff --git a/changelogs/unreleased/fix_gitlab-ce-41891.yml b/changelogs/unreleased/fix_gitlab-ce-41891.yml
new file mode 100644
index 00000000000..56bdc1a7c32
--- /dev/null
+++ b/changelogs/unreleased/fix_gitlab-ce-41891.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix custom header logo design nitpick: Remove unneeded margin on empty logo text'
+merge_request: 16383
+author: Markus Doits
+type: fixed
diff --git a/changelogs/unreleased/mk-fix-permanent-redirect-validation.yml b/changelogs/unreleased/mk-fix-permanent-redirect-validation.yml
new file mode 100644
index 00000000000..153b2ccc25c
--- /dev/null
+++ b/changelogs/unreleased/mk-fix-permanent-redirect-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent invalid Route path if path is unchanged
+merge_request: 16397
+author:
+type: fixed
diff --git a/config/routes/project.rb b/config/routes/project.rb
index bdf4b199c0a..43ada9ba145 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -50,6 +50,7 @@ constraints(ProjectUrlConstrainer.new) do
post :revert
post :cherry_pick
get :diff_for_path
+ get :merge_requests
end
end
diff --git a/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb b/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb
new file mode 100644
index 00000000000..1b360b231a8
--- /dev/null
+++ b/db/migrate/20170827123848_add_index_on_merge_request_diff_commit_sha.rb
@@ -0,0 +1,17 @@
+# rubocop:disable RemoveIndex
+
+class AddIndexOnMergeRequestDiffCommitSha < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_request_diff_commits, :sha, length: Gitlab::Database.mysql? ? 20 : nil
+ end
+
+ def down
+ remove_index :merge_request_diff_commits, :sha if index_exists? :merge_request_diff_commits, :sha
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8a6db61250b..f47accca21a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1013,6 +1013,7 @@ ActiveRecord::Schema.define(version: 20180105212544) do
end
add_index "merge_request_diff_commits", ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_commits_on_mr_diff_id_and_order", unique: true, using: :btree
+ add_index "merge_request_diff_commits", ["sha"], name: "index_merge_request_diff_commits_on_sha", using: :btree
create_table "merge_request_diff_files", id: false, force: :cascade do |t|
t.integer "merge_request_diff_id", null: false
diff --git a/doc/api/users.md b/doc/api/users.md
index 478d747a50d..1da6fcf297d 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -415,6 +415,10 @@ GET /user
}
```
+## List user projects
+
+Please refer to the [List of user projects ](projects.md#list-user-projects).
+
## List SSH keys
Get a list of currently authenticated user's SSH keys.
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 8385ef936c6..01fb6cdf374 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -10,25 +10,6 @@ They are written by members of the GitLab Team and by
Part of the articles listed below link to the [GitLab Blog](https://about.gitlab.com/blog/),
where they were originally published.
-## Build, test, and deploy with GitLab CI/CD
-
-Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/README.md):
-
-| Article title | Category | Publishing date |
-| :------------ | :------: | --------------: |
-| [Autoscaling GitLab Runners on AWS](runner_autoscale_aws/index.md) | Admin guide | 2017-11-24 |
-| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017-07-13 |
-| [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) | Concepts | 2017-07-11 |
-| [Continuous Integration: From Jenkins to GitLab Using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/) | Concepts | 2017-07-27 |
-| [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) | Tutorial | 2016-12-14 |
-| [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/) | Tutorial | 2016-11-30 |
-| [Automated Debian Package Build with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) | Tutorial | 2016-10-12 |
-| [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/) | Tutorial | 2016-08-11 |
-| [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/) | Technical overview | 2016-06-09 |
-| [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/) | Technical overview | 2016-05-23 |
-| [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) | Technical overview | 2017-05-15 |
-| [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) | Tutorial | 2016-03-10 |
-
## GitLab Pages
Learn how to deploy a static website with [GitLab Pages](../user/project/pages/index.md#getting-started):
diff --git a/doc/articles/runner_autoscale_aws/index.md b/doc/articles/runner_autoscale_aws/index.md
index 9d4c4a57ce5..e2667aebc5f 100644
--- a/doc/articles/runner_autoscale_aws/index.md
+++ b/doc/articles/runner_autoscale_aws/index.md
@@ -1,410 +1 @@
----
-last_updated: 2017-11-24
----
-
-> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** Admin guide ||
-> **Level:** intermediary ||
-> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) ||
-> **Publication date:** 2017/11/24
-
-# Autoscaling GitLab Runner on AWS
-
-One of the biggest advantages of GitLab Runner is its ability to automatically
-spin up and down VMs to make sure your builds get processed immediately. It's a
-great feature, and if used correctly, it can be extremely useful in situations
-where you don't use your Runners 24/7 and want to have a cost-effective and
-scalable solution.
-
-## Introduction
-
-In this tutorial, we'll explore how to properly configure a GitLab Runner in
-AWS that will serve as the bastion where it will spawn new Docker machines on
-demand.
-
-In addition, we'll make use of [Amazon's EC2 Spot instances][spot] which will
-greatly reduce the costs of the Runner instances while still using quite
-powerful autoscaling machines.
-
-## Prerequisites
-
-NOTE: **Note:**
-A familiarity with Amazon Web Services (AWS) is required as this is where most
-of the configuration will take place.
-
-Your GitLab instance is going to need to talk to the Runners over the network,
-and that is something you need think about when configuring any AWS security
-groups or when setting up your DNS configuration.
-
-For example, you can keep the EC2 resources segmented away from public traffic
-in a different VPC to better strengthen your network security. Your environment
-is likely different, so consider what works best for your situation.
-
-### AWS security groups
-
-Docker Machine will attempt to use a
-[default security group](https://docs.docker.com/machine/drivers/aws/#security-group)
-with rules for port `2376`, which is required for communication with the Docker
-daemon. Instead of relying on Docker, you can create a security group with the
-rules you need and provide that in the Runner options as we will
-[see below](#the-runners-machine-section). This way, you can customize it to your
-liking ahead of time based on your networking environment.
-
-### AWS credentials
-
-You'll need an [AWS Access Key](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)
-tied to a user with permission to scale (EC2) and update the cache (via S3).
-Create a new user with [policies](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-policies-for-amazon-ec2.html)
-for EC2 (AmazonEC2FullAccess) and S3 (AmazonS3FullAccess). To be more secure,
-you can disable console login for that user. Keep the tab open or copy paste the
-security credentials in an editor as we'll use them later during the
-[Runner configuration](#the-runners-machine-section).
-
-## Prepare the bastion instance
-
-The first step is to install GitLab Runner in an EC2 instance that will serve
-as the bastion that spawns new machines. This doesn't have to be a powerful
-machine since it will not run any jobs itself, a `t2.micro` instance will do.
-This machine will be a dedicated host since we need it always up and running,
-thus it will be the only standard cost.
-
-NOTE: **Note:**
-For the bastion instance, choose a distribution that both Docker and GitLab
-Runner support, for example either Ubuntu, Debian, CentOS or RHEL will work fine.
-
-Install the prerequisites:
-
-1. Log in to your server
-1. [Install GitLab Runner from the official GitLab repository](https://docs.gitlab.com/runner/install/linux-repository.html)
-1. [Install Docker](https://docs.docker.com/engine/installation/#server)
-1. [Install Docker Machine](https://docs.docker.com/machine/install-machine/)
-
-Now that the Runner is installed, it's time to register it.
-
-## Registering the GitLab Runner
-
-Before configuring the GitLab Runner, you need to first register it, so that
-it connects with your GitLab instance:
-
-1. [Obtain a Runner token](../../ci/runners/README.md)
-1. [Register the Runner](https://docs.gitlab.com/runner/register/index.html#gnu-linux)
-1. When asked the executor type, enter `docker+machine`
-
-You can now move on to the most important part, configuring the GitLab Runner.
-
-TIP: **Tip:**
-If you want every user in your instance to be able to use the autoscaled Runners,
-register the Runner as a shared one.
-
-## Configuring the GitLab Runner
-
-Now that the Runner is registered, you need to edit its configuration file and
-add the required options for the AWS machine driver.
-
-Let's first break it down to pieces.
-
-### The global section
-
-In the global section, you can define the limit of the jobs that can be run
-concurrently across all Runners (`concurrent`). This heavily depends on your
-needs, like how many users your Runners will accommodate, how much time your
-builds take, etc. You can start with something low like `10`, and increase or
-decrease its value going forward.
-
-The `check_interval` option defines how often the Runner should check GitLab
-for new jobs, in seconds.
-
-Example:
-
-```toml
-concurrent = 10
-check_interval = 0
-```
-
-[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section)
-about all the options you can use.
-
-### The `runners` section
-
-From the `[[runners]]` section, the most important part is the `executor` which
-must be set to `docker+machine`. Most of those settings are taken care of when
-you register the Runner for the first time.
-
-`limit` sets the maximum number of machines (running and idle) that this Runner
-will spawn. For more info check the [relationship between `limit`, `concurrent`
-and `IdleCount`](https://docs.gitlab.com/runner/configuration/autoscale.html#how-concurrent-limit-and-idlecount-generate-the-upper-limit-of-running-machines).
-
-Example:
-
-```toml
-[[runners]]
- name = "gitlab-aws-autoscaler"
- url = "<URL of your GitLab instance>"
- token = "<Runner's token>"
- executor = "docker+machine"
- limit = 20
-```
-
-[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section)
-about all the options you can use under `[[runners]]`.
-
-### The `runners.docker` section
-
-In the `[runners.docker]` section you can define the default Docker image to
-be used by the child Runners if it's not defined in [`.gitlab-ci.yml`](../../ci/yaml/README.md).
-By using `privileged = true`, all Runners will be able to run
-[Docker in Docker](../../ci/docker/using_docker_build.md#use-docker-in-docker-executor)
-which is useful if you plan to build your own Docker images via GitLab CI/CD.
-
-Next, we use `disable_cache = true` to disable the Docker executor's inner
-cache mechanism since we will use the distributed cache mode as described
-in the following section.
-
-Example:
-
-```toml
- [runners.docker]
- image = "alpine"
- privileged = true
- disable_cache = true
-```
-
-[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-docker-section)
-about all the options you can use under `[runners.docker]`.
-
-### The `runners.cache` section
-
-To speed up your jobs, GitLab Runner provides a cache mechanism where selected
-directories and/or files are saved and shared between subsequent jobs.
-While not required for this setup, it is recommended to use the distributed cache
-mechanism that GitLab Runner provides. Since new instances will be created on
-demand, it is essential to have a common place where the cache is stored.
-
-In the following example, we use Amazon S3:
-
-```toml
- [runners.cache]
- Type = "s3"
- ServerAddress = "s3.amazonaws.com"
- AccessKey = "<your AWS Access Key ID>"
- SecretKey = "<your AWS Secret Access Key>"
- BucketName = "<the bucket where your cache should be kept>"
- BucketLocation = "us-east-1"
- Shared = true
-```
-
-Here's some more info to further explore the cache mechanism:
-
-- [Reference for `runners.cache`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-cache-section)
-- [Deploying and using a cache server for GitLab Runner](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching)
-- [How cache works](../../ci/yaml/README.md#cache)
-
-### The `runners.machine` section
-
-This is the most important part of the configuration and it's the one that
-tells GitLab Runner how and when to spawn new or remove old Docker Machine
-instances.
-
-We will focus on the AWS machine options, for the rest of the settings read
-about the:
-
-- [Autoscaling algorithm and the parameters it's based on](https://docs.gitlab.com/runner/configuration/autoscale.html#autoscaling-algorithm-and-parameters) - depends on the needs of your organization
-- [Off peak time configuration](https://docs.gitlab.com/runner/configuration/autoscale.html#off-peak-time-mode-configuration) - useful when there are regular time periods in your organization when no work is done, for example weekends
-
-Here's an example of the `runners.machine` section:
-
-```toml
- [runners.machine]
- IdleCount = 1
- IdleTime = 1800
- MaxBuilds = 10
- OffPeakPeriods = [
- "* * 0-9,18-23 * * mon-fri *",
- "* * * * * sat,sun *"
- ]
- OffPeakIdleCount = 0
- OffPeakIdleTime = 1200
- MachineDriver = "amazonec2"
- MachineName = "gitlab-docker-machine-%s"
- MachineOptions = [
- "amazonec2-access-key=XXXX",
- "amazonec2-secret-key=XXXX",
- "amazonec2-region=us-central-1",
- "amazonec2-vpc-id=vpc-xxxxx",
- "amazonec2-subnet-id=subnet-xxxxx",
- "amazonec2-use-private-address=true",
- "amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true",
- "amazonec2-security-group=docker-machine-scaler",
- "amazonec2-instance-type=m4.2xlarge",
- ]
-```
-
-The Docker Machine driver is set to `amazonec2` and the machine name has a
-standard prefix followed by `%s` (required) that is replaced by the ID of the
-child Runner: `gitlab-docker-machine-%s`.
-
-Now, depending on your AWS infrastructure, there are many options you can set up
-under `MachineOptions`. Below you can see the most common ones.
-
-| Machine option | Description |
-| -------------- | ----------- |
-| `amazonec2-access-key=XXXX` | The AWS access key of the user that has permissions to create EC2 instances, see [AWS credentials](#aws-credentials). |
-| `amazonec2-secret-key=XXXX` | The AWS secret key of the user that has permissions to create EC2 instances, see [AWS credentials](#aws-credentials). |
-| `amazonec2-region=eu-central-1` | The region to use when launching the instance. You can omit this entirely and the default `us-east-1` will be used. |
-| `amazonec2-vpc-id=vpc-xxxxx` | Your [VPC ID](https://docs.docker.com/machine/drivers/aws/#vpc-id) to launch the instance in. |
-| `amazonec2-subnet-id=subnet-xxxx` | The AWS VPC subnet ID. |
-| `amazonec2-use-private-address=true` | Use the private IP address of Docker Machines, but still create a public IP address. Useful to keep the traffic internal and avoid extra costs.|
-| `amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true` | AWS extra tag key-value pairs, useful to identify the instances on the AWS console. The "Name" tag is set to the machine name by default. We set the "runner-manager-name" to match the Runner name set in `[[runners]]`, so that we can filter all the EC2 instances created by a specific manager setup. Read more about [using tags in AWS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html). |
-| `amazonec2-security-group=docker-machine-scaler` | AWS VPC security group name, see [AWS security groups](#aws-security-groups). |
-| `amazonec2-instance-type=m4.2xlarge` | The instance type that the child Runners will run on. |
-
-TIP: **Tip:**
-Under `MachineOptions` you can add anything that the [AWS Docker Machine driver
-supports](https://docs.docker.com/machine/drivers/aws/#options). You are highly
-encouraged to read Docker's docs as your infrastructure setup may warrant
-different options to be applied.
-
-NOTE: **Note:**
-The child instances will use by default Ubuntu 16.04 unless you choose a
-different AMI ID by setting `amazonec2-ami`.
-
-NOTE: **Note:**
-If you specify `amazonec2-private-address-only=true` as one of the machine
-options, your EC2 instance won't get assigned a public IP. This is ok if your
-VPC is configured correctly with an Internet Gateway (IGW) and routing is fine,
-but it’s something to consider if you've got a more complex configuration. Read
-more in [Docker docs about VPC connectivity](https://docs.docker.com/machine/drivers/aws/#vpc-connectivity).
-
-[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-machine-section)
-about all the options you can use under `[runners.machine]`.
-
-### Getting it all together
-
-Here's the full example of `/etc/gitlab-runner/config.toml`:
-
-```toml
-concurrent = 10
-check_interval = 0
-
-[[runners]]
- name = "gitlab-aws-autoscaler"
- url = "<URL of your GitLab instance>"
- token = "<Runner's token>"
- executor = "docker+machine"
- limit = 20
- [runners.docker]
- image = "alpine"
- privileged = true
- disable_cache = true
- [runners.cache]
- Type = "s3"
- ServerAddress = "s3.amazonaws.com"
- AccessKey = "<your AWS Access Key ID>"
- SecretKey = "<your AWS Secret Access Key>"
- BucketName = "<the bucket where your cache should be kept>"
- BucketLocation = "us-east-1"
- Shared = true
- [runners.machine]
- IdleCount = 1
- IdleTime = 1800
- MaxBuilds = 100
- OffPeakPeriods = [
- "* * 0-9,18-23 * * mon-fri *",
- "* * * * * sat,sun *"
- ]
- OffPeakIdleCount = 0
- OffPeakIdleTime = 1200
- MachineDriver = "amazonec2"
- MachineName = "gitlab-docker-machine-%s"
- MachineOptions = [
- "amazonec2-access-key=XXXX",
- "amazonec2-secret-key=XXXX",
- "amazonec2-region=us-central-1",
- "amazonec2-vpc-id=vpc-xxxxx",
- "amazonec2-subnet-id=subnet-xxxxx",
- "amazonec2-use-private-address=true",
- "amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true",
- "amazonec2-security-group=docker-machine-scaler",
- "amazonec2-instance-type=m4.2xlarge",
- ]
-```
-
-## Cutting down costs with Amazon EC2 Spot instances
-
-As [described by][spot] Amazon:
-
->
-Amazon EC2 Spot instances allow you to bid on spare Amazon EC2 computing capacity.
-Since Spot instances are often available at a discount compared to On-Demand
-pricing, you can significantly reduce the cost of running your applications,
-grow your application’s compute capacity and throughput for the same budget,
-and enable new types of cloud computing applications.
-
-In addition to the [`runners.machine`](#the-runners-machine-section) options
-you picked above, in `/etc/gitlab-runner/config.toml` under the `MachineOptions`
-section, add the following:
-
-```toml
- MachineOptions = [
- "amazonec2-request-spot-instance=true",
- "amazonec2-spot-price=0.03",
- "amazonec2-block-duration-minutes=60"
- ]
-```
-
-With this configuration, Docker Machines are created on Spot instances with a
-maximum bid price of $0.03 per hour and the duration of the Spot instance is
-capped at 60 minutes. The `0.03` number mentioned above is just an example, so
-be sure to check on the current pricing based on the region you picked.
-
-To learn more about Amazon EC2 Spot instances, visit the following links:
-
-- https://aws.amazon.com/ec2/spot/
-- https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html
-- https://aws.amazon.com/blogs/aws/focusing-on-spot-instances-lets-talk-about-best-practices/
-
-### Caveats of Spot instances
-
-While Spot instances is a great way to use unused resources and minimize the
-costs of your infrastructure, you must be aware of the implications.
-
-Running CI jobs on Spot instances may increase the failure rates because of the
-Spot instances pricing model. If the price exceeds your bid, the existing Spot
-instances will be immediately terminated and all your jobs on that host will fail.
-
-As a consequence, the auto-scale Runner would fail to create new machines while
-it will continue to request new instances. This eventually will make 60 requests
-and then AWS won't accept any more. Then once the Spot price is acceptable, you
-are locked out for a bit because the call amount limit is exceeded.
-
-If you encounter that case, you can use the following command in the bastion
-machine to see the Docker Machines state:
-
-```sh
-docker-machine ls -q --filter state=Error --format "{{.NAME}}"
-```
-
-NOTE: **Note:**
-There are some issues regarding making GitLab Runner gracefully handle Spot
-price changes, and there are reports of `docker-machine` attempting to
-continually remove a Docker Machine. GitLab has provided patches for both cases
-in the upstream project. For more information, see issues
-[#2771](https://gitlab.com/gitlab-org/gitlab-runner/issues/2771) and
-[#2772](https://gitlab.com/gitlab-org/gitlab-runner/issues/2772).
-
-## Conclusion
-
-In this guide we learned how to install and configure a GitLab Runner in
-autoscale mode on AWS.
-
-Using the autoscale feature of GitLab Runner can save you both time and money.
-Using the Spot instances that AWS provides can save you even more, but you must
-be aware of the implications. As long as your bid is high enough, there shouldn't
-be an issue.
-
-You can read the following use cases from which this tutorial was (heavily)
-influenced:
-
-- [HumanGeo - Scaling GitLab CI](http://blog.thehumangeo.com/gitlab-autoscale-runners.html)
-- [subtrakt Health - Autoscale GitLab CI Runners and save 90% on EC2 costs](https://substrakthealth.com/news/gitlab-ci-cost-savings/)
-
-[spot]: https://aws.amazon.com/ec2/spot/
+This document was moved to [another location](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/index.html).
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 5829aaee9c9..3a10365af77 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -2,151 +2,118 @@
comments: false
---
-# GitLab Continuous Integration (GitLab CI)
+# GitLab Continuous Integration (GitLab CI/CD)
![Pipeline graph](img/cicd_pipeline_infograph.png)
The benefits of Continuous Integration are huge when automation plays an
integral part of your workflow. GitLab comes with built-in Continuous
-Integration, Continuous Deployment, and Continuous Delivery support to build,
-test, and deploy your application.
+Integration, Continuous Deployment, and Continuous Delivery support
+to build, test, and deploy your application.
Here's some info we've gathered to get you started.
## Getting started
-The first steps towards your GitLab CI journey.
+The first steps towards your GitLab CI/CD journey.
-- [Getting started with GitLab CI](quick_start/README.md)
-- [Pipelines and jobs](pipelines.md)
-- [Configure a Runner, the application that runs your jobs](runners/README.md)
-- **Articles:**
- - [Getting started with GitLab and GitLab CI - Intro to CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
- - [Continuous Integration, Delivery, and Deployment with GitLab - Intro to CI/CD](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
- - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
- - [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
- - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- [Getting started with GitLab CI/CD](quick_start/README.md): understand how GitLab CI/CD works.
+- GitLab CI/CD configuration file: [`.gitlab-ci.yml`](yaml/README.md) - Learn all about the ins and outs of `.gitlab-ci.yml`.
+- [Pipelines and jobs](pipelines.md): configure your GitLab CI/CD pipelines to build, test, and deploy your application.
+- Runners: The [GitLab Runner](https://docs.gitlab.com/runner/) is responsible by running the jobs in your CI/CD pipeline. On GitLab.com, Shared Runners are enabled by default, so
+you don't need to set up anything to start to use them with GitLab CI/CD.
+
+### Introduction to GitLab CI/CD
+
+- Article (2016-08-05): [Continuous Integration, Delivery, and Deployment with GitLab - Intro to CI/CD](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+- Article (2015-12-14): [Getting started with GitLab and GitLab CI - Intro to CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+- Article (2017-07-13): [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/)
- **Videos:**
- - [Demo (Streamed live on Jul 17, 2017): GitLab CI/CD Deep Dive](https://youtu.be/pBe4t1CD8Fc?t=195)
- - [Demo (March, 2017): how to get started using CI/CD with GitLab](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
- - [Webcast (April, 2016): getting started with CI in GitLab](https://about.gitlab.com/2016/04/20/webcast-recording-and-slides-introduction-to-ci-in-gitlab/)
+ - Demo (Streamed live on Jul 17, 2017): [GitLab CI/CD Deep Dive](https://youtu.be/pBe4t1CD8Fc?t=195)
+ - Demo (March, 2017): [How to get started using CI/CD with GitLab](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
+ - Webcast (April, 2016): [Getting started with CI in GitLab](https://about.gitlab.com/2016/04/20/webcast-recording-and-slides-introduction-to-ci-in-gitlab/)
- **Third-party videos:**
- [Intégration continue avec GitLab (September, 2016)](https://www.youtube.com/watch?v=URcMBXjIr24&t=13s)
- [GitLab CI for Minecraft Plugins (July, 2016)](https://www.youtube.com/watch?v=Z4pcI9F8yf8)
-## Reference guides
+### Why GitLab CI/CD?
+
+ - Article (2016-10-17): [Why We Chose GitLab CI for our CI/CD Solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
+ - Article (2016-07-22): [Building our web-app on GitLab CI: 5 reasons why Captain Train migrated from Jenkins to GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
-Once you get familiar with the getting started guides, you'll find yourself
-digging into specific reference guides.
+## Exploring GitLab CI/CD
-- [`.gitlab-ci.yml` reference](yaml/README.md) - Learn all about the ins and
- outs of `.gitlab-ci.yml` definitions
-- [CI Variables](variables/README.md) - Learn how to use variables defined in
+- [CI/CD Variables](variables/README.md) - Learn how to use variables defined in
your `.gitlab-ci.yml` or secured ones defined in your project's settings
- **The permissions model** - Learn about the access levels a user can have for
performing certain CI actions
- [User permissions](../user/permissions.md#gitlab-ci)
- [Job permissions](../user/permissions.md#job-permissions)
-
-## Auto DevOps
-
-- [Auto DevOps](../topics/autodevops/index.md)
-
-## GitLab CI + Docker
-
-Leverage the power of Docker to run your CI pipelines.
-
-- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
-- [Use CI to build Docker images](docker/using_docker_build.md)
-- [CI services (linked Docker containers)](services/README.md)
-- **Articles:**
- - [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+- [Configure a Runner, the application that runs your jobs](runners/README.md)
+- Article (2016-03-01): [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+- Article (2016-07-29): [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+- Article (2016-08-26): [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- Article (2016-05-23): [Introduction to GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
## Advanced use
-Once you get familiar with the basics of GitLab CI, it's time to dive in and
+Once you get familiar with the basics of GitLab CI/CD, it's time to dive in and
learn how to leverage its potential even more.
-- [Environments and deployments](environments.md) - Separate your jobs into
+- [Environments and deployments](environments.md): Separate your jobs into
environments and use them for different purposes like testing, building and
deploying
- [Job artifacts](../user/project/pipelines/job_artifacts.md)
-- [Git submodules](git_submodules.md) - How to run your CI jobs when Git
+- [Git submodules](git_submodules.md): How to run your CI jobs when Git
submodules are involved
-- [Auto deploy](autodeploy/index.md)
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger pipelines through the GitLab API](triggers/README.md)
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
+## GitLab CI/CD for Docker
+
+Leverage the power of Docker to run your CI pipelines.
+
+- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
+- [Use CI to build Docker images](docker/using_docker_build.md)
+- [CI services (linked Docker containers)](services/README.md)
+- Article (2016-03-01): [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+
## Review Apps
-- [Review Apps](review_apps/index.md)
-- **Articles:**
- - [Introducing Review Apps](https://about.gitlab.com/2016/11/22/introducing-review-apps/)
- - [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
+- [Review Apps documentation](review_apps/index.md)
+- Article (2016-11-22): [Introducing Review Apps](https://about.gitlab.com/2016/11/22/introducing-review-apps/)
+- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
+
+## Auto DevOps
+
+- [Auto DevOps](../topics/autodevops/index.md): Auto DevOps automatically detects, builds, tests, deploys, and monitors your applications.
## GitLab CI for GitLab Pages
-See the topic on [GitLab Pages](../user/project/pages/index.md).
+See the documentation on [GitLab Pages](../user/project/pages/index.md).
-## Special configuration
+## Special configuration (GitLab admin)
-You can change the default behavior of GitLab CI in your whole GitLab instance
-as well as in each project.
+As a GitLab administrator, you can change the default behavior of GitLab CI/CD in
+your whole GitLab instance as well as in each project.
-- **Project specific**
+- **Project specific:**
- [Pipelines settings](../user/project/pipelines/settings.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
-- **Affecting the whole GitLab instance**
+- **Affecting the whole GitLab instance:**
- [Continuous Integration admin settings](../user/admin_area/settings/continuous_integration.md)
## Examples
->**Note:**
-A collection of `.gitlab-ci.yml` files is maintained at the
-[GitLab CI Yml project][gitlab-ci-templates].
-If your favorite programming language or framework is missing we would love
-your help by sending a merge request with a `.gitlab-ci.yml`.
-
-Here is an collection of tutorials and guides on setting up your CI pipeline.
-
-- [GitLab CI examples](examples/README.md) for the following languages and frameworks:
- - [PHP](examples/php.md)
- - [Ruby](examples/test-and-deploy-ruby-application-to-heroku.md)
- - [Python](examples/test-and-deploy-python-application-to-heroku.md)
- - [Clojure](examples/test-clojure-application.md)
- - [Scala](examples/test-scala-application.md)
- - [Phoenix](examples/test-phoenix-application.md)
- - [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
- - [Analyze code quality with the Code Climate CLI](examples/code_climate.md)
-- **Articles**
- - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](examples/laravel_with_gitlab_and_envoy/index.md)
- - [How to deploy Maven projects to Artifactory with GitLab CI/CD](examples/artifactory_and_gitlab/index.md)
- - [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
- - [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- - [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
- - [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- - [CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
- - [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
-- **Miscellaneous**
- - [Using `dpl` as deployment tool](examples/deployment/README.md)
- - [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
- - [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
+Check the [GitLab CI/CD examples](examples/README.md) for a collection of tutorials and guides on setting up your CI/CD pipeline for various programming languages, frameworks,
+and operating systems.
## Integrations
-- **Articles:**
- - [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
- - [Getting Started with GitLab and Shippable Continuous Integration](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
- - [GitLab Partners with DigitalOcean to make Continuous Integration faster, safer, and more affordable](https://about.gitlab.com/2016/04/19/gitlab-partners-with-digitalocean-to-make-continuous-integration-faster-safer-and-more-affordable/)
-
-## Why GitLab CI?
-
-- **Articles:**
- - [Why We Chose GitLab CI for our CI/CD Solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
- - [Building our web-app on GitLab CI: 5 reasons why Captain Train migrated from Jenkins to GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
+- Article (2016-06-09): [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+- Article (2016-05-05): [Getting Started with GitLab and Shippable Continuous Integration](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
+- Article (2016-04-19): [GitLab Partners with DigitalOcean to make Continuous Integration faster, safer, and more affordable](https://about.gitlab.com/2016/04/19/gitlab-partners-with-digitalocean-to-make-continuous-integration-faster-safer-and-more-affordable/)
## Breaking changes
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index d4590d0f495..b53bd79f39e 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -2,81 +2,59 @@
comments: false
---
-# GitLab CI Examples
+# GitLab CI/CD Examples
-A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
-If your favorite programming language or framework are missing we would love your help by sending a merge request
-with a `.gitlab-ci.yml`.
+A collection of `.gitlab-ci.yml` template files is maintained at the [GitLab CI/CD YAML project][gitlab-ci-templates]. When you create a new file via the UI,
+GitLab will give you the option to choose one of the templates existent on this project.
+If your favorite programming language or framework are missing we would love your
+help by sending a merge request with a new `.gitlab-ci.yml` to this project.
-Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
+There's also a collection of repositories with [example projects](https://gitlab.com/gitlab-examples) for various languages. You can fork an adjust them to your own needs.
## Languages, frameworks, OSs
-### PHP
+- **PHP**:
+ - [Testing a PHP application](php.md)
+ - [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
+ - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md)
+- **Ruby**: [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
+- **Python**: [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
+- **Java**: [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+- **Scala**: [Test a Scala application](test-scala-application.md)
+- **Clojure**: [Test a Clojure application](test-clojure-application.md)
+- **Elixir**:
+ - [Test a Phoenix application](test-phoenix-application.md)
+ - [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
+- **iOS and macOS**:
+ - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+ - [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/)
+- **Android**: [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+- **Debian**: [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
+- **Maven**: [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md)
+
+### Miscellaneous
-- [Testing a PHP application](php.md)
-- [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
-- [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md)
-
-### Ruby
-
-- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
-
-### Python
-
-- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
-
-### Java
-
-- [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
-
-### Scala
-
-- [Test a Scala application](test-scala-application.md)
-
-### Clojure
-
-- [Test a Clojure application](test-clojure-application.md)
-
-### Elixir
-
-- [Test a Phoenix application](test-phoenix-application.md)
-- [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
-
-### iOS
-
-- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
-
-### Android
-
-- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+- [Using `dpl` as deployment tool](deployment/README.md)
+- [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
### Code quality analysis
-- [Analyze code quality with the Code Climate CLI](code_climate.md)
+[Analyze code quality with the Code Climate CLI](code_climate.md).
-### Other
-
-- [Using `dpl` as deployment tool](deployment/README.md)
-- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
-- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
-- [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md)
+### GitLab CI/CD for Review Apps
-## GitLab CI/CD for GitLab Pages
+- [Example project](https://gitlab.com/gitlab-examples/review-apps-nginx/) that shows how to use GitLab CI/CD for [Review Apps](../review_apps/index.html).
+- [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/)
-- [Example projects](https://gitlab.com/pages)
-- [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](../../user/project/pages/getting_started_part_four.md)
-- [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/):
-examples for Ruby-, NodeJS-, Python-, and GoLang-based SSGs
-- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
-- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
+### GitLab CI/CD for GitLab Pages
See the documentation on [GitLab Pages](../../user/project/pages/index.md) for a complete overview.
-## More
+## Contributing
-Contributions are very much welcomed! You can help your favorite programming
-language and GitLab by sending a merge request with a guide for that language.
+Contributions are very welcome! You can help your favorite programming
+language users and GitLab by sending a merge request with a guide for that language.
+You may want to apply for the [GitLab Community Writers Program](https://about.gitlab.com/community-writers/)
+to get paid for writing complete articles for GitLab.
[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index d6cfa524a3a..819354bb780 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -164,7 +164,7 @@ Feature: Project Issues
Given project "Shop" have "Release 0.4" open issue
When I visit issue page "Release 0.4"
Then I should see that I am subscribed
- When I click button "Unsubscribe"
+ When I click the subscription toggle
Then I should see that I am unsubscribed
@javascript
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 3843374678c..3cd26bb429b 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -21,20 +21,20 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I should see that I am subscribed' do
wait_for_requests
- expect(find('.js-issuable-subscribe-button span')).to have_content 'Unsubscribe'
+ expect(find('.js-issuable-subscribe-button')).to have_css 'button.is-checked'
end
step 'I should see that I am unsubscribed' do
wait_for_requests
- expect(find('.js-issuable-subscribe-button span')).to have_content 'Subscribe'
+ expect(find('.js-issuable-subscribe-button')).to have_css 'button:not(.is-checked)'
end
step 'I click link "Closed"' do
find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
end
- step 'I click button "Unsubscribe"' do
- click_on "Unsubscribe"
+ step 'I click the subscription toggle' do
+ find('.js-issuable-subscribe-button button').click
end
step 'I should see "Release 0.3" in issues' do
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 5c197afd782..f6169b2c85d 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -50,15 +50,22 @@ module Banzai
end
def process_link_to_upload_attr(html_attr)
- uri_parts = [html_attr.value]
+ path_parts = [html_attr.value]
if group
- uri_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
+ path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
elsif project
- uri_parts.unshift(relative_url_root, project.full_path)
+ path_parts.unshift(relative_url_root, project.full_path)
end
- html_attr.value = File.join(*uri_parts)
+ path = File.join(*path_parts)
+
+ html_attr.value =
+ if context[:only_path]
+ path
+ else
+ URI.join(Gitlab.config.gitlab.base_url, path).to_s
+ end
end
def process_link_to_repository_attr(html_attr)
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index 476c46341ae..4e0121ca34d 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -7,6 +7,7 @@ module Gitlab
class PrepareUntrackedUploads # rubocop:disable Metrics/ClassLength
# For bulk_queue_background_migration_jobs_by_range
include Database::MigrationHelpers
+ include ::Gitlab::Utils::StrongMemoize
FIND_BATCH_SIZE = 500
RELATIVE_UPLOAD_DIR = "uploads".freeze
@@ -142,7 +143,9 @@ module Gitlab
end
def postgresql?
- @postgresql ||= Gitlab::Database.postgresql?
+ strong_memoize(:postgresql) do
+ Gitlab::Database.postgresql?
+ end
end
def can_bulk_insert_and_ignore_duplicates?
@@ -150,8 +153,9 @@ module Gitlab
end
def postgresql_pre_9_5?
- @postgresql_pre_9_5 ||= postgresql? &&
- Gitlab::Database.version.to_f < 9.5
+ strong_memoize(:postgresql_pre_9_5) do
+ postgresql? && Gitlab::Database.version.to_f < 9.5
+ end
end
def schedule_populate_untracked_uploads_jobs
diff --git a/lib/gitlab/bare_repository_import/repository.rb b/lib/gitlab/bare_repository_import/repository.rb
index 85b79362196..c0c666dfb7b 100644
--- a/lib/gitlab/bare_repository_import/repository.rb
+++ b/lib/gitlab/bare_repository_import/repository.rb
@@ -1,6 +1,8 @@
module Gitlab
module BareRepositoryImport
class Repository
+ include ::Gitlab::Utils::StrongMemoize
+
attr_reader :group_path, :project_name, :repo_path
def initialize(root_path, repo_path)
@@ -41,11 +43,15 @@ module Gitlab
private
def wiki?
- @wiki ||= repo_path.end_with?('.wiki.git')
+ strong_memoize(:wiki) do
+ repo_path.end_with?('.wiki.git')
+ end
end
def hashed?
- @hashed ||= repo_relative_path.include?('@hashed')
+ strong_memoize(:hashed) do
+ repo_relative_path.include?('@hashed')
+ end
end
def repo_relative_path
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
index 9a72de87bab..32cbb7ca6af 100644
--- a/lib/gitlab/ci/pipeline/chain/skip.rb
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -3,6 +3,8 @@ module Gitlab
module Pipeline
module Chain
class Skip < Chain::Base
+ include ::Gitlab::Utils::StrongMemoize
+
SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i
def perform!
@@ -24,7 +26,9 @@ module Gitlab
def commit_message_skips_ci?
return false unless @pipeline.git_commit_message
- @skipped ||= !!(@pipeline.git_commit_message =~ SKIP_PATTERN)
+ strong_memoize(:commit_message_skips_ci) do
+ !!(@pipeline.git_commit_message =~ SKIP_PATTERN)
+ end
end
end
end
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
index bc97aa63b02..f33c87f554d 100644
--- a/lib/gitlab/ci/stage/seed.rb
+++ b/lib/gitlab/ci/stage/seed.rb
@@ -2,6 +2,8 @@ module Gitlab
module Ci
module Stage
class Seed
+ include ::Gitlab::Utils::StrongMemoize
+
attr_reader :pipeline
delegate :project, to: :pipeline
@@ -50,7 +52,9 @@ module Gitlab
private
def protected_ref?
- @protected_ref ||= project.protected_for?(pipeline.ref)
+ strong_memoize(:protected_ref) do
+ project.protected_for?(pipeline.ref)
+ end
end
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 5da9befa08e..4f160e4a447 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -14,6 +14,8 @@ module Gitlab
# puts label.name
# end
class Client
+ include ::Gitlab::Utils::StrongMemoize
+
attr_reader :octokit
# A single page of data and the corresponding page number.
@@ -173,7 +175,9 @@ module Gitlab
end
def rate_limiting_enabled?
- @rate_limiting_enabled ||= api_endpoint.include?('.github.com')
+ strong_memoize(:rate_limiting_enabled) do
+ api_endpoint.include?('.github.com')
+ end
end
def api_endpoint
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index d9a5af09f08..f357488ac61 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -16,8 +16,10 @@ module Gitlab
def can_do_action?(action)
return false unless can_access_git?
- @permission_cache ||= {}
- @permission_cache[action] ||= user.can?(action, project)
+ permission_cache[action] =
+ permission_cache.fetch(action) do
+ user.can?(action, project)
+ end
end
def cannot_do_action?(action)
@@ -88,6 +90,10 @@ module Gitlab
private
+ def permission_cache
+ @permission_cache ||= {}
+ end
+
def can_access_git?
user && user.can?(:access_git)
end
diff --git a/qa/README.md b/qa/README.md
index 8fa04e80825..3c1b61900d9 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -17,6 +17,17 @@ against any existing instance.
1. Along with GitLab Docker Images we also build and publish GitLab QA images.
1. GitLab QA project uses these images to execute integration tests.
+## Validating GitLab views / partials / selectors in merge requests
+
+We recently added a new CI job that is going to be triggered for every push
+event in CE and EE projects. The job is called `qa:selectors` and it will
+verify coupling between page objects implemented as a part of GitLab QA
+and corresponding views / partials / selectors in CE / EE.
+
+Whenever `qa:selectors` job fails in your merge request, you are supposed to
+fix [page objects](qa/page/README.md). You should also trigger end-to-end tests
+using `package-qa` manual action, to test if everything works fine.
+
## How can I use it?
You can use GitLab QA to exercise tests on any live instance! For example, the
diff --git a/rubocop/cop/gitlab/predicate_memoization.rb b/rubocop/cop/gitlab/predicate_memoization.rb
new file mode 100644
index 00000000000..3c25d61d087
--- /dev/null
+++ b/rubocop/cop/gitlab/predicate_memoization.rb
@@ -0,0 +1,39 @@
+module RuboCop
+ module Cop
+ module Gitlab
+ class PredicateMemoization < RuboCop::Cop::Cop
+ MSG = <<~EOL.freeze
+ Avoid using `@value ||= query` inside predicate methods in order to
+ properly memoize `false` or `nil` values.
+ https://docs.gitlab.com/ee/development/utilities.html#strongmemoize
+ EOL
+
+ def on_def(node)
+ return unless predicate_method?(node)
+
+ select_offenses(node).each do |offense|
+ add_offense(offense, location: :expression)
+ end
+ end
+
+ private
+
+ def predicate_method?(node)
+ node.method_name.to_s.end_with?('?')
+ end
+
+ def or_ivar_assignment?(or_assignment)
+ lhs = or_assignment.each_child_node.first
+
+ lhs.ivasgn_type?
+ end
+
+ def select_offenses(node)
+ node.each_descendant(:or_asgn).select do |or_assignment|
+ or_ivar_assignment?(or_assignment)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 57af87f7fb9..9110237c538 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,4 +1,5 @@
require_relative 'cop/gitlab/module_with_instance_variables'
+require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/line_break_around_conditional_block'
require_relative 'cop/migration/add_column'
diff --git a/spec/factories/redirect_routes.rb b/spec/factories/redirect_routes.rb
new file mode 100644
index 00000000000..c29c81c5df9
--- /dev/null
+++ b/spec/factories/redirect_routes.rb
@@ -0,0 +1,15 @@
+FactoryBot.define do
+ factory :redirect_route do
+ sequence(:path) { |n| "redirect#{n}" }
+ source factory: :group
+ permanent false
+
+ trait :permanent do
+ permanent true
+ end
+
+ trait :temporary do
+ permanent false
+ end
+ end
+end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 205900615c4..b2dbfcd0031 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -334,14 +334,14 @@ describe 'Issue Boards', :js do
wait_for_requests
page.within('.subscriptions') do
- click_button 'Subscribe'
+ find('.js-issuable-subscribe-button button:not(.is-checked)').click
wait_for_requests
- expect(page).to have_content('Unsubscribe')
+ expect(page).to have_css('.js-issuable-subscribe-button button.is-checked')
end
end
- it 'has "Unsubscribe" button when already subscribed' do
+ it 'has checked subscription toggle when already subscribed' do
create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
visit project_board_path(project, board)
wait_for_requests
@@ -350,10 +350,10 @@ describe 'Issue Boards', :js do
wait_for_requests
page.within('.subscriptions') do
- click_button 'Unsubscribe'
+ find('.js-issuable-subscribe-button button.is-checked').click
wait_for_requests
- expect(page).to have_content('Subscribe')
+ expect(page).to have_css('.js-issuable-subscribe-button button:not(.is-checked)')
end
end
end
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
index 4ca435491cb..f55eb5c6664 100644
--- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -13,20 +13,18 @@ describe 'User manages subscription', :js do
end
it 'toggles subscription' do
- subscribe_button = find('.js-issuable-subscribe-button')
+ page.within('.js-issuable-subscribe-button') do
+ expect(page).to have_css 'button:not(.is-checked)'
+ find('button:not(.is-checked)').click
- expect(subscribe_button).to have_content('Subscribe')
+ wait_for_requests
- click_on('Subscribe')
+ expect(page).to have_css 'button.is-checked'
+ find('button.is-checked').click
- wait_for_requests
+ wait_for_requests
- expect(subscribe_button).to have_content('Unsubscribe')
-
- click_on('Unsubscribe')
-
- wait_for_requests
-
- expect(subscribe_button).to have_content('Subscribe')
+ expect(page).to have_css 'button:not(.is-checked)'
+ end
end
end
diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/javascripts/commit_merge_requests_spec.js
new file mode 100644
index 00000000000..3466ef51ea8
--- /dev/null
+++ b/spec/javascripts/commit_merge_requests_spec.js
@@ -0,0 +1,60 @@
+import * as CommitMergeRequests from '~/commit_merge_requests';
+
+describe('CommitMergeRequests', () => {
+ describe('createContent', () => {
+ it('should return created content', () => {
+ const content1 = CommitMergeRequests.createContent([{ iid: 1, path: '/path1', title: 'foo' }, { iid: 2, path: '/path2', title: 'baz' }])[0];
+ expect(content1.tagName).toEqual('SPAN');
+ expect(content1.childElementCount).toEqual(4);
+
+ const content2 = CommitMergeRequests.createContent([])[0];
+ expect(content2.tagName).toEqual('SPAN');
+ expect(content2.childElementCount).toEqual(0);
+ expect(content2.innerText).toEqual('No related merge requests found');
+ });
+ });
+
+ describe('getHeaderText', () => {
+ it('should return header text', () => {
+ expect(CommitMergeRequests.getHeaderText(0, 1)).toEqual('1 merge request');
+ expect(CommitMergeRequests.getHeaderText(0, 2)).toEqual('2 merge requests');
+ expect(CommitMergeRequests.getHeaderText(1, 1)).toEqual(',');
+ expect(CommitMergeRequests.getHeaderText(1, 2)).toEqual(',');
+ });
+ });
+
+ describe('createHeader', () => {
+ it('should return created header', () => {
+ const header = CommitMergeRequests.createHeader(0, 1)[0];
+ expect(header.tagName).toEqual('SPAN');
+ expect(header.innerText).toEqual('1 merge request');
+ });
+ });
+
+ describe('createItem', () => {
+ it('should return created item', () => {
+ const item = CommitMergeRequests.createItem({ iid: 1, path: '/path', title: 'foo' })[0];
+ expect(item.tagName).toEqual('SPAN');
+ expect(item.childElementCount).toEqual(2);
+ expect(item.children[0].tagName).toEqual('A');
+ expect(item.children[1].tagName).toEqual('SPAN');
+ });
+ });
+
+ describe('createLink', () => {
+ it('should return created link', () => {
+ const link = CommitMergeRequests.createLink({ iid: 1, path: '/path', title: 'foo' })[0];
+ expect(link.tagName).toEqual('A');
+ expect(link.href).toMatch(/\/path$/);
+ expect(link.innerText).toEqual('!1');
+ });
+ });
+
+ describe('createTitle', () => {
+ it('should return created title', () => {
+ const title = CommitMergeRequests.createTitle({ iid: 1, path: '/path', title: 'foo' })[0];
+ expect(title.tagName).toEqual('SPAN');
+ expect(title.innerText).toEqual('foo');
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index 83395ea451e..a9df0418d5d 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -31,6 +31,7 @@ describe('Job details header', () => {
email: 'foo@bar.com',
avatar_url: 'link',
},
+ started: '2018-01-08T09:48:27.319Z',
new_issue_path: 'path',
},
isLoading: false,
@@ -43,15 +44,32 @@ describe('Job details header', () => {
vm.$destroy();
});
- it('should render provided job information', () => {
- expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
- ).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
+ describe('triggered job', () => {
+ beforeEach(() => {
+ vm = mountComponent(HeaderComponent, props);
+ });
+
+ it('should render provided job information', () => {
+ expect(
+ vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ ).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
+ });
+
+ it('should render new issue link', () => {
+ expect(
+ vm.$el.querySelector('.js-new-issue').getAttribute('href'),
+ ).toEqual(props.job.new_issue_path);
+ });
});
- it('should render new issue link', () => {
- expect(
- vm.$el.querySelector('.js-new-issue').getAttribute('href'),
- ).toEqual(props.job.new_issue_path);
+ describe('created job', () => {
+ it('should render created key', () => {
+ props.job.started = false;
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(
+ vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ ).toEqual('failed Job #123 created 3 weeks ago by Foo');
+ });
});
});
diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
index 20e352dd8bd..104d03377b6 100644
--- a/spec/javascripts/notes/components/comment_form_spec.js
+++ b/spec/javascripts/notes/components/comment_form_spec.js
@@ -139,13 +139,21 @@ describe('issue_comment_form component', () => {
});
describe('event enter', () => {
- it('should save note when cmd/ctrl+enter is pressed', () => {
+ it('should save note when cmd+enter is pressed', () => {
spyOn(vm, 'handleSave').and.callThrough();
vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true));
expect(vm.handleSave).toHaveBeenCalled();
});
+
+ it('should save note when ctrl+enter is pressed', () => {
+ spyOn(vm, 'handleSave').and.callThrough();
+ vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
+ vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true));
+
+ expect(vm.handleSave).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js
index 86e9e2a32a9..f841a408d09 100644
--- a/spec/javascripts/notes/components/note_form_spec.js
+++ b/spec/javascripts/notes/components/note_form_spec.js
@@ -69,13 +69,20 @@ describe('issue_note_form component', () => {
});
describe('enter', () => {
- it('should submit note', () => {
+ it('should save note when cmd+enter is pressed', () => {
spyOn(vm, 'handleUpdate').and.callThrough();
vm.$el.querySelector('textarea').value = 'Foo';
vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true));
expect(vm.handleUpdate).toHaveBeenCalled();
});
+ it('should save note when ctrl+enter is pressed', () => {
+ spyOn(vm, 'handleUpdate').and.callThrough();
+ vm.$el.querySelector('textarea').value = 'Foo';
+ vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, false, true));
+
+ expect(vm.handleUpdate).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index 9b33dd02fb9..79db05f04ed 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -20,23 +20,23 @@ describe('Subscriptions', function () {
subscribed: undefined,
});
- expect(vm.$refs.loadingButton.loading).toBe(true);
- expect(vm.$refs.loadingButton.label).toBeUndefined();
+ expect(vm.$refs.toggleButton.isLoading).toBe(true);
+ expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-loading');
});
- it('has "Subscribe" text when currently not subscribed', () => {
+ it('is toggled "off" when currently not subscribed', () => {
vm = mountComponent(Subscriptions, {
subscribed: false,
});
- expect(vm.$refs.loadingButton.label).toBe('Subscribe');
+ expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass('is-checked');
});
- it('has "Unsubscribe" text when currently not subscribed', () => {
+ it('is toggled "on" when currently subscribed', () => {
vm = mountComponent(Subscriptions, {
subscribed: true,
});
- expect(vm.$refs.loadingButton.label).toBe('Unsubscribe');
+ expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked');
});
});
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index f38f0776303..7e17457ce70 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -8,7 +8,8 @@ describe Banzai::Filter::RelativeLinkFilter do
group: group,
project_wiki: project_wiki,
ref: ref,
- requested_path: requested_path
+ requested_path: requested_path,
+ only_path: only_path
})
described_class.call(doc, contexts)
@@ -37,6 +38,7 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:commit) { project.commit(ref) }
let(:project_wiki) { nil }
let(:requested_path) { '/' }
+ let(:only_path) { true }
shared_examples :preserve_unchanged do
it 'does not modify any relative URL in anchor' do
@@ -240,26 +242,35 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:commit) { nil }
let(:ref) { nil }
let(:requested_path) { nil }
+ let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
+ let(:relative_path) { "/#{project.full_path}#{upload_path}" }
context 'to a project upload' do
+ context 'with an absolute URL' do
+ let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
+ let(:only_path) { false }
+
+ it 'rewrites the link correctly' do
+ doc = filter(link(upload_path))
+
+ expect(doc.at_css('a')['href']).to eq(absolute_path)
+ end
+ end
+
it 'rebuilds relative URL for a link' do
- doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('a')['href'])
- .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ doc = filter(link(upload_path))
+ expect(doc.at_css('a')['href']).to eq(relative_path)
- doc = filter(nested(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')))
- expect(doc.at_css('a')['href'])
- .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ doc = filter(nested(link(upload_path)))
+ expect(doc.at_css('a')['href']).to eq(relative_path)
end
it 'rebuilds relative URL for an image' do
- doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('img')['src'])
- .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ doc = filter(image(upload_path))
+ expect(doc.at_css('img')['src']).to eq(relative_path)
- doc = filter(nested(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')))
- expect(doc.at_css('img')['src'])
- .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+ doc = filter(nested(image(upload_path)))
+ expect(doc.at_css('img')['src']).to eq(relative_path)
end
it 'does not modify absolute URL' do
@@ -288,6 +299,17 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:project) { nil }
let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
+ context 'with an absolute URL' do
+ let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
+ let(:only_path) { false }
+
+ it 'rewrites the link correctly' do
+ doc = filter(upload_link)
+
+ expect(doc.at_css('a')['href']).to eq(absolute_path)
+ end
+ end
+
it 'rewrites the link correctly' do
doc = filter(upload_link)
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 38829773599..f2efcd9d0e9 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -151,11 +151,11 @@ describe CommitRange do
.with(commit1, user)
.and_return(true)
- expect(commit1.has_been_reverted?(user, issue)).to eq(true)
+ expect(commit1.has_been_reverted?(user, issue.notes_with_associations)).to eq(true)
end
- it 'returns false a commit has not been reverted' do
- expect(commit1.has_been_reverted?(user, issue)).to eq(false)
+ it 'returns false if the commit has not been reverted' do
+ expect(commit1.has_been_reverted?(user, issue.notes_with_associations)).to eq(false)
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 817254c7d1e..d3826417762 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -513,4 +513,17 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
+
+ describe '#merge_requests' do
+ let!(:project) { create(:project, :repository) }
+ let!(:merge_request1) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+ let!(:merge_request2) { create(:merge_request, source_project: project, source_branch: 'merged-target', target_branch: 'feature') }
+ let(:commit1) { merge_request1.merge_request_diff.commits.last }
+ let(:commit2) { merge_request1.merge_request_diff.commits.first }
+
+ it 'returns merge_requests that introduced that commit' do
+ expect(commit1.merge_requests).to eq([merge_request1, merge_request2])
+ expect(commit2.merge_requests).to eq([merge_request1])
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index d556004eccf..b4249d72fc8 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -15,6 +15,28 @@ describe MergeRequestDiff do
it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
end
+ describe '.by_commit_sha' do
+ subject(:by_commit_sha) { described_class.by_commit_sha(sha) }
+
+ let!(:merge_request) { create(:merge_request, :with_diffs) }
+
+ context 'with sha contained in' do
+ let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ it 'returns merge request diffs' do
+ expect(by_commit_sha).to eq([merge_request.merge_request_diff])
+ end
+ end
+
+ context 'with sha not contained in' do
+ let(:sha) { 'b83d6e3' }
+
+ it 'returns empty result' do
+ expect(by_commit_sha).to be_empty
+ end
+ end
+ end
+
describe '#latest' do
let!(:mr) { create(:merge_request, :with_diffs) }
let!(:first_diff) { mr.merge_request_diff }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8ff82c4f791..c76f32b3989 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -87,6 +87,39 @@ describe MergeRequest do
it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
end
+ describe '.by_commit_sha' do
+ subject(:by_commit_sha) { described_class.by_commit_sha(sha) }
+
+ let!(:merge_request) { create(:merge_request, :with_diffs) }
+
+ context 'with sha contained in latest merge request diff' do
+ let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ it 'returns merge requests' do
+ expect(by_commit_sha).to eq([merge_request])
+ end
+ end
+
+ context 'with sha contained not in latest merge request diff' do
+ let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ it 'returns empty requests' do
+ latest_merge_request_diff = merge_request.merge_request_diffs.create
+ latest_merge_request_diff.merge_request_diff_commits.where(sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0').delete_all
+
+ expect(by_commit_sha).to be_empty
+ end
+ end
+
+ context 'with sha not contained in' do
+ let(:sha) { 'b83d6e3' }
+
+ it 'returns empty result' do
+ expect(by_commit_sha).to be_empty
+ end
+ end
+ end
+
describe '.in_projects' do
it 'returns the merge requests for a set of projects' do
expect(described_class.in_projects(Project.all)).to eq([subject])
@@ -1030,6 +1063,83 @@ describe MergeRequest do
end
end
+ describe '#can_be_reverted?' do
+ context 'when there is no merged_at for the MR' do
+ before do
+ subject.metrics.update!(merged_at: nil)
+ end
+
+ it 'returns false' do
+ expect(subject.can_be_reverted?(nil)).to be_falsey
+ end
+ end
+
+ context 'when there is no merge_commit for the MR' do
+ before do
+ subject.metrics.update!(merged_at: Time.now.utc)
+ end
+
+ it 'returns false' do
+ expect(subject.can_be_reverted?(nil)).to be_falsey
+ end
+ end
+
+ context 'when the MR has been merged' do
+ before do
+ MergeRequests::MergeService
+ .new(subject.target_project, subject.author)
+ .execute(subject)
+ end
+
+ context 'when there is no revert commit' do
+ it 'returns true' do
+ expect(subject.can_be_reverted?(nil)).to be_truthy
+ end
+ end
+
+ context 'when there is a revert commit' do
+ let(:current_user) { subject.author }
+ let(:branch) { subject.target_branch }
+ let(:project) { subject.target_project }
+
+ let(:revert_commit_id) do
+ params = {
+ commit: subject.merge_commit,
+ branch_name: branch,
+ start_branch: branch
+ }
+
+ Commits::RevertService.new(project, current_user, params).execute[:result]
+ end
+
+ before do
+ project.add_master(current_user)
+
+ ProcessCommitWorker.new.perform(project.id,
+ current_user.id,
+ project.commit(revert_commit_id).to_hash,
+ project.default_branch == branch)
+ end
+
+ context 'when the revert commit is mentioned in a note after the MR was merged' do
+ it 'returns false' do
+ expect(subject.can_be_reverted?(current_user)).to be_falsey
+ end
+ end
+
+ context 'when the revert commit is mentioned in a note before the MR was merged' do
+ before do
+ subject.notes.last.update!(created_at: subject.metrics.merged_at - 1.second)
+ end
+
+ it 'returns true' do
+ expect(subject.can_be_reverted?(current_user)).to be_truthy
+ end
+ end
+ end
+ end
+ end
+
describe '#participants' do
let(:project) { create(:project, :public) }
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index ddad6862a63..8a3b1034f3c 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -16,6 +16,66 @@ describe Route do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path).case_insensitive }
+
+ describe '#ensure_permanent_paths' do
+ context 'when the route is not yet persisted' do
+ let(:new_route) { described_class.new(path: 'foo', source: build(:group)) }
+
+ context 'when permanent conflicting redirects exist' do
+ it 'is invalid' do
+ redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz')
+ redirect.save!(validate: false)
+
+ expect(new_route.valid?).to be_falsey
+ expect(new_route.errors.first[1]).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context 'when no permanent conflicting redirects exist' do
+ it 'is valid' do
+ expect(new_route.valid?).to be_truthy
+ end
+ end
+ end
+
+ context 'when path has changed' do
+ before do
+ route.path = 'foo'
+ end
+
+ context 'when permanent conflicting redirects exist' do
+ it 'is invalid' do
+ redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz')
+ redirect.save!(validate: false)
+
+ expect(route.valid?).to be_falsey
+ expect(route.errors.first[1]).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context 'when no permanent conflicting redirects exist' do
+ it 'is valid' do
+ expect(route.valid?).to be_truthy
+ end
+ end
+ end
+
+ context 'when path has not changed' do
+ context 'when permanent conflicting redirects exist' do
+ it 'is valid' do
+ redirect = build(:redirect_route, :permanent, path: 'git_lab/foo/bar')
+ redirect.save!(validate: false)
+
+ expect(route.valid?).to be_truthy
+ end
+ end
+ context 'when no permanent conflicting redirects exist' do
+ it 'is valid' do
+ expect(route.valid?).to be_truthy
+ end
+ end
+ end
+ end
end
describe 'callbacks' do
diff --git a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
new file mode 100644
index 00000000000..21fc4584654
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
@@ -0,0 +1,100 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/predicate_memoization'
+
+describe RuboCop::Cop::Gitlab::PredicateMemoization do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples('registering offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when a predicate method is memoizing via ivar' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ shared_examples('not registering offense') do
+ it 'does not register offenses' do
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'when source is a predicate method memoizing via ivar' do
+ it_behaves_like 'registering offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ class C
+ def really?
+ @really ||= true
+ end
+ end
+ RUBY
+ end
+ end
+
+ it_behaves_like 'registering offense', offending_lines: [4] do
+ let(:source) do
+ <<~RUBY
+ class C
+ def really?
+ value = true
+ @really ||= value
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a predicate method using ivar with assignment' do
+ it_behaves_like 'not registering offense' do
+ let(:source) do
+ <<~RUBY
+ class C
+ def really?
+ @really = true
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a predicate method using local with ||=' do
+ it_behaves_like 'not registering offense' do
+ let(:source) do
+ <<~RUBY
+ class C
+ def really?
+ really ||= true
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a regular method memoizing via ivar' do
+ it_behaves_like 'not registering offense' do
+ let(:source) do
+ <<~RUBY
+ class C
+ def really
+ @really ||= true
+ end
+ end
+ RUBY
+ end
+ end
+ end
+end