diff options
68 files changed, 774 insertions, 367 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index aec734870d6..3e58d2a867e 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -1,3 +1,4 @@ +<!--- Please read this! Before opening a new issue, make sure to search for keywords in the issues @@ -14,10 +15,7 @@ For the Enterprise Edition issue tracker: - https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug and verify the issue you're about to submit isn't a duplicate. - -Please remove this notice if you're confident your issue isn't a duplicate. - ------- +---> ### Summary diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index add24303e7b..9501e35b178 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -6,7 +6,7 @@ import { __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; -import assignees from '../../sidebar/components/assignees/assignees'; +import assignees from '../../sidebar/components/assignees/assignees.vue'; import DueDateSelectors from '../../due_date_select'; import './sidebar/remove_issue'; import IssuableContext from '../../issuable_context'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 034f2923b3b..46d89c825f9 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store'; Vue.use(Translate); -$(() => { +export default () => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; - gl.cycleAnalyticsApp = new Vue({ + new Vue({ // eslint-disable-line no-new el: '#cycle-analytics', name: 'CycleAnalytics', components: { @@ -132,4 +132,4 @@ $(() => { }, }, }); -}); +}; diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index dd947f66969..9d933b8891d 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,8 +1,8 @@ <script> import { mapGetters, mapState, mapActions } from 'vuex'; + import icon from '~/vue_shared/components/icon.vue'; + import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import repoCommitSection from './repo_commit_section.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import panelResizer from '../../vue_shared/components/panel_resizer.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue index af2f7341a91..2fbff2bd789 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -1,6 +1,6 @@ <script> +import icon from '~/vue_shared/components/icon.vue'; import repoTree from './ide_repo_tree.vue'; -import icon from '../../vue_shared/components/icon.vue'; import newDropdown from './new_dropdown/index.vue'; export default { diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue index ed49a0e72a2..32bf7175c88 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -1,6 +1,6 @@ <script> +import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import branchesTree from './ide_project_branches_tree.vue'; -import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index 4651e345d75..4a324264992 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -1,8 +1,8 @@ <script> import { mapState } from 'vuex'; +import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import repoPreviousDirectory from './repo_prev_directory.vue'; import repoFile from './repo_file.vue'; -import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import { treeList } from '../stores/utils'; export default { diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index a68f8ce0169..18b5059a17f 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,9 +1,9 @@ <script> import { mapState, mapActions } from 'vuex'; + import icon from '~/vue_shared/components/icon.vue'; + import panelResizer from '~/vue_shared/components/panel_resizer.vue'; + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import projectTree from './ide_project_tree.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import panelResizer from '../../vue_shared/components/panel_resizer.vue'; - import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index e48c446c4a4..97ae64b206d 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,8 +1,8 @@ <script> import { mapState } from 'vuex'; - import icon from '../../vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import timeAgoMixin from '../../vue_shared/mixins/timeago'; + import icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import timeAgoMixin from '~/vue_shared/mixins/timeago'; export default { components: { diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue index 56e31256132..1e8d5bb6453 100644 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ b/app/assets/javascripts/ide/components/new_branch_form.vue @@ -1,7 +1,7 @@ <script> import { mapState, mapActions } from 'vuex'; - import flash, { hideFlash } from '../../flash'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import flash, { hideFlash } from '~/flash'; + import loadingIcon from '~/vue_shared/components/loading_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 96b1bb78c1d..37f2cf30a29 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -1,8 +1,8 @@ <script> import { mapGetters, mapState, mapActions } from 'vuex'; -import tooltip from '../../vue_shared/directives/tooltip'; -import icon from '../../vue_shared/components/icon.vue'; -import modal from '../../vue_shared/components/modal.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import icon from '~/vue_shared/components/icon.vue'; +import modal from '~/vue_shared/components/modal.vue'; import commitFilesList from './commit_sidebar/list.vue'; export default { diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index c43e9163340..fe4320731d9 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -1,6 +1,6 @@ <script> import { mapGetters, mapActions, mapState } from 'vuex'; -import modal from '../../vue_shared/components/modal.vue'; +import modal from '~/vue_shared/components/modal.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f99228012f4..f31cc12339b 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,7 +1,7 @@ <script> /* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; -import flash from '../../flash'; +import flash from '~/flash'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 3aeb6f0b28f..79af8c0b0c7 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -1,6 +1,6 @@ <script> import { mapState } from 'vuex'; - import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue index e47270a9855..a216269e292 100644 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ b/app/assets/javascripts/ide/components/repo_preview.vue @@ -1,7 +1,7 @@ <script> import { mapGetters } from 'vuex'; - import LineHighlighter from '../../line_highlighter'; - import syntaxHighlight from '../../syntax_highlight'; + import LineHighlighter from '~/line_highlighter'; + import syntaxHighlight from '~/syntax_highlight'; export default { computed: { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index d007d0ae78f..2c690b1f635 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import { visitUrl } from '../../lib/utils/url_utility'; -import flash from '../../flash'; +import { visitUrl } from '~/lib/utils/url_utility'; +import flash from '~/flash'; import service from '../services'; import * as types from './mutation_types'; import { stripHtml } from '../../lib/utils/text_utility'; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 89a246f56cf..9b46bbf83da 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -21,7 +21,7 @@ export default class LabelsSelect { } $els.each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); $toggleText = $dropdown.find('.dropdown-toggle-text'); @@ -53,13 +53,6 @@ export default class LabelsSelect { .map(function () { return this.value; }).get(); - if (issueUpdateURL != null) { - issueURLSplit = issueUpdateURL.split('/'); - } - if (issueUpdateURL) { - labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); - labelNoneHTMLTemplate = '<span class="no-value">None</span>'; - } const handleClick = options.handleClick; $sidebarLabelTooltip.tooltip(); @@ -91,14 +84,17 @@ export default class LabelsSelect { $loading.fadeOut(); $dropdown.trigger('loaded.gl.dropdown'); $selectbox.hide(); - data.issueURLSplit = issueURLSplit; + data.issueUpdateURL = issueUpdateURL; labelCount = 0; - if (data.labels.length) { - template = labelHTMLTemplate(data); + if (data.labels.length && issueUpdateURL) { + template = LabelsSelect.getLabelTemplate({ + labels: data.labels, + issueUpdateURL, + }); labelCount = data.labels.length; } else { - template = labelNoneHTMLTemplate; + template = '<span class="no-value">None</span>'; } $value.removeAttr('style').html(template); $sidebarCollapsedValue.text(labelCount); @@ -418,6 +414,26 @@ export default class LabelsSelect { this.bindEvents(); } + static getLabelTemplate(tplData) { + // We could use ES6 template string here + // and properly indent markup for readability + // but that also introduces unintended white-space + // so best approach is to use traditional way of + // concatenation + // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays + const tpl = _.template([ + '<% _.each(labels, function(label){ %>', + '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', + '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<%- label.title %>', + '</span>', + '</a>', + '<% }); %>', + ].join('')); + + return tpl(tplData); + } + bindEvents() { return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); } diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index b3f6a72fdcb..42f7460ad55 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -2,9 +2,9 @@ import { visitUrl } from '~/lib/utils/url_utility'; import UsersSelect from '~/users_select'; import { isMetaClick } from '~/lib/utils/common_utils'; -import { __ } from '../../../../locale'; -import flash from '../../../../flash'; -import axios from '../../../../lib/utils/axios_utils'; +import { __ } from '~/locale'; +import flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; export default class Todos { constructor() { diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js new file mode 100644 index 00000000000..df58e9dd072 --- /dev/null +++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js @@ -0,0 +1,3 @@ +import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle'; + +document.addEventListener('DOMContentLoaded', initCycleAnalytics); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js deleted file mode 100644 index 643877b9d47..00000000000 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.js +++ /dev/null @@ -1,224 +0,0 @@ -export default { - name: 'Assignees', - data() { - return { - defaultRenderCount: 5, - defaultMaxCounter: 99, - showLess: true, - }; - }, - props: { - rootPath: { - type: String, - required: true, - }, - users: { - type: Array, - required: true, - }, - editable: { - type: Boolean, - required: true, - }, - }, - computed: { - firstUser() { - return this.users[0]; - }, - hasMoreThanTwoAssignees() { - return this.users.length > 2; - }, - hasMoreThanOneAssignee() { - return this.users.length > 1; - }, - hasAssignees() { - return this.users.length > 0; - }, - hasNoUsers() { - return !this.users.length; - }, - hasOneUser() { - return this.users.length === 1; - }, - renderShowMoreSection() { - return this.users.length > this.defaultRenderCount; - }, - numberOfHiddenAssignees() { - return this.users.length - this.defaultRenderCount; - }, - isHiddenAssignees() { - return this.numberOfHiddenAssignees > 0; - }, - hiddenAssigneesLabel() { - return `+ ${this.numberOfHiddenAssignees} more`; - }, - collapsedTooltipTitle() { - const maxRender = Math.min(this.defaultRenderCount, this.users.length); - const renderUsers = this.users.slice(0, maxRender); - const names = renderUsers.map(u => u.name); - - if (this.users.length > maxRender) { - names.push(`+ ${this.users.length - maxRender} more`); - } - - return names.join(', '); - }, - sidebarAvatarCounter() { - let counter = `+${this.users.length - 1}`; - - if (this.users.length > this.defaultMaxCounter) { - counter = `${this.defaultMaxCounter}+`; - } - - return counter; - }, - }, - methods: { - assignSelf() { - this.$emit('assign-self'); - }, - toggleShowLess() { - this.showLess = !this.showLess; - }, - renderAssignee(index) { - return !this.showLess || (index < this.defaultRenderCount && this.showLess); - }, - avatarUrl(user) { - return user.avatar || user.avatar_url || gon.default_avatar_url; - }, - assigneeUrl(user) { - return `${this.rootPath}${user.username}`; - }, - assigneeAlt(user) { - return `${user.name}'s avatar`; - }, - assigneeUsername(user) { - return `@${user.username}`; - }, - shouldRenderCollapsedAssignee(index) { - const firstTwo = this.users.length <= 2 && index <= 2; - - return index === 0 || firstTwo; - }, - }, - template: ` - <div> - <div - class="sidebar-collapsed-icon sidebar-collapsed-user" - :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" - data-container="body" - data-placement="left" - :title="collapsedTooltipTitle" - > - <i - v-if="hasNoUsers" - aria-label="No Assignee" - class="fa fa-user" - /> - <button - type="button" - class="btn-link" - v-for="(user, index) in users" - v-if="shouldRenderCollapsedAssignee(index)" - > - <img - width="24" - class="avatar avatar-inline s24" - :alt="assigneeAlt(user)" - :src="avatarUrl(user)" - /> - <span class="author"> - {{ user.name }} - </span> - </button> - <button - v-if="hasMoreThanTwoAssignees" - class="btn-link" - type="button" - > - <span - class="avatar-counter sidebar-avatar-counter" - > - {{ sidebarAvatarCounter }} - </span> - </button> - </div> - <div class="value hide-collapsed"> - <template v-if="hasNoUsers"> - <span class="assign-yourself no-value"> - No assignee - <template v-if="editable"> - - - <button - type="button" - class="btn-link" - @click="assignSelf" - > - assign yourself - </button> - </template> - </span> - </template> - <template v-else-if="hasOneUser"> - <a - class="author_link bold" - :href="assigneeUrl(firstUser)" - > - <img - width="32" - class="avatar avatar-inline s32" - :alt="assigneeAlt(firstUser)" - :src="avatarUrl(firstUser)" - /> - <span class="author"> - {{ firstUser.name }} - </span> - <span class="username"> - {{ assigneeUsername(firstUser) }} - </span> - </a> - </template> - <template v-else> - <div class="user-list"> - <div - class="user-item" - v-for="(user, index) in users" - v-if="renderAssignee(index)" - > - <a - class="user-link has-tooltip" - data-placement="bottom" - :href="assigneeUrl(user)" - :data-title="user.name" - > - <img - width="32" - class="avatar avatar-inline s32" - :alt="assigneeAlt(user)" - :src="avatarUrl(user)" - /> - </a> - </div> - </div> - <div - v-if="renderShowMoreSection" - class="user-list-more" - > - <button - type="button" - class="btn-link" - @click="toggleShowLess" - > - <template v-if="showLess"> - {{ hiddenAssigneesLabel }} - </template> - <template v-else> - - show less - </template> - </button> - </div> - </template> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue new file mode 100644 index 00000000000..1e7f46454bf --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -0,0 +1,232 @@ +<script> +export default { + name: 'Assignees', + props: { + rootPath: { + type: String, + required: true, + }, + users: { + type: Array, + required: true, + }, + editable: { + type: Boolean, + required: true, + }, + }, + data() { + return { + defaultRenderCount: 5, + defaultMaxCounter: 99, + showLess: true, + }; + }, + computed: { + firstUser() { + return this.users[0]; + }, + hasMoreThanTwoAssignees() { + return this.users.length > 2; + }, + hasMoreThanOneAssignee() { + return this.users.length > 1; + }, + hasAssignees() { + return this.users.length > 0; + }, + hasNoUsers() { + return !this.users.length; + }, + hasOneUser() { + return this.users.length === 1; + }, + renderShowMoreSection() { + return this.users.length > this.defaultRenderCount; + }, + numberOfHiddenAssignees() { + return this.users.length - this.defaultRenderCount; + }, + isHiddenAssignees() { + return this.numberOfHiddenAssignees > 0; + }, + hiddenAssigneesLabel() { + return `+ ${this.numberOfHiddenAssignees} more`; + }, + collapsedTooltipTitle() { + const maxRender = Math.min(this.defaultRenderCount, this.users.length); + const renderUsers = this.users.slice(0, maxRender); + const names = renderUsers.map(u => u.name); + + if (this.users.length > maxRender) { + names.push(`+ ${this.users.length - maxRender} more`); + } + + return names.join(', '); + }, + sidebarAvatarCounter() { + let counter = `+${this.users.length - 1}`; + + if (this.users.length > this.defaultMaxCounter) { + counter = `${this.defaultMaxCounter}+`; + } + + return counter; + }, + }, + methods: { + assignSelf() { + this.$emit('assign-self'); + }, + toggleShowLess() { + this.showLess = !this.showLess; + }, + renderAssignee(index) { + return !this.showLess || (index < this.defaultRenderCount && this.showLess); + }, + avatarUrl(user) { + return user.avatar || user.avatar_url || gon.default_avatar_url; + }, + assigneeUrl(user) { + return `${this.rootPath}${user.username}`; + }, + assigneeAlt(user) { + return `${user.name}'s avatar`; + }, + assigneeUsername(user) { + return `@${user.username}`; + }, + shouldRenderCollapsedAssignee(index) { + const firstTwo = this.users.length <= 2 && index <= 2; + + return index === 0 || firstTwo; + }, + }, +}; +</script> + +<template> + <div> + <div + class="sidebar-collapsed-icon sidebar-collapsed-user" + :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" + data-container="body" + data-placement="left" + :title="collapsedTooltipTitle" + > + <i + v-if="hasNoUsers" + aria-label="No Assignee" + class="fa fa-user" + > + </i> + <button + type="button" + class="btn-link" + v-for="(user, index) in users" + v-if="shouldRenderCollapsedAssignee(index)" + :key="user.id" + > + <img + width="24" + class="avatar avatar-inline s24" + :alt="assigneeAlt(user)" + :src="avatarUrl(user)" + /> + <span class="author"> + {{ user.name }} + </span> + </button> + <button + v-if="hasMoreThanTwoAssignees" + class="btn-link" + type="button" + > + <span + class="avatar-counter sidebar-avatar-counter" + > + {{ sidebarAvatarCounter }} + </span> + </button> + </div> + <div class="value hide-collapsed"> + <template v-if="hasNoUsers"> + <span class="assign-yourself no-value"> + No assignee + <template v-if="editable"> + - + <button + type="button" + class="btn-link" + @click="assignSelf" + > + assign yourself + </button> + </template> + </span> + </template> + <template v-else-if="hasOneUser"> + <a + class="author_link bold" + :href="assigneeUrl(firstUser)" + > + <img + width="32" + class="avatar avatar-inline s32" + :alt="assigneeAlt(firstUser)" + :src="avatarUrl(firstUser)" + /> + <span class="author"> + {{ firstUser.name }} + </span> + <span class="username"> + {{ assigneeUsername(firstUser) }} + </span> + </a> + </template> + <template v-else> + <div class="user-list"> + <div + class="user-item" + v-for="(user, index) in users" + v-if="renderAssignee(index)" + :key="user.id" + > + <a + class="user-link has-tooltip" + data-container="body" + data-placement="bottom" + :href="assigneeUrl(user)" + :data-title="user.name" + > + <img + width="32" + class="avatar avatar-inline s32" + :alt="assigneeAlt(user)" + :src="avatarUrl(user)" + /> + </a> + </div> + </div> + <div + v-if="renderShowMoreSection" + class="user-list-more" + > + <button + type="button" + class="btn-link" + @click="toggleShowLess" + > + <template v-if="showLess"> + {{ hiddenAssigneesLabel }} + </template> + <template v-else> + - show less + </template> + </button> + </div> + </template> + </div> + </div> +</template> + diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index 9e47039d920..8269fe1281d 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -1,6 +1,6 @@ import Flash from '../../../flash'; import AssigneeTitle from './assignee_title'; -import Assignees from './assignees'; +import Assignees from './assignees.vue'; import Store from '../../stores/sidebar_store'; import eventHub from '../../event_hub'; @@ -28,8 +28,8 @@ export default { }, }, components: { - 'assignee-title': AssigneeTitle, - assignees: Assignees, + AssigneeTitle, + Assignees, }, methods: { assignSelf() { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0cf67734237..4c9732c26d9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -103,6 +103,7 @@ .issuable-show-labels { a { margin-bottom: 5px; + margin-right: 5px; display: inline-block; .color-label { @@ -116,6 +117,12 @@ } &.has-labels { + // this font size is a fix to + // prevent unintended spacing between labels + // which shows up when rendering markup has white-space + // characters present. + // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3 + font-size: 0; margin-bottom: -5px; } } diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index b71f1e5fef4..4856be61e88 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController before_action :require_pages_enabled! before_action :authorize_update_pages!, except: [:show] - before_action :domain, only: [:show, :destroy, :verify] + before_action :domain, except: [:new, :create] def show end @@ -24,8 +24,11 @@ class Projects::PagesDomainsController < Projects::ApplicationController redirect_to project_pages_domain_path(@project, @domain) end + def edit + end + def create - @domain = @project.pages_domains.create(pages_domain_params) + @domain = @project.pages_domains.create(create_params) if @domain.valid? redirect_to project_pages_domain_path(@project, @domain) @@ -34,6 +37,16 @@ class Projects::PagesDomainsController < Projects::ApplicationController end end + def update + if @domain.update(update_params) + redirect_to project_pages_path(@project), + status: 302, + notice: 'Domain was updated' + else + render 'edit' + end + end + def destroy @domain.destroy @@ -49,12 +62,12 @@ class Projects::PagesDomainsController < Projects::ApplicationController private - def pages_domain_params - params.require(:pages_domain).permit( - :certificate, - :key, - :domain - ) + def create_params + params.require(:pages_domain).permit(:key, :certificate, :domain) + end + + def update_params + params.require(:pages_domain).permit(:key, :certificate) end def domain diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 13c784bea0d..609620a62bb 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -49,7 +49,7 @@ module Ci ref_protected: 1 } - cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at + cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address # Searches for runners matching the given query. # @@ -157,7 +157,7 @@ module Ci end def update_cached_info(values) - values = values&.slice(:version, :revision, :platform, :architecture) || {} + values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {} values[:contacted_at] = Time.now cache_attributes(values) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 0e235a6d2a0..379a8068023 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -15,6 +15,8 @@ module Projects return error("Could not set the default branch") unless project.change_head(params[:default_branch]) end + ensure_wiki_exists if enabling_wiki? + if project.update_attributes(params.except(:default_branch)) if project.previous_changes.include?('path') project.rename_repo @@ -52,5 +54,18 @@ module Projects project.repository.exists? && new_branch && new_branch != project.default_branch end + + def enabling_wiki? + return false if @project.wiki_enabled? + + params[:project_feature_attributes][:wiki_access_level].to_i > ProjectFeature::DISABLED + end + + def ensure_wiki_exists + ProjectWiki.new(project, project.owner).wiki + rescue ProjectWiki::CouldNotCreateWikiError + log_error("Could not create wiki for #{project.full_name}") + Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki') + end end end diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 140688b52d3..e1cee584929 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -17,6 +17,8 @@ %td = runner.version %td + = runner.ip_address + %td - if runner.shared? n/a - else diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index abec3607cab..9f13dbbbd82 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -60,6 +60,7 @@ %th Runner token %th Description %th Version + %th IP Address %th Projects %th Jobs %th Tags diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index d98e0564da4..02395b6eb9b 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -2,7 +2,6 @@ - page_title "Cycle Analytics" - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('cycle_analytics') #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index ca1b41b140a..d81b07832bb 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -1,34 +1,30 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| - - if @domain.errors.any? - #error_explanation - .alert.alert-danger - - @domain.errors.full_messages.each do |msg| - %p= msg +- if @domain.errors.any? + #error_explanation + .alert.alert-danger + - @domain.errors.full_messages.each do |msg| + %p= msg +.form-group + = f.label :domain, class: 'control-label' do + Domain + .col-sm-10 + = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted? + +- if Gitlab.config.pages.external_https .form-group - = f.label :domain, class: 'control-label' do - Domain + = f.label :certificate, class: 'control-label' do + Certificate (PEM) .col-sm-10 - = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' - - - if Gitlab.config.pages.external_https - .form-group - = f.label :certificate, class: 'control-label' do - Certificate (PEM) - .col-sm-10 - = f.text_area :certificate, rows: 5, class: 'form-control' - %span.help-inline Upload a certificate for your domain with all intermediates - - .form-group - = f.label :key, class: 'control-label' do - Key (PEM) - .col-sm-10 - = f.text_area :key, rows: 5, class: 'form-control' - %span.help-inline Upload a private key for your certificate - - else - .nothing-here-block - Support for custom certificates is disabled. - Ask your system's administrator to enable it. + = f.text_area :certificate, rows: 5, class: 'form-control' + %span.help-inline Upload a certificate for your domain with all intermediates - .form-actions - = f.submit 'Create New Domain', class: "btn btn-save" + .form-group + = f.label :key, class: 'control-label' do + Key (PEM) + .col-sm-10 + = f.text_area :key, rows: 5, class: 'form-control' + %span.help-inline Upload a private key for your certificate +- else + .nothing-here-block + Support for custom certificates is disabled. + Ask your system's administrator to enable it. diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml new file mode 100644 index 00000000000..5645a4604bf --- /dev/null +++ b/app/views/projects/pages_domains/edit.html.haml @@ -0,0 +1,11 @@ +- add_to_breadcrumbs "Pages", project_pages_path(@project) +- breadcrumb_title @domain.domain +- page_title @domain.domain +%h3.page_title + = @domain.domain +%hr.clearfix +%div + = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| + = render 'form', { f: f } + .form-actions + = f.submit 'Save Changes', class: "btn btn-save" diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml index e1477c71d06..5a397c9d3c7 100644 --- a/app/views/projects/pages_domains/new.html.haml +++ b/app/views/projects/pages_domains/new.html.haml @@ -1,6 +1,10 @@ +- add_to_breadcrumbs "Pages", project_pages_path(@project) - page_title 'New Pages Domain' %h3.page_title New Pages Domain %hr.clearfix %div - = render 'form' + = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| + = render 'form', { f: f } + .form-actions + = f.submit 'Create New Domain', class: "btn btn-save" diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index 72e9203bdb0..ba0713daee9 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -1,4 +1,7 @@ +- add_to_breadcrumbs "Pages", project_pages_path(@project) +- breadcrumb_title @domain.domain - page_title "#{@domain.domain}", 'Pages Domains' + - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? - if verification_enabled && @domain.unverified? %p.alert.alert-warning @@ -8,6 +11,7 @@ %h3.page-title Pages Domain + = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right' .table-holder %table.table diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index e660fce652f..49c90869146 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -30,6 +30,11 @@ .col-sm-10 = f.text_field :token, class: 'form-control', readonly: true .form-group + = label_tag :ip_address, class: 'control-label' do + IP Address + .col-sm-10 + = f.text_field :ip_address, class: 'form-control', readonly: true + .form-group = label_tag :description, class: 'control-label' do Description .col-sm-10 diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index dfab04aa1fb..4e57f5f844d 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -41,6 +41,9 @@ %td Version %td= @runner.version %tr + %td IP Address + %td= @runner.ip_address + %tr %td Revision %td= @runner.revision %tr diff --git a/changelogs/unreleased/43489-display-runner-ip.yml b/changelogs/unreleased/43489-display-runner-ip.yml new file mode 100644 index 00000000000..621c2ec709a --- /dev/null +++ b/changelogs/unreleased/43489-display-runner-ip.yml @@ -0,0 +1,5 @@ +--- +title: Display Runner IP Address +merge_request: 17286 +author: +type: added diff --git a/changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml b/changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml new file mode 100644 index 00000000000..173710412a5 --- /dev/null +++ b/changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml @@ -0,0 +1,5 @@ +--- +title: Fix code and wiki search results pages when non-ASCII text is displayed +merge_request: 17413 +author: +type: fixed diff --git a/changelogs/unreleased/43643-fix-mr-label-filtering.yml b/changelogs/unreleased/43643-fix-mr-label-filtering.yml new file mode 100644 index 00000000000..32a44aef243 --- /dev/null +++ b/changelogs/unreleased/43643-fix-mr-label-filtering.yml @@ -0,0 +1,5 @@ +--- +title: Enable filtering MR list based on clicked label in MR sidebar +merge_request: 17390 +author: +type: fixed diff --git a/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml b/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml new file mode 100644 index 00000000000..c0fa8e2e377 --- /dev/null +++ b/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml @@ -0,0 +1,5 @@ +--- +title: Make sure wiki exists when it's enabled +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/assignees-vue-component-missing-data-container.yml b/changelogs/unreleased/assignees-vue-component-missing-data-container.yml new file mode 100644 index 00000000000..233d983b415 --- /dev/null +++ b/changelogs/unreleased/assignees-vue-component-missing-data-container.yml @@ -0,0 +1,5 @@ +--- +title: Add Assignees vue component missing data container +merge_request: 17426 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/feature-edit_pages_domain.yml b/changelogs/unreleased/feature-edit_pages_domain.yml new file mode 100644 index 00000000000..bd0af53296c --- /dev/null +++ b/changelogs/unreleased/feature-edit_pages_domain.yml @@ -0,0 +1,5 @@ +--- +title: 'Pages custom domain: allow update of key/certificate' +merge_request: 17376 +author: rfwatson +type: changed diff --git a/changelogs/unreleased/refactor-move-assignees-vue-component.yml b/changelogs/unreleased/refactor-move-assignees-vue-component.yml new file mode 100644 index 00000000000..98cfa6b4c81 --- /dev/null +++ b/changelogs/unreleased/refactor-move-assignees-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Move Assignees vue component +merge_request: 16952 +author: George Tsiolis +type: performance diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index b89f0419b91..2079d3acb72 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -103,4 +103,6 @@ Doorkeeper.configure do # Some applications require dynamic query parameters on their request_uri # set to true if you want this to be allowed # wildcard_redirect_uri false + + base_controller 'ApplicationController' end diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 8560d24526f..114c1cb512f 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -12,9 +12,14 @@ unless Sidekiq.server? config.lograge.logger = ActiveSupport::Logger.new(filename) # Add request parameters to log output config.lograge.custom_options = lambda do |event| + params = event.payload[:params] + .except(*%w(controller action format)) + .each_pair + .map { |k, v| { key: k, value: v } } + payload = { time: event.time.utc.iso8601(3), - params: event.payload[:params].except(*%w(controller action format)), + params: params, remote_ip: event.payload[:remote_ip], user_id: event.payload[:user_id], username: event.payload[:username] diff --git a/config/routes/project.rb b/config/routes/project.rb index dd1a9c3be3b..34636285c51 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -55,7 +55,7 @@ constraints(ProjectUrlConstrainer.new) do end resource :pages, only: [:show, :destroy] do - resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} } do + resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do member do post :verify end diff --git a/config/webpack.config.js b/config/webpack.config.js index 65785385554..d8ba3c06d3a 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -44,7 +44,6 @@ function generateEntries() { const manualEntries = { balsamiq_viewer: './blob/balsamiq_viewer.js', - cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', environments: './environments/environments_bundle.js', monitoring: './monitoring/monitoring_bundle.js', mr_notes: './mr_notes/index.js', @@ -59,7 +58,6 @@ function generateEntries() { terminal: './terminal/terminal_bundle.js', two_factor_auth: './two_factor_auth.js', - common: './commons/index.js', common_vue: './vue_shared/vue_resource_interceptor.js', locale: './locale/index.js', @@ -225,6 +223,33 @@ const config = { return `${moduleNames[0]}-${hash.substr(0, 6)}`; }), + // create cacheable common library bundle for all vue chunks + new webpack.optimize.CommonsChunkPlugin({ + name: 'common_vue', + chunks: [ + 'boards', + 'deploy_keys', + 'environments', + 'filtered_search', + 'groups', + 'monitoring', + 'mr_notes', + 'notebook_viewer', + 'pdf_viewer', + 'pipelines', + 'pipelines_details', + 'registry_list', + 'ide', + 'schedule_form', + 'schedules_index', + 'sidebar', + 'vue_merge_request_widget', + ], + minChunks: function(module, count) { + return module.resource && (/vue_shared/).test(module.resource); + }, + }), + // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ names: ['main', 'common', 'webpack_runtime'], diff --git a/db/migrate/20180222043024_add_ip_address_to_runner.rb b/db/migrate/20180222043024_add_ip_address_to_runner.rb new file mode 100644 index 00000000000..bf00560b5a8 --- /dev/null +++ b/db/migrate/20180222043024_add_ip_address_to_runner.rb @@ -0,0 +1,9 @@ +class AddIpAddressToRunner < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :ci_runners, :ip_address, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 5bb461169f1..3fb80065ff5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180216121030) do +ActiveRecord::Schema.define(version: 20180222043024) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -437,6 +437,7 @@ ActiveRecord::Schema.define(version: 20180216121030) do t.boolean "run_untagged", default: true, null: false t.boolean "locked", default: false, null: false t.integer "access_level", default: 0, null: false + t.string "ip_address" end add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree diff --git a/doc/README.md b/doc/README.md index 46fcb7c6baf..fb7a23e2750 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,5 +1,4 @@ --- -toc: false comments: false --- @@ -8,15 +7,9 @@ comments: false Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured platform for software development! -GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans. - -With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate. - -Every feature available in Libre is also available in Starter, Premium, and Ultimate. -Starter features are also available in Premium and Ultimate, and Premium features are also -available in Ultimate. - -GitLab.com is our SaaS offering. It's hosted, managed, and administered by GitLab, with [free and paid plans](https://about.gitlab.com/gitlab-com/) for individuals and teams: Free, Bronze, Silver, and Gold. +GitLab offers the most scalable Git-based fully integrated platform for +software development, with flexible products and subscriptions. +To understand what features you have access to, check the [GitLab subscriptions](#gitlab-subscriptions) below. ## Shortcuts to GitLab's most visited docs @@ -124,8 +117,6 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i - [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication. - [Trello Power-Up](integration/trello_power_up.md): Integrate with GitLab's Trello Power-Up ----- - ## Administrator documentation [Administration documentation](administration/index.md) applies to admin users of GitLab @@ -143,3 +134,42 @@ Learn how to contribute to GitLab: - [Development](development/README.md): All styleguides and explanations how to contribute. - [Legal](legal/README.md): Contributor license agreements. - [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs. + +## GitLab subscriptions + +You have two options to use GitLab: + +- GitLab self-hosted: Install, administer, and maintain your own GitLab instance. +- GitLab.com: GitLab's SaaS offering. You don't need to install anything to use GitLab.com, +you only need to [sign up](https://gitlab.com/users/sign_in) and start using GitLab +straight away. + +### GitLab self-hosted + +With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate. + +Every feature available in Libre is also available in Starter, Premium, and Ultimate. +Starter features are also available in Premium and Ultimate, and Premium features are also +available in Ultimate. + +### GitLab.com + +GitLab.com is hosted, managed, and administered by GitLab, Inc., with +[free and paid subscriptions](https://about.gitlab.com/gitlab-com/) for individuals +and teams: Free, Bronze, Silver, and Gold. + +GitLab.com subscriptions grants access +to the same features available in GitLab self-hosted, **expect +[administration](administration/index.md) tools and settings**: + +- GitLab.com Free includes the same features available in GitLab Libre +- GitLab.com Bronze includes the same features available in GitLab Starter +- GitLab.com Silver includes the same features available in GitLab Premium +- GitLab.com Gold includes the same features available in GitLab Ultimate + +For supporting the open source community and encouraging the development of +open source projects, GitLab grants access to **Gold** features +for all GitLab.com **public** projects, regardless of the subscription. + +To know more about GitLab subscriptions and licensing, please refer to the +[GitLab Product Marketing Handbook](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers). diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index f30a85b114e..23ce6a5f210 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -56,6 +56,9 @@ future GitLab releases.** | **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | | **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags | +| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job | +| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job | +| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) | | **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | | **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | | **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source | diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 6c93c29124d..09957feee17 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -507,6 +507,7 @@ This is the entry point for our store. You can use the following as a guide: import Vue from 'vue'; import Vuex from 'vuex'; import * as actions from './actions'; +import * as getters from './getters'; import * as mutations from './mutations'; Vue.use(Vuex); @@ -514,6 +515,7 @@ Vue.use(Vuex); export default new Vuex.Store({ actions, getters, + mutations, state: { users: [], }, diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 3ae98adc465..f8a7dd6b1dc 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -109,8 +109,7 @@ in your SAML IdP: 1. Change the value of `issuer` to a unique name, which will identify the application to the IdP. -1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you - installed GitLab via Omnibus or from source respectively. +1. For the changes to take effect, you must [reconfigure][] GitLab if you installed via Omnibus or [restart GitLab][] if you installed from source. 1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in `issuer`. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index ea7b1c9a0ed..650d60f1585 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -36,12 +36,16 @@ GFM honors the markdown specification in how [paragraphs and line breaks are han A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. Line-breaks, or softreturns, are rendered if you end a line with two or more spaces: - Roses are red [followed by two or more spaces] +[//]: # (Do *NOT* remove the two ending whitespaces in the following line.) +[//]: # (They are needed for the Markdown text to render correctly.) + Roses are red [followed by two or more spaces] Violets are blue Sugar is sweet -Roses are red +[//]: # (Do *NOT* remove the two ending whitespaces in the following line.) +[//]: # (They are needed for the Markdown text to render correctly.) +Roses are red Violets are blue Sugar is sweet diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 0de89f90e21..16027744164 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -34,7 +34,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium) - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter) - [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter) -- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter) +- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter) ## Use cases diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index fbe30192a16..35ac0b4cbca 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -9,16 +9,22 @@ module API Gitlab::CurrentSettings.runners_registration_token) end - def get_runner_version_from_params - return unless params['info'].present? + def authenticate_runner! + forbidden! unless current_runner - attributes_for_keys(%w(name version revision platform architecture), params['info']) + current_runner + .update_cached_info(get_runner_details_from_request) end - def authenticate_runner! - forbidden! unless current_runner + def get_runner_details_from_request + return get_runner_ip unless params['info'].present? + + attributes_for_keys(%w(name version revision platform architecture), params['info']) + .merge(get_runner_ip) + end - current_runner.update_cached_info(get_runner_version_from_params) + def get_runner_ip + { ip_address: request.ip } end def current_runner diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 5469cba69a6..91cdc564002 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -16,7 +16,8 @@ module API optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) end post '/' do - attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list] + attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list]) + .merge(get_runner_details_from_request) runner = if runner_registration_token_valid? @@ -30,7 +31,6 @@ module API return forbidden! unless runner if runner.id - runner.update(get_runner_version_from_params) present runner, with: Entities::RunnerRegistrationDetails else not_found! diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index ac12271a87e..52b44b9b3c5 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -59,7 +59,7 @@ module Gitlab end def pages(limit: nil) - @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| + @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled| if is_enabled gitaly_get_all_pages else @@ -68,9 +68,8 @@ module Gitlab end end - # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039 def page(title:, version: nil, dir: nil) - @repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| + @repository.gitaly_migrate(:wiki_find_page) do |is_enabled| if is_enabled gitaly_find_page(title: title, version: version, dir: dir) else diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 5a5ae7f19d4..781783f4d97 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -1,6 +1,8 @@ module Gitlab class SearchResults class FoundBlob + include EncodingHelper + attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id def initialize(opts = {}) @@ -9,7 +11,7 @@ module Gitlab @basename = opts.fetch(:basename, nil) @ref = opts.fetch(:ref, nil) @startline = opts.fetch(:startline, nil) - @data = opts.fetch(:data, nil) + @data = encode_utf8(opts.fetch(:data, nil)) @per_page = opts.fetch(:per_page, 20) @project_id = opts.fetch(:project_id, nil) end diff --git a/scripts/security-harness b/scripts/security-harness index d454f44dff7..c60b3410095 100755 --- a/scripts/security-harness +++ b/scripts/security-harness @@ -21,6 +21,8 @@ else File.open(hook_path, 'w') do |file| IO.copy_stream(DATA, file) end + + File.chmod(0755, hook_path) end # Toggle the harness on or off diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 004b463e745..149b690ff70 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -34,6 +34,8 @@ describe Oauth::AuthorizationsController do end context 'with valid params' do + render_views + it 'returns 200 code and renders view' do get :new, params diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb index 2192fd5cae2..83a3799e883 100644 --- a/spec/controllers/projects/pages_domains_controller_spec.rb +++ b/spec/controllers/projects/pages_domains_controller_spec.rb @@ -53,6 +53,66 @@ describe Projects::PagesDomainsController do end end + describe 'GET edit' do + it "displays the 'edit' page" do + get(:edit, request_params.merge(id: pages_domain.domain)) + + expect(response).to have_gitlab_http_status(200) + expect(response).to render_template('edit') + end + end + + describe 'PATCH update' do + before do + controller.instance_variable_set(:@domain, pages_domain) + end + + let(:pages_domain_params) do + attributes_for(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate) + end + + let(:params) do + request_params.merge(id: pages_domain.domain, pages_domain: pages_domain_params) + end + + it 'updates the domain' do + expect(pages_domain) + .to receive(:update) + .with(pages_domain_params) + .and_return(true) + + patch(:update, params) + end + + it 'redirects to the project page' do + patch(:update, params) + + expect(flash[:notice]).to eq 'Domain was updated' + expect(response).to redirect_to(project_pages_path(project)) + end + + context 'the domain is invalid' do + it 'renders the edit action' do + allow(pages_domain).to receive(:update).and_return(false) + + patch(:update, params) + + expect(response).to render_template('edit') + end + end + + context 'the parameters include the domain' do + it 'renders 400 Bad Request' do + expect(pages_domain) + .to receive(:update) + .with(hash_not_including(:domain)) + .and_return(true) + + patch(:update, params.deep_merge(pages_domain: { domain: 'abc' })) + end + end + end + describe 'POST verify' do let(:params) { request_params.merge(id: pages_domain.domain) } diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index a96f2c186a4..2a0d235ef04 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -160,6 +160,37 @@ feature 'Pages' do expect(page).to have_content('my.test.domain.com') end + + describe 'updating the certificate for an existing domain' do + let!(:domain) do + create(:pages_domain, :with_key, :with_certificate, project: project) + end + + it 'allows the certificate to be updated' do + visit project_pages_path(project) + + within('#content-body') { click_link 'Details' } + click_link 'Edit' + click_button 'Save Changes' + + expect(page).to have_content('Domain was updated') + end + + context 'when the certificate is invalid' do + it 'tells the user what the problem is' do + visit project_pages_path(project) + + within('#content-body') { click_link 'Details' } + click_link 'Edit' + fill_in 'Certificate (PEM)', with: 'invalid data' + click_button 'Save Changes' + + expect(page).to have_content('Certificate must be a valid PEM certificate') + expect(page).to have_content('Certificate misses intermediates') + expect(page).to have_content("Key doesn't match the certificate") + end + end + end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 37a06b65481..3a8e7c05cc4 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -367,23 +367,6 @@ describe 'Pipelines', :js do expect(build.reload).to be_canceled end end - - context 'dropdown jobs list' do - it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do - find('.js-builds-dropdown-button').click - dropdown_item = find('.mini-pipeline-graph-dropdown-item').native - - %i(alt control).each do |meta_key| - page.driver.browser.action - .key_down(meta_key) - .click(dropdown_item) - .key_up(meta_key) - .perform - end - - expect(page).to have_selector('.js-ci-action-icon') - end - end end context 'with pagination' do diff --git a/spec/javascripts/labels_select_spec.js b/spec/javascripts/labels_select_spec.js new file mode 100644 index 00000000000..b8f7b1dc855 --- /dev/null +++ b/spec/javascripts/labels_select_spec.js @@ -0,0 +1,43 @@ +import LabelsSelect from '~/labels_select'; + +const mockUrl = '/foo/bar/url'; + +const mockLabels = [ + { + id: 26, + title: 'Foo Label', + description: 'Foobar', + color: '#BADA55', + text_color: '#FFFFFF', + }, +]; + +describe('LabelsSelect', () => { + describe('getLabelTemplate', () => { + const label = mockLabels[0]; + let $labelEl; + + beforeEach(() => { + $labelEl = $(LabelsSelect.getLabelTemplate({ + labels: mockLabels, + issueUpdateURL: mockUrl, + })); + }); + + it('generated label item template has correct label URL', () => { + expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label'); + }); + + it('generated label item template has correct label title', () => { + expect($labelEl.find('span.label').text()).toBe(label.title); + }); + + it('generated label item template has label description as title attribute', () => { + expect($labelEl.find('span.label').attr('title')).toBe(label.description); + }); + + it('generated label item template has correct label styles', () => { + expect($labelEl.find('span.label').attr('style')).toBe(`background-color: ${label.color}; color: ${label.text_color};`); + }); + }); +}); diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index c9453a21189..4e4343812bd 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import Assignee from '~/sidebar/components/assignees/assignees'; +import Assignee from '~/sidebar/components/assignees/assignees.vue'; import UsersMock from './mock_data'; import UsersMockHelper from '../helpers/user_mock_data_helper'; diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 1ebb0105cf5..d8250e4b4c6 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::ProjectSearchResults do @@ -105,6 +106,32 @@ describe Gitlab::ProjectSearchResults do end end + context 'when the search returns non-ASCII data' do + context 'with UTF-8' do + let(:results) { project.repository.search_files_by_content("файл", 'master') } + + it 'returns results as UTF-8' do + expect(subject.filename).to eq('encoding/russian.rb') + expect(subject.basename).to eq('encoding/russian') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("Хороший файл") + end + end + + context 'with ISO-8859-1' do + let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) } + + it 'returns results as UTF-8' do + expect(subject.filename).to eq('encoding/iso8859.txt') + expect(subject.basename).to eq('encoding/iso8859') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("Äü\n\nfoo") + end + end + end + context "when filename has extension" do let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index f10b6e43d09..4021e537efc 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -122,6 +122,15 @@ describe API::Runner do end end end + + it "sets the runner's ip_address" do + post api('/runners'), + { token: registration_token }, + { 'REMOTE_ADDR' => '123.111.123.111' } + + expect(response).to have_gitlab_http_status 201 + expect(Ci::Runner.first.ip_address).to eq('123.111.123.111') + end end describe 'DELETE /api/v4/runners' do @@ -422,6 +431,15 @@ describe API::Runner do end end + it "sets the runner's ip_address" do + post api('/jobs/request'), + { token: runner.token }, + { 'User-Agent' => user_agent, 'REMOTE_ADDR' => '123.222.123.222' } + + expect(response).to have_gitlab_http_status 201 + expect(runner.reload.ip_address).to eq('123.222.123.222') + end + context 'when concurrently updating a job' do before do expect_any_instance_of(Ci::Build).to receive(:run!) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index a0b97ceead9..ad5a289290c 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -123,6 +123,40 @@ describe Projects::UpdateService do end end + context 'when we update project but not enabling a wiki' do + it 'does not try to create an empty wiki' do + FileUtils.rm_rf(project.wiki.repository.path) + + result = update_project(project, user, { name: 'test1' }) + + expect(result).to eq({ status: :success }) + expect(project.wiki_repository_exists?).to be false + end + end + + context 'when enabling a wiki' do + it 'creates a wiki' do + project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED) + FileUtils.rm_rf(project.wiki.repository.path) + + result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED }) + + expect(result).to eq({ status: :success }) + expect(project.wiki_repository_exists?).to be true + expect(project.wiki_enabled?).to be true + end + + it 'logs an error and creates a metric when wiki can not be created' do + project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED) + + expect_any_instance_of(ProjectWiki).to receive(:wiki).and_raise(ProjectWiki::CouldNotCreateWikiError) + expect_any_instance_of(described_class).to receive(:log_error).with("Could not create wiki for #{project.full_name}") + expect(Gitlab::Metrics).to receive(:counter) + + update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED }) + end + end + context 'when updating a project that contains container images' do before do stub_container_registry_config(enabled: true) |