diff options
428 files changed, 6518 insertions, 2020 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b42e661dff..f27d809af3c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,6 +40,7 @@ stages: - test - post-test - pages + - post-cleanup # Predefined scopes .dedicated-runner: &dedicated-runner @@ -153,8 +154,7 @@ stages: - master@gitlab/gitlabhq - master@gitlab/gitlab-ee -# Trigger a package build on omnibus-gitlab repository - +# Trigger a package build in omnibus-gitlab repository build-package: image: ruby:2.3-alpine before_script: [] @@ -166,11 +166,47 @@ build-package: cache: {} when: manual script: - - scripts/trigger-build + - scripts/trigger-build-omnibus only: - //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ee +# Review docs base +.review-docs: &review-docs + image: ruby:2.4-alpine + before_script: [] + services: [] + variables: + SETUP_DB: "false" + USE_BUNDLE_INSTALL: "false" + cache: {} + when: manual + only: + - branches + +# Trigger a docs build in gitlab-docs +# Useful to preview the docs changes live +review-docs-deploy: + <<: *review-docs + stage: build + environment: + name: review-docs/$CI_COMMIT_REF_NAME + on_stop: review-docs-cleanup + script: + - gem install gitlab --no-doc + - scripts/trigger-build-docs deploy + +# Cleanup remote environment of gitlab-docs +review-docs-cleanup: + <<: *review-docs + stage: post-cleanup + environment: + name: review-docs/$CI_COMMIT_REF_NAME + action: stop + script: + - gem install gitlab --no-doc + - scripts/trigger-build-docs cleanup + # Retrieve knapsack and rspec_flaky reports retrieve-tests-metadata: <<: *tests-metadata-state @@ -412,6 +448,7 @@ db:migrate:reset-mysql: .migration-paths: &migration-paths <<: *dedicated-runner <<: *pull-cache + <<: *except-docs stage: test variables: SETUP_DB: "false" diff --git a/.rubocop.yml b/.rubocop.yml index 16f2e4484fc..4640681379a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -643,7 +643,7 @@ Metrics/ClassLength: # of test cases needed to validate a method. Metrics/CyclomaticComplexity: Enabled: true - Max: 15 + Max: 14 # Limit lines to 80 characters. Metrics/LineLength: diff --git a/CHANGELOG.md b/CHANGELOG.md index a02b6594fad..4cedfa60b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.5.4 (2017-09-06) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [SECURITY] Prevent a persistent XSS in the commit author block. +- Fix XSS issue in go-get handling. +- Resolve CSRF token leakage via pathname manipulation on environments page. +- Fixes race condition in project uploads. +- Disallow arbitrary properties in `th` and `td` `style` attributes. +- Disallow the `name` attribute on all user-provided markup. + ## 9.5.3 (2017-09-03) - [SECURITY] Filter additional secrets from Rails logs. @@ -203,6 +213,18 @@ entry. - Use a specialized class for querying events to improve performance. - Update build badges to be pipeline badges and display passing instead of success. +## 9.4.6 (2017-09-06) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [SECURITY] Prevent a persistent XSS in the commit author block. +- Fix XSS issue in go-get handling. +- Remove hidden symlinks from project import files. +- Fixes race condition in project uploads. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. +- Disallow arbitrary properties in `th` and `td` `style` attributes. +- Resolve CSRF token leakage via pathname manipulation on environments page. +- Disallow the `name` attribute on all user-provided markup. + ## 9.4.5 (2017-08-14) - Fix deletion of deploy keys linked to other projects. !13162 @@ -453,6 +475,24 @@ entry. - Log rescued exceptions to Sentry. - Remove remaining N+1 queries in merge requests API with emojis and labels. +## 9.3.11 (2017-09-06) + +- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) +- [SECURITY] Prevent a persistent XSS in the commit author block. +- Improve support for external issue references. !12485 +- Use uploads/system directory for personal snippets. +- Remove uploads/appearance symlink. A leftover from a previous migration. +- Fix XSS issue in go-get handling. +- Remove hidden symlinks from project import files. +- Fix an infinite loop when handling user-supplied regular expressions. +- Fixes race condition in project uploads. +- Fixes race condition in project uploads. +- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character. +- Disallow arbitrary properties in `th` and `td` `style` attributes. +- Resolve CSRF token leakage via pathname manipulation on environments page. +- Disallow the `name` attribute on all user-provided markup. +- Renders 404 if given project is not readable by the user on Todos dashboard. + ## 9.3.10 (2017-08-09) - Remove hidden symlinks from project import files. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cc34f1de08..1f25171e8a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ By submitting code as an individual you agree to the By submitting code as an entity you agree to the [corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). -_This notice should stay as the first item in the CONTRIBUTING.MD file._ +_This notice should stay as the first item in the CONTRIBUTING.md file._ --- @@ -21,7 +21,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._ - [Workflow labels](#workflow-labels) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - - [Team labels (~CI, ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) + - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Implement design & UI elements](#implement-design--ui-elements) @@ -115,7 +115,7 @@ Most issues will have labels for at least one of the following: - Type: ~"feature proposal", ~bug, ~customer, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. -- Team: ~CI, ~Discussion, ~Edge, ~Platform, etc. +- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Priority: ~Deliverable, ~Stretch All labels, their meaning and priority are defined on the @@ -157,13 +157,13 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api, Subject labels are always all-lowercase. -### Team labels (~CI, ~Discussion, ~Edge, ~Platform, etc.) +### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.) Team labels specify what team is responsible for this issue. Assigning a team label makes sure issues get the attention of the appropriate people. -The current team labels are ~Build, ~CI, ~Discussion, ~Documentation, ~Edge, +The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge, ~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX". The descriptions on the [labels page][labels-page] explain what falls under the @@ -217,11 +217,11 @@ After adding the ~"Accepting Merge Requests" label, we try to estimate the [weight](#issue-weight) of the issue. We use issue weight to let contributors know how difficult the issue is. Additionally: -- We advertise [~"Accepting Merge Requests" issues with weight < 5][up-for-grabs] +- We advertise ["Accepting Merge Requests" issues with weight < 5][up-for-grabs] as suitable for people that have never contributed to GitLab before on the [Up For Grabs campaign](http://up-for-grabs.net) - We encourage people that have never contributed to any open source project to - look for [~"Accepting Merge Requests" issues with a weight of 1][firt-timers] + look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers] [up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened [firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1 @@ -421,7 +421,7 @@ request is as follows: 1. Fork the project into your personal space on GitLab.com 1. Create a feature branch, branch away from `master` -1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code +1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code 1. [Generate a changelog entry with `bin/changelog`][changelog] 1. If you are writing documentation, make sure to follow the [documentation styleguide][doc-styleguide] diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 8f0916f768f..4b9fcbec101 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 @@ -407,3 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2' # Structured logging gem 'lograge', '~> 0.5' +gem 'grape_logging', '~> 1.6' diff --git a/Gemfile.lock b/Gemfile.lock index 9fbf08d80e2..d7e1c7581d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -355,6 +355,8 @@ GEM activesupport grape (>= 0.16.0) rake + grape_logging (1.6.0) + grape grpc (1.4.5) google-protobuf (~> 3.1) googleauth (~> 0.5.1) @@ -1035,6 +1037,7 @@ DEPENDENCIES grape (~> 1.0) grape-entity (~> 0.6.0) grape-route-helpers (~> 2.1.0) + grape_logging (~> 1.6) haml_lint (~> 0.26.0) hamlit (~> 2.6.1) hashie-forbidden_attributes diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8acddd6194c..38d1effc77c 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -6,7 +6,8 @@ const Api = { namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', projectsPath: '/api/:version/projects.json', - labelsPath: '/:namespace_path/:project_path/labels', + projectLabelsPath: '/:namespace_path/:project_path/labels', + groupLabelsPath: '/groups/:namespace_path/labels', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', @@ -74,9 +75,16 @@ const Api = { }, newLabel(namespacePath, projectPath, data, callback) { - const url = Api.buildUrl(Api.labelsPath) - .replace(':namespace_path', namespacePath) - .replace(':project_path', projectPath); + let url; + + if (projectPath) { + url = Api.buildUrl(Api.projectLabelsPath) + .replace(':namespace_path', namespacePath) + .replace(':project_path', projectPath); + } else { + url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); + } + return $.ajax({ url, type: 'POST', diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 89c14180149..ea00efe4b46 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -53,7 +53,8 @@ $(() => { data: { state: Store.state, loading: true, - endpoint: $boardApp.dataset.endpoint, + boardsEndpoint: $boardApp.dataset.boardsEndpoint, + listsEndpoint: $boardApp.dataset.listsEndpoint, boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase, @@ -68,7 +69,13 @@ $(() => { }, }, created () { - gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); + gl.boardService = new BoardService({ + boardsEndpoint: this.boardsEndpoint, + listsEndpoint: this.listsEndpoint, + bulkUpdatePath: this.bulkUpdatePath, + boardId: this.boardId, + }); + Store.rootPath = this.boardsEndpoint; this.filterManager = new FilteredSearchBoards(Store.filter, true); this.filterManager.setup(); @@ -112,19 +119,21 @@ $(() => { gl.IssueBoardsSearch = new Vue({ el: document.getElementById('js-add-list'), data: { - filters: Store.state.filters + filters: Store.state.filters, }, mounted () { gl.issueBoards.newListDropdownInit(); - } + }, }); gl.IssueBoardsModalAddBtn = new Vue({ mixins: [gl.issueBoards.ModalMixins], el: document.getElementById('js-add-issues-btn'), - data: { - modal: ModalStore.store, - store: Store.state, + data() { + return { + modal: ModalStore.store, + store: Store.state, + }; }, watch: { disabled() { @@ -133,6 +142,9 @@ $(() => { }, computed: { disabled() { + if (!this.store) { + return true; + } return !this.store.lists.filter(list => !list.preset).length; }, tooltipTitle() { @@ -145,7 +157,7 @@ $(() => { }, methods: { updateTooltip() { - const $tooltip = $(this.$el); + const $tooltip = $(this.$refs.addIssuesButton); this.$nextTick(() => { if (this.disabled) { @@ -165,16 +177,19 @@ $(() => { this.updateTooltip(); }, template: ` - <button - class="btn btn-create pull-right prepend-left-10" - type="button" - data-placement="bottom" - :class="{ 'disabled': disabled }" - :title="tooltipTitle" - :aria-disabled="disabled" - @click="openModal"> - Add issues - </button> + <div class="board-extra-actions"> + <button + class="btn btn-create prepend-left-10" + type="button" + data-placement="bottom" + ref="addIssuesButton" + :class="{ 'disabled': disabled }" + :title="tooltipTitle" + :aria-disabled="disabled" + @click="openModal"> + Add issues + </button> + </div> `, }); }); diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index bebca17fb1e..6159680f1e6 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -77,7 +77,7 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { + if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { this.loadNextPage(); } }, @@ -165,11 +165,9 @@ export default { v-if="loading"> <loading-icon /> </div> - <transition name="slide-down"> - <board-new-issue - :list="list" - v-if="list.type !== 'closed' && showIssueForm"/> - </transition> + <board-new-issue + :list="list" + v-if="list.type !== 'closed' && showIssueForm"/> <ul class="board-list" v-show="!loading" diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index 4af8b0c7713..541b8049855 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore; export default { name: 'BoardNewIssue', props: { - list: Object, + list: { + type: Object, + required: true, + }, }, data() { return { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 9a5d87ede7e..bf474879024 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -64,10 +64,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ return this.issue.assignees.length > this.numberOverLimit; }, cardUrl() { - return `${this.issueLinkBase}/${this.issue.id}`; + return `${this.issueLinkBase}/${this.issue.iid}`; }, issueId() { - return `#${this.issue.id}`; + if (this.issue.iid) { + return `#${this.issue.iid}`; + } + return false; }, showLabelFooter() { return this.issue.labels.find(l => this.showLabel(l)) !== undefined; @@ -143,7 +146,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ :title="issue.title">{{ issue.title }}</a> <span class="card-number" - v-if="issue.id" + v-if="issueId" > {{ issueId }} </span> diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 478a1335b2b..a656f0546c0 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ const firstListIndex = 1; const list = this.modal.selectedList || this.state.lists[firstListIndex]; const selectedIssues = ModalStore.getSelectedIssues(); - const issueIds = selectedIssues.map(issue => issue.globalId); + const issueIds = selectedIssues.map(issue => issue.id); // Post the data to the backend gl.boardService.bulkUpdate(issueIds, { diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 72bb9e10fbc..d7f203b3f96 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => { $this.glDropdown({ data(term, callback) { - $.get($this.attr('data-labels')) + $.get($this.attr('data-list-labels-path')) .then((resp) => { callback(resp); }); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 6a900d4abd0..1e623cf58b7 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -18,17 +18,33 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ type: Object, required: true, }, + issueUpdate: { + type: String, + required: true, + }, + }, + computed: { + updateUrl() { + return this.issueUpdate; + }, }, methods: { removeIssue() { const issue = this.issue; const lists = issue.getLists(); - const labelIds = lists.map(list => list.label.id); - - // Post the remove data - gl.boardService.bulkUpdate([issue.globalId], { - remove_label_ids: labelIds, - }).catch(() => { + const listLabelIds = lists.map(list => list.label.id); + let labelIds = this.issue.labels + .map(label => label.id) + .filter(id => !listLabelIds.includes(id)); + if (labelIds.length === 0) { + labelIds = ['']; + } + const data = { + issue: { + label_ids: labelIds, + }, + }; + Vue.http.patch(this.updateUrl, data).catch(() => { new Flash('Failed to remove issue from board, please try again.', 'alert'); lists.forEach((list) => { diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 6c2d8a3781b..407db176446 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -7,8 +7,8 @@ import Vue from 'vue'; class ListIssue { constructor (obj, defaultAvatar) { - this.globalId = obj.id; - this.id = obj.iid; + this.id = obj.id; + this.iid = obj.iid; this.title = obj.title; this.confidential = obj.confidential; this.dueDate = obj.due_date; diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js index 9af88d167d6..98c1ec014c4 100644 --- a/app/assets/javascripts/boards/models/label.js +++ b/app/assets/javascripts/boards/models/label.js @@ -4,6 +4,7 @@ class ListLabel { constructor (obj) { this.id = obj.id; this.title = obj.title; + this.type = obj.type; this.color = obj.color; this.textColor = obj.text_color; this.description = obj.description; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 08f7c5ddcd2..df2809e1805 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -110,11 +110,13 @@ class List { return gl.boardService.newIssue(this.id, issue) .then(resp => resp.json()) .then((data) => { - issue.id = data.iid; + issue.id = data.id; + issue.iid = data.iid; + issue.project = data.project; if (this.issuesSize > 1) { - const moveBeforeIid = this.issues[1].id; - gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); + const moveBeforeId = this.issues[1].id; + gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId); } }); } @@ -126,19 +128,19 @@ class List { } addIssue (issue, listFrom, newIndex) { - let moveBeforeIid = null; - let moveAfterIid = null; + let moveBeforeId = null; + let moveAfterId = null; if (!this.findIssue(issue.id)) { if (newIndex !== undefined) { this.issues.splice(newIndex, 0, issue); if (this.issues[newIndex - 1]) { - moveBeforeIid = this.issues[newIndex - 1].id; + moveBeforeId = this.issues[newIndex - 1].id; } if (this.issues[newIndex + 1]) { - moveAfterIid = this.issues[newIndex + 1].id; + moveAfterId = this.issues[newIndex + 1].id; } } else { this.issues.push(issue); @@ -151,30 +153,30 @@ class List { if (listFrom) { this.issuesSize += 1; - this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid); + this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId); } } } - moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) { + moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { this.issues.splice(oldIndex, 1); this.issues.splice(newIndex, 0, issue); - gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid) + gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId) .catch(() => { // TODO: handle request error }); } - updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) + updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { + gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) .catch(() => { // TODO: handle request error }); } findIssue (id) { - return this.issues.filter(issue => issue.id === id)[0]; + return this.issues.find(issue => issue.id === id); } removeIssue (removeIssue) { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 3742507b236..38eea38f949 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -3,21 +3,21 @@ import Vue from 'vue'; class BoardService { - constructor (root, bulkUpdatePath, boardId) { - this.boards = Vue.resource(`${root}{/id}.json`, {}, { + constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { + this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { issues: { method: 'GET', - url: `${root}/${boardId}/issues.json` + url: `${gon.relative_url_root}/boards/${boardId}/issues.json`, } }); - this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { + this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, { generate: { method: 'POST', - url: `${root}/${boardId}/lists/generate.json` + url: `${listsEndpoint}/generate.json` } }); - this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { + this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {}); + this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, { bulkUpdate: { method: 'POST', url: bulkUpdatePath, @@ -60,12 +60,12 @@ class BoardService { return this.issues.get(data); } - moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) { + moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) { return this.issue.update({ id }, { from_list_id, to_list_id, - move_before_iid, - move_after_iid, + move_before_id, + move_after_id, }); } diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 687f09882a7..16c5d0fa344 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => { propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); pipelineTableViewEl.appendChild(table.$el); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index dd751ec97a8..c931e1e0ea5 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -13,6 +13,10 @@ type: String, required: true, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, mixins: [ pipelinesMixin, @@ -95,6 +99,7 @@ <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsHelpPath" /> </div> </div> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 6db0b18ae5a..f3b537c83e2 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -160,6 +160,9 @@ import initChangesDropdown from './init_changes_dropdown'; const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } + if (page === 'projects:merge_requests:index') { + new UserCallout({ setCalloutPerProject: true }); + } const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; IssuableIndex.init(pagePrefix); @@ -342,6 +345,7 @@ import initChangesDropdown from './init_changes_dropdown'; case 'projects:show': shortcut_handler = new ShortcutsNavigation(); new NotificationsForm(); + new UserCallout({ setCalloutPerProject: true }); if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); @@ -361,6 +365,9 @@ import initChangesDropdown from './init_changes_dropdown'; case 'projects:pipelines:new': new NewBranchForm($('.js-new-pipeline-form')); break; + case 'projects:pipelines:index': + new UserCallout({ setCalloutPerProject: true }); + break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': @@ -418,6 +425,7 @@ import initChangesDropdown from './init_changes_dropdown'; new TreeView(); new BlobViewer(); new NewCommitForm($('.js-create-dir-form')); + new UserCallout({ setCalloutPerProject: true }); $('#tree-slider').waitForImages(function() { gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); @@ -569,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown'; case 'edit': shortcut_handler = new ShortcutsNavigation(); new ProjectNew(); + import(/* webpackChunkName: 'project_permissions' */ './projects/permissions') + .then(permissions => permissions.default()) + .catch(() => {}); break; case 'new': new ProjectNew(); diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index 91ed8c8467f..f54d573db6e 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -111,11 +111,11 @@ export default { }, methods: { - toggleFolder(folder, folderUrl) { + toggleFolder(folder) { this.store.toggleFolder(folder); if (!folder.isOpen) { - this.fetchChildEnvironments(folder, folderUrl, true); + this.fetchChildEnvironments(folder, true); } }, @@ -143,10 +143,10 @@ export default { .catch(this.errorCallback); }, - fetchChildEnvironments(folder, folderUrl, showLoader = false) { + fetchChildEnvironments(folder, showLoader = false) { this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); - this.service.getFolderContent(folderUrl) + this.service.getFolderContent(folder.folder_path) .then(resp => resp.json()) .then(response => this.store.setfolderContent(folder, response.environments)) .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) @@ -173,12 +173,7 @@ export default { // We need to verify if any folder is open to also update it const openFolders = this.store.getOpenFolders(); if (openFolders.length) { - openFolders.forEach((folder) => { - // TODO - Move this to the backend - const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`; - - return this.fetchChildEnvironments(folder, folderUrl); - }); + openFolders.forEach(folder => this.fetchChildEnvironments(folder)); } }, diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index d8b1b2f1b92..6de01fa53d0 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -410,20 +410,11 @@ export default { this.hasStopAction || this.canRetry; }, - - /** - * Constructs folder URL based on the current location and the folder id. - * - * @return {String} - */ - folderUrl() { - return `${window.location.pathname}/folders/${this.model.folderName}`; - }, }, methods: { onClickFolder() { - eventHub.$emit('toggleFolder', this.model, this.folderUrl); + eventHub.$emit('toggleFolder', this.model); }, }, }; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 038239bf466..9178fec085a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -332,7 +332,14 @@ class FilteredSearchManager { const removeElements = []; [].forEach.call(this.tokensContainer.children, (t) => { - if (t.classList.contains('js-visual-token')) { + let canClearToken = t.classList.contains('js-visual-token'); + + if (canClearToken) { + const tokenKey = t.querySelector('.name').textContent.trim(); + canClearToken = this.canEdit && this.canEdit(tokenKey); + } + + if (canClearToken) { removeElements.push(t); } }); @@ -411,8 +418,14 @@ class FilteredSearchManager { }); } + // allows for modifying params array when a param can't be included in the URL (e.g. Service Desk) + getAllParams(urlParams) { + return this.modifyUrlParams ? this.modifyUrlParams(urlParams) : urlParams; + } + loadSearchParamsFromURL() { - const params = gl.utils.getUrlParamsArray(); + const urlParams = gl.utils.getUrlParamsArray(); + const params = this.getAllParams(urlParams); const usernameParams = this.getUsernameParams(); let hasFilteredSearch = false; diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 063155a167a..4b19f7b4188 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -21,8 +21,10 @@ let headerHeight = 50; export const getHeaderHeight = () => headerHeight; +export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only'); + export const canShowActiveSubItems = (el) => { - if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) { + if (el.classList.contains('active') && !isSidebarCollapsed()) { return false; } @@ -100,12 +102,13 @@ export const moveSubItemsToPosition = (el, subItems) => { export const showSubLevelItems = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); + const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only'); if (!canShowSubItems() || !canShowActiveSubItems(el)) return; el.classList.add(IS_OVER_CLASS); - if (!subItems) return; + if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return; subItems.style.display = 'block'; el.classList.add(IS_SHOWING_FLY_OUT_CLASS); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 7c4f4da6127..c0bd64814ca 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -73,7 +73,7 @@ class Issue { $(document).trigger('issuable:change', isClosed); this.toggleCloseReopenButton(isClosed); - let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, '')); + let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 57394097944..917a45eb06b 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -13,7 +13,7 @@ export function formatRelevantDigits(number) { let relevantDigits = 0; let formattedNumber = ''; if (!isNaN(Number(number))) { - digitsLeft = number.split('.')[0]; + digitsLeft = number.toString().split('.')[0]; switch (digitsLeft.length) { case 1: relevantDigits = 3; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3b3620fe61b..0c3c877ff15 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -243,6 +243,7 @@ import bp from './breakpoints'; propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index cde2ff7ca2a..6b3e341f936 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -3,7 +3,7 @@ import GraphLegend from './graph/legend.vue'; import GraphFlag from './graph/flag.vue'; import GraphDeployment from './graph/deployment.vue'; - import monitoringPaths from './monitoring_paths.vue'; + import GraphPath from './graph_path.vue'; import MonitoringMixin from '../mixins/monitoring_mixins'; import eventHub from '../event_hub'; import measurements from '../utils/measurements'; @@ -40,8 +40,6 @@ graphHeightOffset: 120, margin: {}, unitOfDisplay: '', - areaColorRgb: '#8fbce8', - lineColorRgb: '#1f78d1', yAxisLabel: '', legendTitle: '', reducedDeploymentData: [], @@ -63,7 +61,7 @@ GraphLegend, GraphFlag, GraphDeployment, - monitoringPaths, + GraphPath, }, computed: { @@ -143,7 +141,7 @@ }, renderAxesPaths() { - this.timeSeries = createTimeSeries(this.graphData.queries[0].result, + this.timeSeries = createTimeSeries(this.graphData.queries[0], this.graphWidth, this.graphHeight, this.graphHeightOffset); @@ -162,7 +160,7 @@ const xAxis = d3.svg.axis() .scale(axisXScale) - .ticks(measurements.xTicks) + .ticks(d3.time.minute, 60) .tickFormat(timeScaleFormat) .orient('bottom'); @@ -238,7 +236,7 @@ class="graph-data" :viewBox="innerViewBox" ref="graphData"> - <monitoring-paths + <graph-path v-for="(path, index) in timeSeries" :key="index" :generated-line-path="path.linePath" @@ -246,7 +244,7 @@ :line-color="path.lineColor" :area-color="path.areaColor" /> - <monitoring-deployment + <graph-deployment :show-deploy-info="showDeployInfo" :deployment-data="reducedDeploymentData" :graph-height="graphHeight" diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index a43dad8e601..dbc48c63747 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -81,6 +81,13 @@ formatMetricUsage(series) { return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; }, + + createSeriesString(index, series) { + if (series.metricTag) { + return `${series.metricTag} ${this.formatMetricUsage(series)}`; + } + return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; + }, }, mounted() { this.$nextTick(() => { @@ -164,7 +171,7 @@ ref="legendTitleSvg" x="38" :y="graphHeight - 30"> - {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}} + {{createSeriesString(index, series)}} </text> <text v-else diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/graph_path.vue index 043f1bf66bb..043f1bf66bb 100644 --- a/app/assets/javascripts/monitoring/components/monitoring_paths.vue +++ b/app/assets/javascripts/monitoring/components/graph_path.vue diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 05d551e917c..3cbe06d8fd6 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -1,8 +1,37 @@ import d3 from 'd3'; import _ from 'underscore'; -export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) { - const maxValues = seriesData.map((timeSeries, index) => { +const defaultColorPalette = { + blue: ['#1f78d1', '#8fbce8'], + orange: ['#fc9403', '#feca81'], + red: ['#db3b21', '#ed9d90'], + green: ['#1aaa55', '#8dd5aa'], + purple: ['#6666c4', '#d1d1f0'], +}; + +const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; + +export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) { + let usedColors = []; + + function pickColor(name) { + let pick; + if (name && defaultColorPalette[name]) { + pick = name; + } else { + const unusedColors = _.difference(defaultColorOrder, usedColors); + if (unusedColors.length > 0) { + pick = unusedColors[0]; + } else { + usedColors = []; + pick = defaultColorOrder[0]; + } + } + usedColors.push(pick); + return defaultColorPalette[pick]; + } + + const maxValues = queryData.result.map((timeSeries, index) => { const maxValue = d3.max(timeSeries.values.map(d => d.value)); return { maxValue, @@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr const maxValueFromSeries = _.max(maxValues, val => val.maxValue); - let timeSeriesNumber = 1; - let lineColor = '#1f78d1'; - let areaColor = '#8fbce8'; - return seriesData.map((timeSeries) => { + return queryData.result.map((timeSeries, timeSeriesNumber) => { + let metricTag = ''; + let lineColor = ''; + let areaColor = ''; + const timeSeriesScaleX = d3.time.scale() .range([0, graphWidth - 70]); @@ -23,49 +53,30 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr .range([graphHeight - graphHeightOffset, 0]); timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); + timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); const lineFunction = d3.svg.line() + .interpolate('linear') .x(d => timeSeriesScaleX(d.time)) .y(d => timeSeriesScaleY(d.value)); const areaFunction = d3.svg.area() + .interpolate('linear') .x(d => timeSeriesScaleX(d.time)) .y0(graphHeight - graphHeightOffset) - .y1(d => timeSeriesScaleY(d.value)) - .interpolate('linear'); - - switch (timeSeriesNumber) { - case 1: - lineColor = '#1f78d1'; - areaColor = '#8fbce8'; - break; - case 2: - lineColor = '#fc9403'; - areaColor = '#feca81'; - break; - case 3: - lineColor = '#db3b21'; - areaColor = '#ed9d90'; - break; - case 4: - lineColor = '#1aaa55'; - areaColor = '#8dd5aa'; - break; - case 5: - lineColor = '#6666c4'; - areaColor = '#d1d1f0'; - break; - default: - lineColor = '#1f78d1'; - areaColor = '#8fbce8'; - break; - } + .y1(d => timeSeriesScaleY(d.value)); - if (timeSeriesNumber <= 5) { - timeSeriesNumber = timeSeriesNumber += 1; + const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; + const seriesCustomizationData = queryData.series != null && + _.findWhere(queryData.series[0].when, + { value: timeSeriesMetricLabel }); + if (seriesCustomizationData != null) { + metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; + [lineColor, areaColor] = pickColor(seriesCustomizationData.color); } else { - timeSeriesNumber = 1; + metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`; + [lineColor, areaColor] = pickColor(); } return { @@ -75,6 +86,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr values: timeSeries.values, lineColor, areaColor, + metricTag, }; }); } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a09270d6d24..f5f7bb4653d 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1272,16 +1272,16 @@ export default class Notes { `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> - <a href="/${currentUsername}"> + <a href="/${_.escape(currentUsername)}"> <img class="avatar s40" src="${currentUserAvatar}" /> </a> </div> <div class="timeline-content ${discussionClass}"> <div class="note-header"> <div class="note-header-info"> - <a href="/${currentUsername}"> - <span class="hidden-xs">${currentUserFullname}</span> - <span class="note-headline-light">@${currentUsername}</span> + <a href="/${_.escape(currentUsername)}"> + <span class="hidden-xs">${_.escape(currentUsername)}</span> + <span class="note-headline-light">${_.escape(currentUsername)}</span> </a> </div> </div> @@ -1295,6 +1295,9 @@ export default class Notes { </li>` ); + $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname)); + $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`); + return $tempNote; } diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 2ca5ac2912f..f0b44dfa6d8 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,29 +1,45 @@ <script> -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import popover from '../../vue_shared/directives/popover'; -export default { - props: { - pipeline: { - type: Object, - required: true, + export default { + props: { + pipeline: { + type: Object, + required: true, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, - }, - components: { - userAvatarLink, - }, - directives: { - tooltip, - }, - computed: { - user() { - return this.pipeline.user; + components: { + userAvatarLink, }, - }, -}; + directives: { + tooltip, + popover, + }, + computed: { + user() { + return this.pipeline.user; + }, + popoverOptions() { + return { + html: true, + delay: { hide: 600 }, + trigger: 'hover', + placement: 'top', + title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', + content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, + }; + }, + }, + }; </script> <template> - <div class="table-section section-15 hidden-xs hidden-sm"> + <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -57,6 +73,13 @@ export default { :title="pipeline.yaml_errors"> yaml invalid </span> + <a + v-if="pipeline.flags.auto_devops" + class="js-pipeline-url-autodevops label label-info autodevops-badge" + v-popover="popoverOptions" + role="button"> + Auto DevOps + </a> <span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck label label-warning"> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 010063a0240..5e6d6b2fbdc 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -25,8 +25,8 @@ return { endpoint: pipelinesData.endpoint, - cssClass: pipelinesData.cssClass, helpPagePath: pipelinesData.helpPagePath, + autoDevopsPath: pipelinesData.helpAutoDevopsPath, newPipelinePath: pipelinesData.newPipelinePath, canCreatePipeline: pipelinesData.canCreatePipeline, allPath: pipelinesData.allPath, @@ -139,9 +139,7 @@ }; </script> <template> - <div - class="pipelines-container" - :class="cssClass"> + <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> @@ -200,6 +198,7 @@ <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsPath" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 5088d92209f..7aa0c0e8a7f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -17,6 +17,10 @@ required: false, default: false, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, components: { pipelinesTableRowComponent, @@ -54,6 +58,7 @@ :key="model.id" :pipeline="model" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsHelpPath" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index c3f1c426d8a..5b9bb6c3750 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -25,6 +25,10 @@ export default { required: false, default: false, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, components: { asyncButtonComponent, @@ -218,7 +222,10 @@ export default { </div> </div> - <pipeline-url :pipeline="pipeline" /> + <pipeline-url + :pipeline="pipeline" + :auto-devops-help-path="autoDevopsHelpPath" + /> <div class="table-section section-25"> <div diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue new file mode 100644 index 00000000000..80c5d39f736 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue @@ -0,0 +1,104 @@ +<script> +import projectFeatureToggle from './project_feature_toggle.vue'; + +export default { + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + components: { + projectFeatureToggle, + }, + + computed: { + featureEnabled() { + return this.value !== 0; + }, + + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [ + [0, 'Enable feature to choose access level'], + ]; + }, + + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; + }, + }, + + model: { + prop: 'value', + event: 'change', + }, + + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, + + selectOption(e) { + this.$emit('change', Number(e.target.value)); + }, + }, +}; +</script> + +<template> + <div class="project-feature-controls" :data-for="name"> + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <project-feature-toggle + :value="featureEnabled" + @change="toggleFeature" + :disabledInput="disabledInput" + /> + <div class="select-wrapper"> + <select + class="form-control project-repo-select select-control" + @change="selectOption" + :disabled="displaySelectInput" + > + <option + v-for="[optionValue, optionName] in displayOptions" + :key="optionValue" + :value="optionValue" + :selected="optionValue === value" + > + {{optionName}} + </option> + </select> + <i aria-hidden="true" class="fa fa-chevron-down"></i> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue new file mode 100644 index 00000000000..2403c60186a --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue @@ -0,0 +1,51 @@ +<script> +export default { + props: { + name: { + type: String, + required: false, + default: '', + }, + value: { + type: Boolean, + required: true, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + model: { + prop: 'value', + event: 'change', + }, + + methods: { + toggleFeature() { + if (!this.disabledInput) this.$emit('change', !this.value); + }, + }, +}; +</script> + +<template> + <label class="toggle-wrapper"> + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <button + type="button" + aria-label="Toggle" + class="project-feature-toggle" + data-enabled-text="Enabled" + data-disabled-text="Disabled" + :class="{ checked: value, disabled: disabledInput }" + @click="toggleFeature" + /> + </label> +</template> diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue new file mode 100644 index 00000000000..6140d74fea8 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue @@ -0,0 +1,36 @@ +<script> +export default { + props: { + label: { + type: String, + required: false, + default: null, + }, + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, +}; +</script> + +<template> + <div class="project-feature-row"> + <label v-if="label" class="label-light"> + {{label}} + <a v-if="helpPath" :href="helpPath" target="_blank"> + <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i> + </a> + </label> + <span v-if="helpText" class="help-block"> + {{helpText}} + </span> + <slot /> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue new file mode 100644 index 00000000000..326d9105666 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue @@ -0,0 +1,312 @@ +<script> +import projectFeatureSetting from './project_feature_setting.vue'; +import projectFeatureToggle from './project_feature_toggle.vue'; +import projectSettingRow from './project_setting_row.vue'; +import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; +import { toggleHiddenClassBySelector } from '../external'; + +export default { + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + }, + lfsHelpPath: { + type: String, + required: false, + }, + registryHelpPath: { + type: String, + required: false, + }, + }, + + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; + + return { ...defaults, ...this.currentSettings }; + }, + + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, + + computed: { + featureAccessLevelOptions() { + const options = [ + [10, 'Only Project Members'], + ]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, + + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, + + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, + + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; + }, + }, + + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + this.highlightChanges(); + } + }, + + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; + } + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, + + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, + + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, + + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + }, + }, +}; + +</script> + +<template> + <div> + <div class="project-visibility-setting"> + <project-setting-row + label="Project visibility" + :help-path="visibilityHelpPath" + > + <div class="project-feature-controls"> + <div class="select-wrapper"> + <select + name="project[visibility_level]" + v-model="visibilityLevel" + class="form-control select-control" + :disabled="!canChangeVisibilityLevel" + > + <option + :value="visibilityOptions.PRIVATE" + :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" + > + Private + </option> + <option + :value="visibilityOptions.INTERNAL" + :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" + > + Internal + </option> + <option + :value="visibilityOptions.PUBLIC" + :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" + > + Public + </option> + </select> + <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> + </div> + </div> + <span class="help-block">{{ visibilityLevelDescription }}</span> + <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access"> + <input + type="hidden" + name="project[request_access_enabled]" + :value="requestAccessEnabled" + /> + <input type="checkbox" v-model="requestAccessEnabled" /> + Allow users to request access + </label> + </project-setting-row> + </div> + <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }"> + <project-setting-row + label="Issues" + help-text="Lightweight issue tracking system for this project" + > + <project-feature-setting + name="project[project_feature_attributes][issues_access_level]" + :options="featureAccessLevelOptions" + v-model="issuesAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Repository" + help-text="View and edit files in this project" + > + <project-feature-setting + name="project[project_feature_attributes][repository_access_level]" + :options="featureAccessLevelOptions" + v-model="repositoryAccessLevel" + /> + </project-setting-row> + <div class="project-feature-setting-group"> + <project-setting-row + label="Merge requests" + help-text="Submit changes to be merged upstream" + > + <project-feature-setting + name="project[project_feature_attributes][merge_requests_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="mergeRequestsAccessLevel" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + label="Pipelines" + help-text="Build, test, and deploy your changes" + > + <project-feature-setting + name="project[project_feature_attributes][builds_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="buildsAccessLevel" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="registryAvailable" + label="Container registry" + :help-path="registryHelpPath" + help-text="Every project can have its own space to store its Docker images" + > + <project-feature-toggle + name="project[container_registry_enabled]" + v-model="containerRegistryEnabled" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="lfsAvailable" + label="Git Large File Storage" + :help-path="lfsHelpPath" + help-text="Manages large files such as audio, video, and graphics files" + > + <project-feature-toggle + name="project[lfs_enabled]" + v-model="lfsEnabled" + :disabledInput="!repositoryEnabled" + /> + </project-setting-row> + </div> + <project-setting-row + label="Wiki" + help-text="Pages for project documentation" + > + <project-feature-setting + name="project[project_feature_attributes][wiki_access_level]" + :options="featureAccessLevelOptions" + v-model="wikiAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Snippets" + help-text="Share code pastes with others out of Git repository" + > + <project-feature-setting + name="project[project_feature_attributes][snippets_access_level]" + :options="featureAccessLevelOptions" + v-model="snippetsAccessLevel" + /> + </project-setting-row> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/permissions/constants.js b/app/assets/javascripts/projects/permissions/constants.js new file mode 100644 index 00000000000..ce47562f259 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/constants.js @@ -0,0 +1,11 @@ +export const visibilityOptions = { + PRIVATE: 0, + INTERNAL: 10, + PUBLIC: 20, +}; + +export const visibilityLevelDescriptions = { + [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', + [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.', + [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.', +}; diff --git a/app/assets/javascripts/projects/permissions/external.js b/app/assets/javascripts/projects/permissions/external.js new file mode 100644 index 00000000000..460af4a2111 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/external.js @@ -0,0 +1,18 @@ +const selectorCache = []; + +// workaround since we don't have a polyfill for classList.toggle 2nd parameter +export function toggleHiddenClass(element, hidden) { + if (hidden) { + element.classList.add('hidden'); + } else { + element.classList.remove('hidden'); + } +} + +// hide external feature-specific settings when a given feature is disabled +export function toggleHiddenClassBySelector(selector, hidden) { + if (!selectorCache[selector]) { + selectorCache[selector] = document.querySelectorAll(selector); + } + selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden)); +} diff --git a/app/assets/javascripts/projects/permissions/index.js b/app/assets/javascripts/projects/permissions/index.js new file mode 100644 index 00000000000..dbde8dda634 --- /dev/null +++ b/app/assets/javascripts/projects/permissions/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import settingsPanel from './components/settings_panel.vue'; + +export default function initProjectPermissionsSettings() { + const mountPoint = document.querySelector('.js-project-permissions-form'); + const componentPropsEl = document.querySelector('.js-project-permissions-form-data'); + const componentProps = JSON.parse(componentPropsEl.innerHTML); + + return new Vue({ + el: mountPoint, + render: createElement => createElement(settingsPanel, { props: { ...componentProps } }), + }); +} diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue index fa5efef2919..8d0c29177e6 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -27,7 +27,7 @@ export default { listEmptyMessage() { return this.searchFailed ? s__('ProjectsDropdown|Something went wrong on our end.') : - s__('ProjectsDropdown|No projects matched your query'); + s__('ProjectsDropdown|Sorry, no projects matched your search'); }, }, }; diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index b71997234e5..53bc76d0f2d 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -53,7 +53,7 @@ export default { class="form-control" ref="search" v-model="searchQuery" - :placeholder="s__('ProjectsDropdown|Search projects')" + :placeholder="s__('ProjectsDropdown|Search your projects')" /> <i v-if="!searchQuery" diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 7fa5996d600..8635ccece6e 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -41,4 +41,8 @@ export default function initSettingsPanels() { $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); }); + + if (location.hash) { + expandSection($(location.hash)); + } } diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index ff2208baeab..a45b22f3084 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,7 +1,11 @@ import Cookies from 'js-cookie'; export default class UserCallout { - constructor(className = 'user-callout') { + constructor(options = {}) { + this.options = options; + + const className = this.options.className || 'user-callout'; + this.userCalloutBody = $(`.${className}`); this.cookieName = this.userCalloutBody.data('uid'); this.isCalloutDismissed = Cookies.get(this.cookieName); @@ -17,7 +21,11 @@ export default class UserCallout { dismissCallout(e) { const $currentTarget = $(e.currentTarget); - Cookies.set(this.cookieName, 'true', { expires: 365 }); + if (this.options.setCalloutPerProject) { + Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') }); + } else { + Cookies.set(this.cookieName, 'true', { expires: 365 }); + } if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index a31fedee021..73676bd6de7 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -75,7 +75,7 @@ function UsersSelect(currentUser, els) { if (currentUserInfo) { input.value = currentUserInfo.id; - input.dataset.meta = currentUserInfo.name; + input.dataset.meta = _.escape(currentUserInfo.name); } else if (_this.currentUser) { input.value = _this.currentUser.id; } @@ -198,7 +198,7 @@ function UsersSelect(currentUser, els) { }; } $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', user.name).tooltip('fixTitle'); + $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); }; @@ -506,7 +506,7 @@ function UsersSelect(currentUser, els) { img = ""; if (user.beforeDivider != null) { - `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`; + `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; } else { if (avatar) { img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; @@ -518,7 +518,7 @@ function UsersSelect(currentUser, els) { <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'> ${img} <strong class='dropdown-menu-user-full-name'> - ${user.name} + ${_.escape(user.name)} </strong> ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''} </a> @@ -643,11 +643,11 @@ UsersSelect.prototype.formatResult = function(user) { } else { avatar = gon.default_avatar_url; } - return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>"; + return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + _.escape(user.name) + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>"; }; UsersSelect.prototype.formatSelection = function(user) { - return user.name; + return _.escape(user.name); }; UsersSelect.prototype.user = function(user_id, callback) { diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js new file mode 100644 index 00000000000..05fa563cbd0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/popover.js @@ -0,0 +1,20 @@ +/** + * Helper to user bootstrap popover in vue.js. + * Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover + * + * @example + * import popover from 'vue_shared/directives/popover.js'; + * { + * directives: [popover] + * } + * <a v-popover="{options}">popover</a> + */ +export default { + bind(el, binding) { + $(el).popover(binding.value); + }, + + unbind(el) { + $(el).popover('destroy'); + }, +}; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c0524bf6aa3..35e7a10379f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -19,6 +19,7 @@ @import "framework/flash"; @import "framework/forms"; @import "framework/gfm"; +@import "framework/gitlab-theme"; @import "framework/header"; @import "framework/highlight"; @import "framework/issue_box"; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index a85051642dd..706a9cffe87 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -412,11 +412,12 @@ table { .gl-accessibility { &:focus { + display: flex; + align-items: center; top: 1px; left: 1px; width: auto; height: 100%; - line-height: 50px; padding: 0 10px; clip: auto; text-decoration: none; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4a1bb8402c..2bcd23a15e6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -183,7 +183,7 @@ width: auto; top: 100%; left: 0; - z-index: 200; + z-index: 300; min-width: 240px; max-width: 500px; margin-top: 2px; @@ -734,6 +734,11 @@ overflow: hidden; } +@mixin dropdown-item-hover { + background-color: $dropdown-item-hover-bg; + color: $gl-text-color; +} + // TODO: change global style and remove mixin @mixin new-style-dropdown($selector: '') { #{$selector}.dropdown-menu, @@ -760,6 +765,10 @@ padding: 8px 16px; } + &.droplab-item-active button { + @include dropdown-item-hover; + } + a, button, .menu-item { @@ -779,6 +788,8 @@ &:hover, &:active, &:focus { + @include dropdown-item-hover; + background-color: $dropdown-item-hover-bg; color: $gl-text-color; @@ -837,17 +848,30 @@ } } +@media (max-width: $screen-xs-max) { + .navbar-gitlab { + li.header-projects, + li.header-more, + li.header-new, + li.header-user { + position: static; + } + } + + header.navbar-gitlab .dropdown { + .dropdown-menu, + .dropdown-menu-nav { + width: 100%; + min-width: 100%; + } + } +} + @include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.js-namespace-select + '); header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { padding: 0; - - @media (max-width: $screen-xs-max) { - display: table; - left: -50px; - min-width: 300px; - } } .projects-dropdown-container { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss new file mode 100644 index 00000000000..71f764923ff --- /dev/null +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -0,0 +1,265 @@ +/** + * Styles the GitLab application with a specific color theme + */ + +@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { + // Header + + header.navbar-gitlab-new { + background: linear-gradient(to right, $color-900, $color-800); + + .navbar-collapse { + color: $color-200; + } + + .container-fluid { + .navbar-toggle { + border-left: 1px solid lighten($color-700, 10%); + } + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + background-color: rgba($color-200, .2); + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + svg { + fill: currentColor; + } + } + + &.line-separator { + border-left: 1px solid rgba($color-200, .2); + } + } + } + + .navbar-sub-nav { + color: $color-200; + } + + .nav { + > li { + color: $color-200; + + > a { + svg { + fill: $color-200; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $color-200; + } + } + + &:hover, + &:focus { + @media (min-width: $screen-sm-min) { + background-color: rgba($color-200, .2); + } + + svg { + fill: currentColor; + } + } + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + &:hover { + svg { + fill: $color-900; + } + } + } + + .impersonated-user, + .impersonated-user:hover { + svg { + fill: $color-900; + } + } + } + } + } + + .title { + > a { + &:hover, + &:focus { + background-color: rgba($color-200, .2); + } + } + } + + .search { + form { + background-color: rgba($color-200, .2); + + &:hover { + background-color: rgba($color-200, .3); + } + } + + .location-badge { + color: $color-100; + background-color: rgba($color-200, .1); + border-right: 1px solid $color-800; + } + + .search-input::placeholder { + color: rgba($color-200, .8); + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: rgba($color-200, .8); + } + } + + &.search-active { + form { + background-color: $white-light; + } + + .location-badge { + color: $gl-text-color; + } + + .search-input-wrap { + .search-icon { + color: rgba($color-200, .8); + } + } + } + } + + .btn-sign-in { + background-color: $color-100; + color: $color-900; + } + + + // Sidebar + .nav-sidebar li.active { + box-shadow: inset 4px 0 0 $color-700; + + > a { + color: $color-900; + } + + svg { + fill: $color-900; + } + } +} + + +body { + &.ui_indigo { + @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light); + } + + &.ui_dark { + @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light); + } + + &.ui_blue { + @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light); + } + + &.ui_green { + @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light); + } + + &.ui_light { + @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); + + header.navbar-gitlab-new { + background: $theme-gray-100; + box-shadow: 0 2px 0 0 $border-color; + + .logo-text svg { + fill: $theme-gray-900; + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + color: $theme-gray-900; + } + + &.active > a { + color: $white-light; + + &:hover { + color: $white-light; + } + } + } + } + + .container-fluid { + .navbar-toggle, + .navbar-toggle:hover { + color: $theme-gray-700; + border-left: 1px solid $theme-gray-200; + } + } + } + + .search { + form { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $border-color; + + &:hover { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $blue-100; + + .location-badge { + box-shadow: inset 0 0 0 1px $blue-100; + } + } + } + + .search-input-wrap { + .search-icon { + color: $theme-gray-200; + } + } + + .location-badge { + color: $theme-gray-700; + box-shadow: inset 0 0 0 1px $border-color; + background-color: $nav-badge-bg; + border-right: 0; + } + } + + .nav-sidebar li.active { + > a { + color: $theme-gray-900; + } + + svg { + fill: $theme-gray-900; + } + } + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b00a2d053e2..ab3c34df1fb 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -111,7 +111,6 @@ header { svg { height: 16px; width: 23px; - fill: currentColor; } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index e20108b171b..5ffa67a1220 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -288,11 +288,7 @@ display: flex; max-width: 350px; overflow: hidden; - - @media(max-width: $screen-xs-max) { - width: 100%; - max-width: none; - } + float: right; .new-project-item-link { white-space: nowrap; @@ -305,6 +301,23 @@ } } +.empty-state .project-item-select-holder.btn-group { + float: none; + display: inline-block; + + .btn { + // overrides styles applied to plain `.empty-state .btn` + margin: 10px 0; + max-width: 300px; + width: auto; + + @media(max-width: $screen-xs-max) { + max-width: 250px; + } + + } +} + .new-project-item-select-button .fa-caret-down { margin-left: 2px; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 88b08998dfd..3857226cddb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -74,6 +74,8 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +// GitLab themes + $indigo-50: #f7f7ff; $indigo-100: #ebebfa; $indigo-200: #d1d1f0; @@ -86,6 +88,43 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; +$theme-gray-50: #fafafa; +$theme-gray-100: #f2f2f2; +$theme-gray-200: #dfdfdf; +$theme-gray-300: #cccccc; +$theme-gray-400: #bababa; +$theme-gray-500: #a7a7a7; +$theme-gray-600: #949494; +$theme-gray-700: #707070; +$theme-gray-800: #4f4f4f; +$theme-gray-900: #2e2e2e; +$theme-gray-950: #1f1f1f; + +$theme-blue-50: #f4f8fc; +$theme-blue-100: #e6edf5; +$theme-blue-200: #c8d7e6; +$theme-blue-300: #97b3cf; +$theme-blue-400: #648cb4; +$theme-blue-500: #4a79a8; +$theme-blue-600: #3e6fa0; +$theme-blue-700: #305c88; +$theme-blue-800: #25496e; +$theme-blue-900: #1a3652; +$theme-blue-950: #0f2235; + +$theme-green-50: #f2faf6; +$theme-green-100: #e4f3ea; +$theme-green-200: #c0dfcd; +$theme-green-300: #8ac2a1; +$theme-green-400: #52a274; +$theme-green-500: #35935c; +$theme-green-600: #288a50; +$theme-green-700: #1c7441; +$theme-green-800: #145d33; +$theme-green-900: #0d4524; +$theme-green-950: #072d16; + + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $almost-black: #242424; @@ -540,6 +579,11 @@ $project-breadcrumb-color: #999; $project-private-forks-notice-odd: $green-600; $project-network-controls-color: #888; +$feature-toggle-color: #fff; +$feature-toggle-text-color: #fff; +$feature-toggle-color-disabled: #999; +$feature-toggle-color-enabled: #4a8bee; + /* * Runners */ diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index e4b52ab480d..8e095cbdd7e 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -9,10 +9,20 @@ header.navbar-gitlab-new { color: $white-light; - background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; min-height: $new-navbar-height; + .logo-text { + line-height: initial; + + svg { + width: 55px; + height: 14px; + margin: 0; + fill: $white-light; + } + } + .header-content { display: -webkit-flex; display: flex; @@ -38,10 +48,10 @@ header.navbar-gitlab-new { img { height: 28px; - margin-right: 10px; + margin-right: 8px; } - > a { + a { display: -webkit-flex; display: flex; align-items: center; @@ -54,22 +64,6 @@ header.navbar-gitlab-new { margin-right: 8px; } } - - .logo-text { - line-height: initial; - - svg { - width: 55px; - height: 14px; - margin: 0; - fill: $white-light; - } - } - - &:hover, - &:focus { - background-color: rgba($indigo-200, .2); - } } } @@ -106,7 +100,6 @@ header.navbar-gitlab-new { .navbar-collapse { padding-left: 0; - color: $indigo-200; box-shadow: 0; @media (max-width: $screen-xs-max) { @@ -132,7 +125,6 @@ header.navbar-gitlab-new { font-size: 14px; text-align: center; color: currentColor; - border-left: 1px solid lighten($indigo-700, 10%); &:hover, &:focus, @@ -167,63 +159,49 @@ header.navbar-gitlab-new { will-change: color; margin: 4px 2px; padding: 6px 8px; - color: $indigo-200; height: 32px; @media (max-width: $screen-xs-max) { padding: 0; } - svg { - fill: $indigo-200; - } - &.header-user-dropdown-toggle { margin-left: 2px; .header-user-avatar { - border-color: $indigo-200; margin-right: 0; } } - } - - .header-new-dropdown-toggle { - margin-right: 0; - } - > a:hover, - > a:focus { - text-decoration: none; - outline: 0; - opacity: 1; - color: $white-light; - - @media (min-width: $screen-sm-min) { - background-color: rgba($indigo-200, .2); - } + &:hover, + &:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; - svg { - fill: currentColor; - } + svg { + fill: currentColor; + } - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } } } } + .header-new-dropdown-toggle { + margin-right: 0; + } + .impersonated-user, .impersonated-user:hover { margin-right: 1px; background-color: $white-light; border-top-right-radius: 0; border-bottom-right-radius: 0; - - svg { - fill: $indigo-900; - } } .impersonation-btn, @@ -241,8 +219,6 @@ header.navbar-gitlab-new { &.active > a, &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -256,7 +232,6 @@ header.navbar-gitlab-new { display: -webkit-flex; display: flex; margin: 0 0 0 6px; - color: $indigo-200; .dropdown-chevron { position: relative; @@ -274,17 +249,6 @@ header.navbar-gitlab-new { text-decoration: none; outline: 0; color: $white-light; - background-color: rgba($indigo-200, .2); - - svg { - fill: currentColor; - } - } - - &.active > a, - &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -309,7 +273,6 @@ header.navbar-gitlab-new { } &.line-separator { - border-left: 1px solid rgba($indigo-200, .2); margin: 8px; } } @@ -339,17 +302,14 @@ header.navbar-gitlab-new { height: 32px; border: 0; border-radius: $border-radius-default; - background-color: rgba($indigo-200, .2); transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { - background-color: rgba($indigo-200, .3); box-shadow: none; } } &.search-active form { - background-color: $white-light; box-shadow: none; .search-input { @@ -377,43 +337,26 @@ header.navbar-gitlab-new { } .search-input::placeholder { - color: rgba($indigo-200, .8); transition: color ease-in-out 0.15s; } .location-badge { font-size: 12px; - color: $indigo-100; - background-color: rgba($indigo-200, .1); - will-change: color; margin: -4px 4px -4px -4px; line-height: 25px; padding: 4px 8px; border-radius: 2px 0 0 2px; - border-right: 1px solid $indigo-800; height: 32px; transition: border-color ease-in-out 0.15s; } - .search-input-wrap { - .search-icon, - .clear-icon { - color: rgba($indigo-200, .8); - } - } - &.search-active { .location-badge { - color: $gl-text-color; background-color: $nav-badge-bg; border-color: $border-color; } .search-input-wrap { - .search-icon { - color: rgba($indigo-200, .8); - } - .clear-icon { color: $white-light; } @@ -515,20 +458,8 @@ header.navbar-gitlab-new { } } -.top-area { - .nav-controls-new-nav { - .dropdown { - @media (min-width: $screen-sm-min) { - margin-right: 0; - } - } - } -} - .btn-sign-in { margin-top: 3px; - background-color: $indigo-100; - color: $indigo-900; font-weight: $gl-font-weight-bold; &:hover { diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index fd5e344d8c9..4bbd30056a9 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -106,11 +106,8 @@ $new-sidebar-collapsed-width: 50px; overflow-x: hidden; } - .badge, - .sidebar-context-title { - display: none; - } - + .badge:not(.fly-out-badge), + .sidebar-context-title, .nav-item-name { display: none; } @@ -118,6 +115,10 @@ $new-sidebar-collapsed-width: 50px; .sidebar-top-level-items > li > a { min-height: 44px; } + + .fly-out-top-item { + display: block; + } } &.nav-sidebar-expanded { @@ -154,16 +155,9 @@ $new-sidebar-collapsed-width: 50px; } li.active { - box-shadow: inset 4px 0 0 $active-border; - > a { - color: $active-color; font-weight: $gl-font-weight-bold; } - - svg { - fill: $active-color; - } } @media (max-width: $screen-xs-max) { @@ -179,6 +173,10 @@ $new-sidebar-collapsed-width: 50px; width: 16px; } } + + .fly-out-top-item { + display: none; + } } .nav-sidebar-inner-scroll { @@ -249,7 +247,7 @@ $new-sidebar-collapsed-width: 50px; left: $new-sidebar-width; min-width: 150px; margin-top: -1px; - padding: 8px 1px; + padding: 4px 1px; background-color: $white-light; box-shadow: 2px 1px 3px $dropdown-shadow-color; border: 1px solid $gray-darker; @@ -270,6 +268,13 @@ $new-sidebar-collapsed-width: 50px; margin-top: 1px; } + .divider { + height: 1px; + margin: 4px -1px; + padding: 0; + background-color: $dropdown-divider-color; + } + > .active { box-shadow: none; @@ -309,7 +314,7 @@ $new-sidebar-collapsed-width: 50px; font-weight: $gl-font-weight-bold; } - .sidebar-sub-level-items { + .sidebar-sub-level-items:not(.is-fly-out-only) { display: block; } } @@ -407,6 +412,19 @@ $new-sidebar-collapsed-width: 50px; } } +.fly-out-top-item { + > a { + display: flex; + } + + .fly-out-badge { + margin-left: 8px; + } +} + +.fly-out-top-item-name { + flex: 1; +} // Mobile nav diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 314dd2d1a21..700be173039 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -117,13 +117,12 @@ } .board-title { - position: initial; padding: 0; border-bottom: 0; > span { display: block; - transform: rotate(90deg) translate(25px, 0); + transform: rotate(90deg) translate(35px, 10px); } } @@ -151,11 +150,18 @@ } .board-header { - border-top-left-radius: $border-radius-default; - border-top-right-radius: $border-radius-default; + position: relative; - &.has-border { + &.has-border::before { border-top: 3px solid; + border-color: inherit; + border-top-left-radius: $border-radius-default; + border-top-right-radius: $border-radius-default; + content: ''; + position: absolute; + width: calc(100% + 2px); + top: 0; + left: 0; margin-top: -1px; margin-right: -1px; margin-left: -1px; @@ -176,12 +182,16 @@ } .board-title { - position: relative; margin: 0; - padding: $gl-padding; - padding-bottom: ($gl-padding + 3px); + padding: 12px $gl-padding; font-size: 1em; border-bottom: 1px solid $border-color; + display: flex; + align-items: center; +} + +.board-title-text { + margin-right: auto; } .board-delete { @@ -221,43 +231,10 @@ } } -.slide-down-enter { - transform: translateY(-100%); -} - -.slide-down-enter-active { - transition: transform $fade-in-duration; - - + .board-list { - transform: translateY(-136px); - transition: none; - } -} - -.slide-down-enter-to { - + .board-list { - transform: translateY(0); - transition: transform $fade-in-duration ease; - } -} - -.slide-down-leave { - transform: translateY(0); -} - -.slide-down-leave-active { - transition: all $fade-in-duration; - transform: translateY(-136px); - - + .board-list { - transition: transform $fade-in-duration ease; - transform: translateY(-136px); - } -} - .board-list-component { height: calc(100% - 49px); overflow: hidden; + position: relative; } .board-list { @@ -429,7 +406,7 @@ } .board-new-issue-form { - z-index: 1; + z-index: 4; margin: 5px; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 587a202d6dd..994707422bb 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -226,6 +226,14 @@ vertical-align: baseline; } + a.autodevops-badge { + color: $white-light; + } + + a.autodevops-link { + color: $gl-link-color; + } + .commit-row-description { font-size: 14px; padding: 10px 15px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cb8815e4775..296b6310552 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -202,6 +202,10 @@ .btn-group.open .dropdown-toggle { box-shadow: none; } + + .pipeline-tags .label-container { + white-space: normal; + } } .stage-cell { @@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle { .pipelines-container .top-area .nav-controls > .btn:last-child { float: none; } + +.autodevops-title { + font-weight: $gl-font-weight-normal; + line-height: 1.5; +} diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 305feaacaa1..c197494b152 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,3 +1,67 @@ +@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) { + .one { + background-color: $color-1; + border-top-left-radius: $border-radius-default; + } + + .two { + background-color: $color-2; + border-top-right-radius: $border-radius-default; + } + + .three { + background-color: $color-3; + border-bottom-left-radius: $border-radius-default; + } + + .four { + background-color: $color-4; + border-bottom-right-radius: $border-radius-default; + } +} + +.application-theme { + label { + margin-right: 20px; + text-align: center; + } + + .preview { + font-size: 0; + margin-bottom: 10px; + + &.indigo { + @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500); + } + + &.dark { + @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600); + } + + &.light { + @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100); + } + + &.blue { + @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500); + } + + &.green { + @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500); + } + } + + .preview-row { + display: block; + } + + .quadrant { + display: inline-block; + height: 50px; + width: 80px; + } +} + .syntax-theme { label { margin-right: 20px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index dd600a27545..94e4f4334d4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -10,41 +10,6 @@ .edit-project, .import-project { - .sharing-and-permissions { - .header { - padding-top: $gl-vert-padding; - } - - .label-light { - margin-bottom: 0; - } - - .help-block { - margin-top: 0; - } - - .form-group { - margin-bottom: 5px; - } - - > .form-group { - padding-left: 0; - } - - select option[disabled] { - display: none; - } - } - - select { - transition: background 2s ease-out; - - &.highlight-changes { - background: $highlight-changes-color; - transition: none; - } - } - .help-block { margin-bottom: 10px; } @@ -90,6 +55,162 @@ } } +.toggle-wrapper { + margin-top: 5px; +} + +.project-feature-row > .toggle-wrapper { + margin: 10px 0; +} + +.project-visibility-setting, +.project-feature-settings { + border: 1px solid $border-color; + padding: 10px 32px; + + @media (max-width: $screen-xs-min) { + padding: 10px 20px; + } +} + +.project-visibility-setting .request-access { + line-height: 2; +} + +.project-feature-settings { + background: $gray-lighter; + border-top: none; + margin-bottom: 16px; +} + +.project-repo-select { + transition: background 2s ease-out; + + &:disabled { + opacity: 0.75; + } + + .highlight-changes & { + background: $highlight-changes-color; + transition: none; + } +} + +.project-feature-controls { + display: flex; + align-items: center; + margin: 8px 0; + max-width: 432px; + + .toggle-wrapper { + flex: 0; + margin-right: 10px; + } + + .select-wrapper { + flex: 1; + } +} + +.project-feature-setting-group { + padding-left: 32px; + + .project-feature-controls { + max-width: 400px; + } + + @media (max-width: $screen-xs-min) { + padding-left: 20px; + } +} + +.project-feature-toggle { + position: relative; + border: none; + outline: 0; + display: block; + width: 100px; + height: 24px; + cursor: pointer; + user-select: none; + background: $feature-toggle-color-disabled; + border-radius: 12px; + padding: 3px; + transition: all .4s ease; + + &::selection, + &::before::selection, + &::after::selection { + background: none; + } + + &::before { + color: $feature-toggle-text-color; + font-size: 12px; + line-height: 24px; + position: absolute; + top: 0; + left: 25px; + right: 5px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + animation: animate-disabled .2s ease-in; + content: attr(data-disabled-text); + } + + &::after { + position: relative; + display: block; + content: ""; + width: 22px; + height: 18px; + left: 0; + border-radius: 9px; + background: $feature-toggle-color; + transition: all .2s ease; + } + + &.checked { + background: $feature-toggle-color-enabled; + + &::before { + left: 5px; + right: 25px; + animation: animate-enabled .2s ease-in; + content: attr(data-enabled-text); + } + + &::after { + left: calc(100% - 22px); + } + } + + &.disabled { + opacity: 0.4; + cursor: not-allowed; + } + + @media (max-width: $screen-xs-min) { + width: 50px; + + &::before, + &.checked::before { + display: none; + } + } + + @keyframes animate-enabled { + 0%, 35% { opacity: 0; } + 100% { opacity: 1; } + } + + @keyframes animate-disabled { + 0%, 35% { opacity: 0; } + 100% { opacity: 1; } + } +} + .project-home-panel, .group-home-panel { padding-top: 24px; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index efc47861768..69abb13add4 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -43,8 +43,10 @@ display: inline-block; } -.blob-viewer[data-type="rich"] { - margin: 20px; +@media (min-width: $screen-md-min) { + .blob-viewer[data-type="rich"] { + margin: 20px; + } } .repository-view { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 615020ca856..13dd7b5a780 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -166,7 +166,7 @@ input[type="checkbox"]:hover { .dropdown-menu { transition-duration: 100ms, 75ms; transition-delay: 75ms, 100ms; - transform: translateY(13px); + transform: translateY(7px); opacity: 1; } } diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 762e36ee2e9..c49b6459452 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController before_action :finder, only: [:edit, :update, :destroy] def index - @broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page]) + @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page]) @broadcast_message = BroadcastMessage.new end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 05e749c00c0..e85cdcb8db7 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,7 +1,7 @@ class Admin::DashboardController < Admin::ApplicationController def index - @projects = Project.without_deleted.with_route.limit(10) - @users = User.limit(10) - @groups = Group.with_route.limit(10) + @projects = Project.order_id_desc.without_deleted.with_route.limit(10) + @users = User.order_id_desc.limit(10) + @groups = Group.order_id_desc.with_route.limit(10) end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index a99563b7100..cbcef70e957 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -17,7 +17,7 @@ class Admin::UsersController < Admin::ApplicationController end def keys - @keys = user.keys + @keys = user.keys.order_id_desc end def new @@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController :provider, :remember_me, :skype, + :theme_id, :twitter, :username, :website_url diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb new file mode 100644 index 00000000000..b2675025fc0 --- /dev/null +++ b/app/controllers/boards/application_controller.rb @@ -0,0 +1,21 @@ +module Boards + class ApplicationController < ::ApplicationController + respond_to :json + + rescue_from ActiveRecord::RecordNotFound, with: :record_not_found + + private + + def board + @board ||= Board.find(params[:board_id]) + end + + def board_parent + @board_parent ||= board.parent + end + + def record_not_found(exception) + render json: { error: exception.message }, status: :not_found + end + end +end diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb new file mode 100644 index 00000000000..8d4ec2d6d9d --- /dev/null +++ b/app/controllers/boards/issues_controller.rb @@ -0,0 +1,90 @@ +module Boards + class IssuesController < Boards::ApplicationController + include BoardsResponses + + before_action :authorize_read_issue, only: [:index] + before_action :authorize_create_issue, only: [:create] + before_action :authorize_update_issue, only: [:update] + skip_before_action :authenticate_user!, only: [:index] + + def index + issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute + issues = issues.page(params[:page]).per(params[:per] || 20) + make_sure_position_is_set(issues) + + render json: { + issues: serialize_as_json(issues.preload(:project)), + size: issues.total_count + } + end + + def create + service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params) + issue = service.execute + + if issue.valid? + render json: serialize_as_json(issue) + else + render json: issue.errors, status: :unprocessable_entity + end + end + + def update + service = Boards::Issues::MoveService.new(board_parent, current_user, move_params) + + if service.execute(issue) + head :ok + else + head :unprocessable_entity + end + end + + private + + def make_sure_position_is_set(issues) + issues.each do |issue| + issue.move_to_end && issue.save unless issue.relative_position + end + end + + def issue + @issue ||= issues_finder.execute.find(params[:id]) + end + + def filter_params + params.merge(board_id: params[:board_id], id: params[:list_id]) + .reject { |_, value| value.nil? } + end + + def issues_finder + IssuesFinder.new(current_user, project_id: board_parent.id) + end + + def project + board_parent + end + + def move_params + params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_id, :move_after_id) + end + + def issue_params + params.require(:issue) + .permit(:title, :milestone_id, :project_id) + .merge(board_id: params[:board_id], list_id: params[:list_id], request: request) + end + + def serialize_as_json(resource) + resource.as_json( + labels: true, + only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], + include: { + project: { only: [:id, :path] }, + assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, + milestone: { only: [:id, :title] } + }, + user: current_user + ) + end + end +end diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb new file mode 100644 index 00000000000..381fd4d7508 --- /dev/null +++ b/app/controllers/boards/lists_controller.rb @@ -0,0 +1,75 @@ +module Boards + class ListsController < Boards::ApplicationController + include BoardsResponses + + before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate] + before_action :authorize_read_list, only: [:index] + skip_before_action :authenticate_user!, only: [:index] + + def index + lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board) + + render json: serialize_as_json(lists) + end + + def create + list = Boards::Lists::CreateService.new(board.parent, current_user, list_params).execute(board) + + if list.valid? + render json: serialize_as_json(list) + else + render json: list.errors, status: :unprocessable_entity + end + end + + def update + list = board.lists.movable.find(params[:id]) + service = Boards::Lists::MoveService.new(board_parent, current_user, move_params) + + if service.execute(list) + head :ok + else + head :unprocessable_entity + end + end + + def destroy + list = board.lists.destroyable.find(params[:id]) + service = Boards::Lists::DestroyService.new(board_parent, current_user) + + if service.execute(list) + head :ok + else + head :unprocessable_entity + end + end + + def generate + service = Boards::Lists::GenerateService.new(board_parent, current_user) + + if service.execute(board) + render json: serialize_as_json(board.lists.movable) + else + head :unprocessable_entity + end + end + + private + + def list_params + params.require(:list).permit(:label_id) + end + + def move_params + params.require(:list).permit(:position) + end + + def serialize_as_json(resource) + resource.as_json( + only: [:id, :list_type, :position], + methods: [:title], + label: true + ) + end + end +end diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb new file mode 100644 index 00000000000..2c9c095a5d7 --- /dev/null +++ b/app/controllers/concerns/boards_responses.rb @@ -0,0 +1,42 @@ +module BoardsResponses + def authorize_read_list + authorize_action_for!(board.parent, :read_list) + end + + def authorize_read_issue + authorize_action_for!(board.parent, :read_issue) + end + + def authorize_update_issue + authorize_action_for!(issue, :admin_issue) + end + + def authorize_create_issue + authorize_action_for!(project, :admin_issue) + end + + def authorize_admin_list + authorize_action_for!(board.parent, :admin_list) + end + + def authorize_action_for!(resource, ability) + return render_403 unless can?(current_user, ability, resource) + end + + def respond_with_boards + respond_with(@boards) + end + + def respond_with_board + respond_with(@board) + end + + def respond_with(resource) + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(resource) + end + end + end +end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 23909bd2d39..0d0e53d4b76 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -10,6 +10,22 @@ module IssuableCollections private + def set_issues_index + @collection_type = "Issue" + @issues = issues_collection + @issues = @issues.page(params[:page]) + @issuable_meta_data = issuable_meta_data(@issues, @collection_type) + @total_pages = issues_page_count(@issues) + + return if redirect_out_of_range(@issues, @total_pages) + + if params[:label_name].present? + @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute + end + + @users = [] + end + def issues_collection issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace) end diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 742157d113d..8057a0b455c 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,5 +1,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController def index + @sort = params[:sort] || 'id_desc' + @groups = if params[:parent_id] && Group.supports_nested_groups? parent = Group.find_by(id: params[:parent_id]) @@ -15,7 +17,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? @groups = @groups.includes(:route) - @groups = @groups.sort(@sort = params[:sort]) + @groups = @groups.sort(@sort) @groups = @groups.page(params[:page]) respond_to do |format| diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 994e736d66e..3769a2cde33 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController # Authorize before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] - before_action :authorize_create_group!, only: [:new, :create] + before_action :authorize_create_group!, only: [:new] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :group_merge_requests, only: [:merge_requests] @@ -25,14 +25,7 @@ class GroupsController < Groups::ApplicationController end def new - @group = Group.new - - if params[:parent_id].present? - parent = Group.find_by(id: params[:parent_id]) - if can?(current_user, :create_subgroup, parent) - @group.parent = parent - end - end + @group = Group.new(params.permit(:parent_id)) end def create @@ -128,9 +121,14 @@ class GroupsController < Groups::ApplicationController end def authorize_create_group! - unless can?(current_user, :create_group) - return render_404 - end + allowed = if params[:parent_id].present? + parent = Group.find_by(id: params[:parent_id]) + can?(current_user, :create_subgroup, parent) + else + can?(current_user, :create_group) + end + + render_404 unless allowed end def determine_layout diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 17b66df43e7..ddb67d1c4d1 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -1,7 +1,7 @@ class Profiles::EmailsController < Profiles::ApplicationController def index @primary = current_user.email - @emails = current_user.emails + @emails = current_user.emails.order_id_desc end def create diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 88f49da555a..f9f0e8eef83 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -2,7 +2,7 @@ class Profiles::KeysController < Profiles::ApplicationController skip_before_action :authenticate_user!, only: [:get_keys] def index - @keys = current_user.keys + @keys = current_user.keys.order_id_desc @key = Key.new end diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 1e557c47638..cce2a847b53 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController :color_scheme_id, :layout, :dashboard, - :project_view + :project_view, + :theme_id ) end end diff --git a/app/controllers/projects/boards/application_controller.rb b/app/controllers/projects/boards/application_controller.rb deleted file mode 100644 index dad38fff6b9..00000000000 --- a/app/controllers/projects/boards/application_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Projects - module Boards - class ApplicationController < Projects::ApplicationController - respond_to :json - - rescue_from ActiveRecord::RecordNotFound, with: :record_not_found - - private - - def record_not_found(exception) - render json: { error: exception.message }, status: :not_found - end - end - end -end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb deleted file mode 100644 index 653e7bc7e40..00000000000 --- a/app/controllers/projects/boards/issues_controller.rb +++ /dev/null @@ -1,94 +0,0 @@ -module Projects - module Boards - class IssuesController < Boards::ApplicationController - before_action :authorize_read_issue!, only: [:index] - before_action :authorize_create_issue!, only: [:create] - before_action :authorize_update_issue!, only: [:update] - - def index - issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute - issues = issues.page(params[:page]).per(params[:per] || 20) - make_sure_position_is_set(issues) - - render json: { - issues: serialize_as_json(issues), - size: issues.total_count - } - end - - def create - service = ::Boards::Issues::CreateService.new(project, current_user, issue_params) - issue = service.execute - - if issue.valid? - render json: serialize_as_json(issue) - else - render json: issue.errors, status: :unprocessable_entity - end - end - - def update - service = ::Boards::Issues::MoveService.new(project, current_user, move_params) - - if service.execute(issue) - head :ok - else - head :unprocessable_entity - end - end - - private - - def make_sure_position_is_set(issues) - issues.each do |issue| - issue.move_to_end && issue.save unless issue.relative_position - end - end - - def issue - @issue ||= - IssuesFinder.new(current_user, project_id: project.id) - .execute - .where(iid: params[:id]) - .first! - end - - def authorize_read_issue! - return render_403 unless can?(current_user, :read_issue, project) - end - - def authorize_create_issue! - return render_403 unless can?(current_user, :admin_issue, project) - end - - def authorize_update_issue! - return render_403 unless can?(current_user, :update_issue, issue) - end - - def filter_params - params.merge(board_id: params[:board_id], id: params[:list_id]) - .reject { |_, value| value.nil? } - end - - def move_params - params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid) - end - - def issue_params - params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request) - end - - def serialize_as_json(resource) - resource.as_json( - labels: true, - only: [:id, :iid, :title, :confidential, :due_date, :relative_position], - include: { - assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, - milestone: { only: [:id, :title] } - }, - user: current_user - ) - end - end - end -end diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb deleted file mode 100644 index ad53bb749a0..00000000000 --- a/app/controllers/projects/boards/lists_controller.rb +++ /dev/null @@ -1,86 +0,0 @@ -module Projects - module Boards - class ListsController < Boards::ApplicationController - before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate] - before_action :authorize_read_list!, only: [:index] - - def index - lists = ::Boards::Lists::ListService.new(project, current_user).execute(board) - - render json: serialize_as_json(lists) - end - - def create - list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board) - - if list.valid? - render json: serialize_as_json(list) - else - render json: list.errors, status: :unprocessable_entity - end - end - - def update - list = board.lists.movable.find(params[:id]) - service = ::Boards::Lists::MoveService.new(project, current_user, move_params) - - if service.execute(list) - head :ok - else - head :unprocessable_entity - end - end - - def destroy - list = board.lists.destroyable.find(params[:id]) - service = ::Boards::Lists::DestroyService.new(project, current_user) - - if service.execute(list) - head :ok - else - head :unprocessable_entity - end - end - - def generate - service = ::Boards::Lists::GenerateService.new(project, current_user) - - if service.execute(board) - render json: serialize_as_json(board.lists.movable) - else - head :unprocessable_entity - end - end - - private - - def authorize_admin_list! - return render_403 unless can?(current_user, :admin_list, project) - end - - def authorize_read_list! - return render_403 unless can?(current_user, :read_list, project) - end - - def board - @board ||= project.boards.find(params[:board_id]) - end - - def list_params - params.require(:list).permit(:label_id) - end - - def move_params - params.require(:list).permit(:position) - end - - def serialize_as_json(resource) - resource.as_json( - only: [:id, :list_type, :position], - methods: [:title], - label: true - ) - end - end - end -end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 808affa4f98..d1b99ecce4a 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,32 +1,31 @@ class Projects::BoardsController < Projects::ApplicationController + include BoardsResponses include IssuableCollections before_action :authorize_read_board!, only: [:index, :show] + before_action :assign_endpoint_vars def index - @boards = ::Boards::ListService.new(project, current_user).execute - - respond_to do |format| - format.html - format.json do - render json: serialize_as_json(@boards) - end - end + @boards = Boards::ListService.new(project, current_user).execute + + respond_with_boards end def show @board = project.boards.find(params[:id]) - respond_to do |format| - format.html - format.json do - render json: serialize_as_json(@board) - end - end + respond_with_board end private + def assign_endpoint_vars + @boards_endpoint = project_boards_url(project) + @bulk_issues_path = bulk_update_project_issues_path(project) + @namespace_path = project.namespace.full_path + @labels_endpoint = project_labels_path(project) + end + def authorize_read_board! return access_denied! unless can?(current_user, :read_board, project) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ab9f132b502..8990c919ca0 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,6 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :check_issues_available! before_action :issue, except: [:index, :new, :create, :bulk_update] + before_action :set_issues_index, only: [:index] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -23,20 +24,6 @@ class Projects::IssuesController < Projects::ApplicationController respond_to :html def index - @collection_type = "Issue" - @issues = issues_collection - @issues = @issues.page(params[:page]) - @issuable_meta_data = issuable_meta_data(@issues, @collection_type) - @total_pages = issues_page_count(@issues) - - return if redirect_out_of_range(@issues, @total_pages) - - if params[:label_name].present? - @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute - end - - @users = [] - if params[:assignee_id].present? assignee = User.find_by_id(params[:assignee_id]) @users.push(assignee) if assignee diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 109418c73f7..d60a24d3f1d 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic @merge_request.merge_request_diff end - @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } if params[:start_sha].present? diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 9d24ebe2138..abab2e2f0c9 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController end def update - if @project.update_attributes(update_params) + if @project.update(update_params) flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." redirect_to project_settings_ci_cd_path(@project) else @@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController private - def create_params - params.require(:pipeline).permit(:ref) - end - def update_params params.require(:project).permit( - :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, :auto_cancel_pending_pipelines, :ci_config_path + :runners_token, :builds_enabled, :build_allow_git_fetch, + :build_timeout_in_minutes, :build_coverage_regex, :public_builds, + :auto_cancel_pending_pipelines, :ci_config_path, + auto_devops_attributes: [:id, :domain, :enabled] ) end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index f8ff7413b53..d925dcd21ff 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -47,6 +47,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController end end + def import + @projects = current_user.authorized_projects.order_id_desc + end + def apply_import source_project = Project.find(params[:source_project_id]) diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 15a2ff56b92..b029b31f9af 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -8,6 +8,7 @@ module Projects define_secret_variables define_triggers_variables define_badges_variables + define_auto_devops_variables end private @@ -42,6 +43,10 @@ module Projects badge.new(@project, @ref).metadata end end + + def define_auto_devops_variables + @auto_devops = @project.auto_devops || ProjectAutoDevops.new + end end end end diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb index 79eb45568be..038d5565a1e 100644 --- a/app/finders/move_to_project_finder.rb +++ b/app/finders/move_to_project_finder.rb @@ -9,6 +9,7 @@ class MoveToProjectFinder projects = @user.projects_where_can_admin_issues projects = projects.search(search) if search.present? projects = projects.excluding_project(from_project) + projects = projects.order_id_desc # infinite scroll using offset projects = projects.where('projects.id < ?', offset_id) if offset_id.present? diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index fa6fea2588a..eac6095d8dc 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -121,7 +121,7 @@ class ProjectsFinder < UnionFinder end def sort(items) - params[:sort].present? ? items.sort(params[:sort]) : items + params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc end def by_archived(projects) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index b276116f0c6..3502bf08971 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -118,7 +118,7 @@ class TodosFinder end def sort(items) - params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) + params[:sort] ? items.sort(params[:sort]) : items.order_id_desc end def by_action(items) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b93f5f0af1c..7bd34df5c95 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -115,6 +115,7 @@ module ApplicationSettingsHelper :after_sign_up_text, :akismet_api_key, :akismet_enabled, + :auto_devops_enabled, :clientside_sentry_dsn, :clientside_sentry_enabled, :container_registry_token_expire_delay, diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb new file mode 100644 index 00000000000..4ff38f86b5f --- /dev/null +++ b/app/helpers/auto_devops_helper.rb @@ -0,0 +1,7 @@ +module AutoDevopsHelper + def show_auto_devops_callout?(project) + show_callout?('auto_devops_settings_dismissed') && + can?(current_user, :admin_pipeline, project) && + project.has_auto_devops_implicitly_disabled? + end +end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 8b33c362a9c..4bd61aa8f86 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -1,15 +1,80 @@ module BoardsHelper - def board_data - board = @board || @boards.first + def board + @board ||= @board || @boards.first + end + def board_data { - endpoint: project_boards_path(@project), + boards_endpoint: @boards_endpoint, + lists_endpoint: board_lists_url(board), board_id: board.id, - disabled: "#{!can?(current_user, :admin_list, @project)}", - issue_link_base: project_issues_path(@project), + disabled: "#{!can?(current_user, :admin_list, current_board_parent)}", + issue_link_base: build_issue_link_base, root_path: root_path, - bulk_update_path: bulk_update_project_issues_path(@project), + bulk_update_path: @bulk_issues_path, default_avatar: image_path(default_avatar) } end + + def build_issue_link_base + project_issues_path(@project) + end + + def current_board_json + board = @board || @boards.first + + board.to_json( + only: [:id, :name, :milestone_id], + include: { + milestone: { only: [:title] } + } + ) + end + + def board_base_url + project_boards_path(@project) + end + + def multiple_boards_available? + current_board_parent.multiple_issue_boards_available?(current_user) + end + + def current_board_path(board) + @current_board_path ||= project_board_path(current_board_parent, board) + end + + def current_board_parent + @current_board_parent ||= @project + end + + def can_admin_issue? + can?(current_user, :admin_issue, current_board_parent) + end + + def board_list_data + { + toggle: "dropdown", + list_labels_path: labels_filter_path(true), + labels: labels_filter_path(true), + labels_endpoint: @labels_endpoint, + namespace_path: @namespace_path, + project_path: @project&.try(:path) + } + end + + def board_sidebar_user_data + dropdown_options = issue_assignees_dropdown_options + + { + toggle: 'dropdown', + field_name: 'issue[assignee_ids][]', + first_user: current_user&.username, + current_user: 'true', + project_id: @project&.try(:id), + null_user: 'true', + multi_select: 'true', + 'dropdown-header': dropdown_options[:data][:'dropdown-header'], + 'max-select': dropdown_options[:data][:'max-select'] + } + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 9651f9733f9..08fb9db6c0f 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -137,7 +137,7 @@ module CommitsHelper text = if options[:avatar] - %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>} + content_tag(:span, person_name, class: "commit-#{options[:source]}-name") else person_name end @@ -148,9 +148,9 @@ module CommitsHelper } if user.nil? - mail_to(source_email, text.html_safe, options) + mail_to(source_email, text, options) else - link_to(text.html_safe, user_path(user), options) + link_to(text, user_path(user), options) end end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index c53ea4519da..f7e17f5cc01 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -7,7 +7,8 @@ module GraphHelper refs << commit_refs.join(' ') # append note count - refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 + notes_count = @graph.notes[commit.id] + refs << "[#{notes_count} #{pluralize(notes_count, 'note')}]" if notes_count > 0 refs end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index eab1feb8a1f..36b79da1bde 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -3,6 +3,10 @@ module GroupsHelper can?(current_user, :change_visibility_level, group) end + def can_change_share_with_group_lock?(group) + can?(current_user, :change_share_with_group_lock, group) + end + def group_icon(group) if group.is_a?(String) group = Group.find_by_full_path(group) @@ -65,6 +69,20 @@ module GroupsHelper { group_name: group.name } end + def share_with_group_lock_help_text(group) + return default_help unless group.parent&.share_with_group_lock? + + if group.share_with_group_lock? + if can?(current_user, :change_share_with_group_lock, group.parent) + ancestor_locked_but_you_can_override(group) + else + ancestor_locked_so_ask_the_owner(group) + end + else + ancestor_locked_and_has_been_overridden(group) + end + end + private def group_title_link(group, hidable: false, show_avatar: false) @@ -80,4 +98,45 @@ module GroupsHelper output.html_safe end end + + def ancestor_group(group) + ancestor = oldest_consecutively_locked_ancestor(group) + if can?(current_user, :read_group, ancestor) + link_to ancestor.name, group_path(ancestor) + else + ancestor.name + end + end + + def remove_the_share_with_group_lock_from_ancestor(group) + ancestor = oldest_consecutively_locked_ancestor(group) + text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name } + if can?(current_user, :admin_group, ancestor) + link_to text, edit_group_path(ancestor) + else + text + end + end + + def oldest_consecutively_locked_ancestor(group) + group.ancestors.find do |group| + !group.has_parent? || !group.parent.share_with_group_lock? + end + end + + def default_help + s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.") + end + + def ancestor_locked_but_you_can_override(group) + s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) } + end + + def ancestor_locked_so_ask_the_owner(group) + s_("GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) } + end + + def ancestor_locked_and_has_been_overridden(group) + s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) } + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index ce2999e6696..66e1e607e01 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -347,6 +347,14 @@ module IssuablesHelper end end + def labels_path + if @project + project_labels_path(@project) + elsif @group + group_labels_path(@group) + end + end + def issuable_sidebar_options(issuable, can_edit_issuable) { endpoint: "#{issuable_json_path(issuable)}?basic=true", diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 3d0fdce6a43..212cdbb8157 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -56,7 +56,7 @@ module IssuesHelper end def project_options(issuable, current_user, ability: :read_project) - projects = current_user.authorized_projects + projects = current_user.authorized_projects.order_id_desc projects = projects.select do |project| current_user.can?(ability, project) end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e60513b35c7..e1ba7898ee6 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -121,13 +121,14 @@ module LabelsHelper end end - def labels_filter_path - return group_labels_path(@group, :json) if @group - + def labels_filter_path(only_group_labels = false) project = @target_project || @project if project project_labels_path(project, :json) + elsif @group + options = { only_group_labels: only_group_labels } if only_group_labels + group_labels_path(@group, :json, options) else dashboard_labels_path(:json) end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index d36bb4ab074..0d7347ed30d 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -40,6 +40,10 @@ module PreferencesHelper ] end + def user_application_theme + @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class + end + def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 86665ea2aec..c0114dd0256 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -15,9 +15,13 @@ module ProjectsHelper end def link_to_member_avatar(author, opts = {}) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } + default_opts = { size: 16 } opts = default_opts.merge(opts) - image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar] + + classes = %W[avatar avatar-inline s#{opts[:size]}] + classes << opts[:avatar_class] if opts[:avatar_class] + + image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: classes, alt: '') end def link_to_member(project, author, opts = {}, &block) @@ -29,7 +33,7 @@ module ProjectsHelper author_html = "" # Build avatar image tag - author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar] + author_html << link_to_member_avatar(author, opts) if opts[:avatar] # Build name span tag if opts[:by_username] @@ -541,6 +545,43 @@ module ProjectsHelper current_application_settings.restricted_visibility_levels || [] end + def project_permissions_settings(project) + feature = project.project_feature + { + visibilityLevel: project.visibility_level, + requestAccessEnabled: !!project.request_access_enabled, + issuesAccessLevel: feature.issues_access_level, + repositoryAccessLevel: feature.repository_access_level, + mergeRequestsAccessLevel: feature.merge_requests_access_level, + buildsAccessLevel: feature.builds_access_level, + wikiAccessLevel: feature.wiki_access_level, + snippetsAccessLevel: feature.snippets_access_level, + containerRegistryEnabled: !!project.container_registry_enabled, + lfsEnabled: !!project.lfs_enabled + } + end + + def project_permissions_panel_data(project) + data = { + currentSettings: project_permissions_settings(project), + canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), + allowedVisibilityOptions: project_allowed_visibility_levels(project), + visibilityHelpPath: help_page_path('public_access/public_access'), + registryAvailable: Gitlab.config.registry.enabled, + registryHelpPath: help_page_path('user/project/container_registry'), + lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?, + lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + } + + data.to_json.html_safe + end + + def project_allowed_visibility_levels(project) + Gitlab::VisibilityLevel.values.select do |level| + project.visibility_level_allowed?(level) && !restricted_levels.include?(level) + end + end + def find_file_path return unless @project && !@project.empty_repo? diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 98e824a8c65..cf28a917fd1 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -92,7 +92,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - current_user.authorized_groups.search(term).limit(limit).map do |group| + current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group| { category: "Groups", id: group.id, @@ -104,7 +104,7 @@ module SearchHelper # Autocomplete results for the current user's projects def projects_autocomplete(term, limit = 5) - current_user.authorized_projects.search_by_title(term) + current_user.authorized_projects.order_id_desc.search_by_title(term) .sorted_by_stars.non_archived.limit(limit).map do |p| { category: "Projects", @@ -134,19 +134,21 @@ module SearchHelper end def search_filter_input_options(type) - opts = { - id: "filtered-search-#{type}", - placeholder: 'Search or filter results...', - data: { - 'username-params' => @users.to_json(only: [:id, :username]) + opts = + { + id: "filtered-search-#{type}", + placeholder: 'Search or filter results...', + data: { + 'username-params' => @users.to_json(only: [:id, :username]) + } } - } if @project.present? opts[:data]['project-id'] = @project.id opts[:data]['base-endpoint'] = project_path(@project) else # Group context + opts[:data]['group-id'] = @group.id opts[:data]['base-endpoint'] = group_canonical_path(@group) end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 95dbdc8ec46..c4ea0f5ac53 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -104,7 +104,7 @@ module TreeHelper subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) if subtree.count == 1 && subtree.first.dir? - return tree_join(tree.name, flatten_tree(subtree.first)) + return tree_join(tree.name, flatten_tree(root_path, subtree.first)) else return tree.name end diff --git a/app/models/board.rb b/app/models/board.rb index 97d0f550925..5bb7d3d3722 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -3,7 +3,19 @@ class Board < ActiveRecord::Base has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - validates :project, presence: true + validates :project, presence: true, if: :project_needed? + + def project_needed? + true + end + + def parent + project + end + + def group_board? + false + end def backlog_list lists.merge(List.backlog).take diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index fdc5a2adea0..0b561203914 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -33,7 +33,7 @@ class BroadcastMessage < ActiveRecord::Base end def self.current_and_future_messages - where('ends_at > :now', now: Time.zone.now).reorder(id: :asc) + where('ends_at > :now', now: Time.zone.now).order_id_asc end def active? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 64c93966dff..5ebe6f180e6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -216,6 +216,7 @@ module Ci variables += runner.predefined_variables if runner variables += project.container_registry_variables variables += project.deployment_variables if has_environment? + variables += project.auto_devops_variables variables += yaml_variables variables += user_variables variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 46e5c344fdc..871c76fbad3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -38,6 +38,7 @@ module Ci validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? + after_initialize :set_config_source, if: :new_record? after_create :keep_around_commits, unless: :importing? enum source: { @@ -50,6 +51,12 @@ module Ci external: 6 } + enum config_source: { + unknown_source: nil, + repository_source: 1, + auto_devops_source: 2 + } + state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -316,6 +323,14 @@ module Ci builds.latest.failed_but_allowed.any? end + def set_config_source + if ci_yaml_from_repo + self.config_source = :repository_source + elsif implied_ci_yaml_file + self.config_source = :auto_devops_source + end + end + def config_processor return unless ci_yaml_file return @config_processor if defined?(@config_processor) @@ -342,11 +357,17 @@ module Ci def ci_yaml_file return @ci_yaml_file if defined?(@ci_yaml_file) - @ci_yaml_file = begin - project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) - rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal - self.yaml_errors = - "Failed to load CI/CD config file at #{ci_yaml_file_path}" + @ci_yaml_file = + if auto_devops_source? + implied_ci_yaml_file + else + ci_yaml_from_repo + end + + if @ci_yaml_file + @ci_yaml_file + else + self.yaml_errors = "Failed to load CI/CD config file for #{sha}" nil end end @@ -434,6 +455,23 @@ module Ci private + def ci_yaml_from_repo + return unless project + return unless sha + + project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) + rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal + nil + end + + def implied_ci_yaml_file + return unless project + + if project.auto_devops_enabled? + Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content + end + end + def pipeline_data Gitlab::DataBuilder::Pipeline.build(self) end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 7cb9a28a284..e961c97e337 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -10,8 +10,12 @@ module RelativePositioning after_save :save_positionable_neighbours end + def project_ids + [project.id] + end + def max_relative_position - self.class.in_projects(project.id).maximum(:relative_position) + self.class.in_projects(project_ids).maximum(:relative_position) end def prev_relative_position @@ -19,7 +23,7 @@ module RelativePositioning if self.relative_position prev_pos = self.class - .in_projects(project.id) + .in_projects(project_ids) .where('relative_position < ?', self.relative_position) .maximum(:relative_position) end @@ -32,7 +36,7 @@ module RelativePositioning if self.relative_position next_pos = self.class - .in_projects(project.id) + .in_projects(project_ids) .where('relative_position > ?', self.relative_position) .minimum(:relative_position) end @@ -59,7 +63,7 @@ module RelativePositioning pos_after = before.next_relative_position if before.shift_after? - issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after) + issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after) issue_to_move.move_after @positionable_neighbours = [issue_to_move] @@ -74,7 +78,7 @@ module RelativePositioning pos_before = after.prev_relative_position if after.shift_before? - issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before) + issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before) issue_to_move.move_before @positionable_neighbours = [issue_to_move] diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index a155a064032..db3cd257584 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -6,10 +6,6 @@ module Sortable extend ActiveSupport::Concern included do - # By default all models should be ordered - # by created_at field starting from newest - default_scope { order_id_desc } - scope :order_id_desc, -> { reorder(id: :desc) } scope :order_id_asc, -> { reorder(id: :asc) } scope :order_created_desc, -> { reorder(created_at: :desc) } diff --git a/app/models/environment.rb b/app/models/environment.rb index 435eeaf0e2e..9b05f8b1cd5 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -82,12 +82,7 @@ class Environment < ActiveRecord::Base def set_environment_type names = name.split('/') - self.environment_type = - if names.many? - names.first - else - nil - end + self.environment_type = names.many? ? names.first : nil end def includes_commit?(commit) @@ -101,7 +96,7 @@ class Environment < ActiveRecord::Base end def update_merge_request_metrics? - (environment_type || name) == "production" + folder_name == "production" end def first_deployment_for(commit) @@ -223,6 +218,10 @@ class Environment < ActiveRecord::Base format: :json) end + def folder_name + self.environment_type || self.name + end + private # Slugifying a name may remove the uniqueness guarantee afforded by it being diff --git a/app/models/event.rb b/app/models/event.rb index 996768a267b..c313bbb66f8 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,5 +1,6 @@ class Event < ActiveRecord::Base include Sortable + include IgnorableColumn default_scope { reorder(nil).where.not(author_id: nil) } CREATED = 1 @@ -50,13 +51,9 @@ class Event < ActiveRecord::Base belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations has_one :push_event_payload, foreign_key: :event_id - # For Hash only - serialize :data # rubocop:disable Cop/ActiveRecordSerialize - # Callbacks after_create :reset_project_activity after_create :set_last_repository_updated_at, if: :push? - after_create :replicate_event_for_push_events_migration # Scopes scope :recent, -> { reorder(id: :desc) } @@ -82,6 +79,10 @@ class Event < ActiveRecord::Base self.inheritance_column = 'action' + # "data" will be removed in 10.0 but it may be possible that JOINs happen that + # include this column, hence we're ignoring it as well. + ignore_column :data + class << self def model_name ActiveModel::Name.new(self, nil, 'event') @@ -159,7 +160,7 @@ class Event < ActiveRecord::Base end def push? - action == PUSHED && valid_push? + false end def merged? @@ -272,87 +273,6 @@ class Event < ActiveRecord::Base end end - def valid_push? - data[:ref] && ref_name.present? - rescue - false - end - - def tag? - Gitlab::Git.tag_ref?(data[:ref]) - end - - def branch? - Gitlab::Git.branch_ref?(data[:ref]) - end - - def new_ref? - Gitlab::Git.blank_ref?(commit_from) - end - - def rm_ref? - Gitlab::Git.blank_ref?(commit_to) - end - - def md_ref? - !(rm_ref? || new_ref?) - end - - def commit_from - data[:before] - end - - def commit_to - data[:after] - end - - def ref_name - if tag? - tag_name - else - branch_name - end - end - - def branch_name - @branch_name ||= Gitlab::Git.ref_name(data[:ref]) - end - - def tag_name - @tag_name ||= Gitlab::Git.ref_name(data[:ref]) - end - - # Max 20 commits from push DESC - def commits - @commits ||= (data[:commits] || []).reverse - end - - def commit_title - commit = commits.last - - commit[:message] if commit - end - - def commit_id - commit_to || commit_from - end - - def commits_count - data[:total_commits_count] || commits.count || 0 - end - - def ref_type - tag? ? "tag" : "branch" - end - - def push_with_commits? - !commits.empty? && commit_from && commit_to - end - - def last_push_to_non_root? - branch? && project.default_branch != branch_name - end - def target_iid target.respond_to?(:iid) ? target.iid : target_id end @@ -432,16 +352,6 @@ class Event < ActiveRecord::Base user ? author_id == user.id : false end - # We're manually replicating data into the new table since database triggers - # are not dumped to db/schema.rb. This could mean that a new installation - # would not have the triggers in place, thus losing events data in GitLab - # 10.0. - def replicate_event_for_push_events_migration - new_attributes = attributes.with_indifferent_access.except(:title, :data) - - EventForMigration.create!(new_attributes) - end - def to_partial_path # We are intentionally using `Event` rather than `self.class` so that # subclasses also use the `Event` implementation. diff --git a/app/models/event_for_migration.rb b/app/models/event_for_migration.rb deleted file mode 100644 index a1672da5eec..00000000000 --- a/app/models/event_for_migration.rb +++ /dev/null @@ -1,5 +0,0 @@ -# This model is used to replicate events between the old "events" table and the -# new "events_for_migration" table that will replace "events" in GitLab 10.0. -class EventForMigration < ActiveRecord::Base - self.table_name = 'events_for_migration' -end diff --git a/app/models/label.rb b/app/models/label.rb index 674bb3f2720..958141a7358 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -34,7 +34,8 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } scope :with_title, ->(title) { where(title: title) } - scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) } + scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) } + scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } def self.prioritized(project) joins(:priorities) @@ -172,6 +173,7 @@ class Label < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| + json[:type] = self.try(:type) json[:priority] = priority(options[:project]) if options.key?(:project) end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e7cbc5170e8..4a9a23fea1f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } + before_create :sync_share_with_group_lock_with_parent + before_update :sync_share_with_group_lock_with_parent, if: :parent_changed? + after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? } + # Legacy Storage specific hooks after_update :move_dir, if: :path_changed? @@ -219,4 +223,14 @@ class Namespace < ActiveRecord::Base errors.add(:parent_id, "has too deep level of nesting") end end + + def sync_share_with_group_lock_with_parent + if parent&.share_with_group_lock? + self.share_with_group_lock = true + end + end + + def force_share_with_group_lock_on_descendants + descendants.update_all(share_with_group_lock: true) + end end diff --git a/app/models/project.rb b/app/models/project.rb index fdd516ec2ae..ff5638dd155 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -187,9 +187,12 @@ class Project < ActiveRecord::Base has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' + has_one :auto_devops, class_name: 'ProjectAutoDevops' + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature accepts_nested_attributes_for :import_data + accepts_nested_attributes_for :auto_devops delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -466,6 +469,18 @@ class Project < ActiveRecord::Base self[:lfs_enabled] && Gitlab.config.lfs.enabled end + def auto_devops_enabled? + if auto_devops&.enabled.nil? + current_application_settings.auto_devops_enabled? + else + auto_devops.enabled? + end + end + + def has_auto_devops_implicitly_disabled? + auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled? + end + def repository_storage_path Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') end @@ -1378,6 +1393,10 @@ class Project < ActiveRecord::Base Gitlab::Utils.slugify(full_path.to_s) end + def has_ci? + repository.gitlab_ci_yml || auto_devops_enabled? + end + def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, @@ -1423,6 +1442,12 @@ class Project < ActiveRecord::Base deployment_service.predefined_variables end + def auto_devops_variables + return [] unless auto_devops_enabled? + + auto_devops&.variables || [] + end + def append_or_update_attribute(name, value) old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend @@ -1486,6 +1511,14 @@ class Project < ActiveRecord::Base end end + def multiple_issue_boards_available?(user) + feature_available?(:multiple_issue_boards, user) + end + + def issue_board_milestone_available?(user = nil) + feature_available?(:issue_board_milestone, user) + end + def full_path_was File.join(namespace.full_path, previous_changes['path'].first) end diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb new file mode 100644 index 00000000000..53731579e87 --- /dev/null +++ b/app/models/project_auto_devops.rb @@ -0,0 +1,11 @@ +class ProjectAutoDevops < ActiveRecord::Base + belongs_to :project + + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } + + def variables + variables = [] + variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present? + variables + end +end diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 3f1ff979de6..23ffb0d4ea8 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -15,15 +15,21 @@ class PushEvent < Event # should ensure the ID points to a valid project. validates :project_id, presence: true - # The "data" field must not be set for push events since it's not used and a - # waste of space. - validates :data, absence: true - # These fields are also not used for push events, thus storing them would be a # waste. validates :target_id, absence: true validates :target_type, absence: true + delegate :branch?, to: :push_event_payload + delegate :tag?, to: :push_event_payload + delegate :commit_from, to: :push_event_payload + delegate :commit_to, to: :push_event_payload + delegate :ref_type, to: :push_event_payload + delegate :commit_title, to: :push_event_payload + + delegate :commit_count, to: :push_event_payload + alias_method :commits_count, :commit_count + def self.sti_name PUSHED end @@ -36,86 +42,35 @@ class PushEvent < Event !!(commit_from && commit_to) end - def tag? - return super unless push_event_payload - - push_event_payload.tag? - end - - def branch? - return super unless push_event_payload - - push_event_payload.branch? - end - def valid_push? - return super unless push_event_payload - push_event_payload.ref.present? end def new_ref? - return super unless push_event_payload - push_event_payload.created? end def rm_ref? - return super unless push_event_payload - push_event_payload.removed? end - def commit_from - return super unless push_event_payload - - push_event_payload.commit_from - end - - def commit_to - return super unless push_event_payload - - push_event_payload.commit_to + def md_ref? + !(rm_ref? || new_ref?) end def ref_name - return super unless push_event_payload - push_event_payload.ref end - def ref_type - return super unless push_event_payload - - push_event_payload.ref_type - end - - def branch_name - return super unless push_event_payload - - ref_name - end - - def tag_name - return super unless push_event_payload - - ref_name - end - - def commit_title - return super unless push_event_payload - - push_event_payload.commit_title - end + alias_method :branch_name, :ref_name + alias_method :tag_name, :ref_name def commit_id commit_to || commit_from end - def commits_count - return super unless push_event_payload - - push_event_payload.commit_count + def last_push_to_non_root? + branch? && project.default_branch != branch_name end def validate_push_action diff --git a/app/models/user.rb b/app/models/user.rb index 105eb62f1fa..d7549409b15 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,7 @@ class User < ActiveRecord::Base default_value_for :project_view, :files default_value_for :notified_of_own_activity, false default_value_for :preferred_language, I18n.default_locale + default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, @@ -72,7 +73,7 @@ class User < ActiveRecord::Base # # Namespace for personal projects - has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent + has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent # Profile has_many :keys, -> do @@ -259,11 +260,13 @@ class User < ActiveRecord::Base end def sort(method) - case method.to_s + order_method = method || 'id_desc' + + case order_method.to_s when 'recent_sign_in' then order_recent_sign_in when 'oldest_sign_in' then order_oldest_sign_in else - order_by(method) + order_by(order_method) end end @@ -371,7 +374,7 @@ class User < ActiveRecord::Base # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) - find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) + Key.find_by(id: key_id)&.user end def find_by_full_path(path, follow_redirects: false) diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 8ada661e571..420991ff6d6 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -15,6 +15,11 @@ class GroupPolicy < BasePolicy condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? } + condition(:has_parent, scope: :subject) { @subject.has_parent? } + condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? } + condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? } + condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) } + condition(:has_projects) do GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? end @@ -44,7 +49,7 @@ class GroupPolicy < BasePolicy enable :change_visibility_level end - rule { owner & can_create_group & nested_groups_supported }.enable :create_subgroup + rule { owner & nested_groups_supported }.enable :create_subgroup rule { public_group | logged_in_viewable }.enable :view_globally @@ -54,6 +59,8 @@ class GroupPolicy < BasePolicy rule { ~can?(:view_globally) }.prevent :request_access rule { has_access }.prevent :request_access + rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock + def access_level return GroupMember::NO_ACCESS if @user.nil? diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index dcaccc3007d..ba0ae6ba8a0 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -26,5 +26,9 @@ class EnvironmentEntity < Grape::Entity terminal_project_environment_path(environment.project, environment) end + expose :folder_path do |environment| + folder_project_environments_path(environment.project, environment.folder_name) + end + expose :created_at, :updated_at end diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index d0a60f134da..88842a9aa75 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -36,9 +36,9 @@ class EnvironmentSerializer < BaseSerializer private def itemize(resource) - items = resource.order('folder_name ASC') + items = resource.order('folder ASC') .group('COALESCE(environment_type, name)') - .select('COALESCE(environment_type, name) AS folder_name', + .select('COALESCE(environment_type, name) AS folder', 'COUNT(*) AS size', 'MAX(id) AS last_id') # It makes a difference when you call `paginate` method, because @@ -49,7 +49,7 @@ class EnvironmentSerializer < BaseSerializer environments = resource.where(id: items.map(&:last_id)).index_by(&:id) items.map do |item| - Item.new(item.folder_name, item.size, environments[item.last_id]) + Item.new(item.folder, item.size, environments[item.last_id]) end end end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index c4f000b0ca3..357fc71f877 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity expose :flags do expose :latest?, as: :latest expose :stuck?, as: :stuck + expose :auto_devops_source?, as: :auto_devops expose :has_yaml_errors?, as: :yaml_errors expose :can_retry?, as: :retryable expose :can_cancel?, as: :cancelable diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb new file mode 100644 index 00000000000..72822ffffa1 --- /dev/null +++ b/app/services/boards/base_service.rb @@ -0,0 +1,10 @@ +module Boards + class BaseService < ::BaseService + # Parent can either a group or a project + attr_accessor :parent, :current_user, :params + + def initialize(parent, user, params = {}) + @parent, @current_user, @params = parent, user, params.dup + end + end +end diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 9eedb9e65a2..bd0bb387662 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -1,5 +1,5 @@ module Boards - class CreateService < BaseService + class CreateService < Boards::BaseService def execute create_board! if can_create_board? end @@ -7,11 +7,11 @@ module Boards private def can_create_board? - project.boards.size == 0 + parent.boards.size == 0 end def create_board! - board = project.boards.create(params) + board = parent.boards.create(params) if board.persisted? board.lists.create(list_type: :backlog) diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb index c0d7ff5b585..7c4a79f555e 100644 --- a/app/services/boards/issues/create_service.rb +++ b/app/services/boards/issues/create_service.rb @@ -1,6 +1,14 @@ module Boards module Issues - class CreateService < BaseService + class CreateService < Boards::BaseService + attr_accessor :project + + def initialize(parent, project, user, params = {}) + @project = project + + super(parent, user, params) + end + def execute create_issue(params.merge(label_ids: [list.label_id])) end @@ -8,7 +16,7 @@ module Boards private def board - @board ||= project.boards.find(params.delete(:board_id)) + @board ||= parent.boards.find(params.delete(:board_id)) end def list diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index eb345fead2d..d85d93e251b 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -1,6 +1,6 @@ module Boards module Issues - class ListService < BaseService + class ListService < Boards::BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless movable_list? || closed_list? @@ -11,7 +11,7 @@ module Boards private def board - @board ||= project.boards.find(params[:board_id]) + @board ||= parent.boards.find(params[:board_id]) end def list @@ -33,14 +33,14 @@ module Boards end def filter_params - set_project + set_parent set_state params end - def set_project - params[:project_id] = project.id + def set_parent + params[:project_id] = parent.id end def set_state diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index ecabb2a48e4..797d6df7c1a 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -1,17 +1,17 @@ module Boards module Issues - class MoveService < BaseService + class MoveService < Boards::BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) return false if issue_params.empty? - update_service.execute(issue) + update(issue) end private def board - @board ||= project.boards.find(params[:board_id]) + @board ||= parent.boards.find(params[:board_id]) end def move_between_lists? @@ -27,8 +27,8 @@ module Boards @moving_to_list ||= board.lists.find_by(id: params[:to_list_id]) end - def update_service - ::Issues::UpdateService.new(project, current_user, issue_params) + def update(issue) + ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) end def issue_params @@ -42,7 +42,7 @@ module Boards ) end - attrs[:move_between_iids] = move_between_iids if move_between_iids + attrs[:move_between_ids] = move_between_ids if move_between_ids attrs end @@ -61,16 +61,16 @@ module Boards if moving_to_list.movable? moving_from_list.label_id else - Label.on_project_boards(project.id).pluck(:label_id) + Label.on_project_boards(parent.id).pluck(:label_id) end Array(label_ids).compact end - def move_between_iids - return unless params[:move_after_iid] || params[:move_before_iid] + def move_between_ids + return unless params[:move_after_id] || params[:move_before_id] - [params[:move_after_iid], params[:move_before_iid]] + [params[:move_after_id], params[:move_before_id]] end end end diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb index 84f1fc3a4e2..6d0dd0a9f99 100644 --- a/app/services/boards/list_service.rb +++ b/app/services/boards/list_service.rb @@ -1,14 +1,14 @@ module Boards - class ListService < BaseService + class ListService < Boards::BaseService def execute - create_board! if project.boards.empty? - project.boards + create_board! if parent.boards.empty? + parent.boards end private def create_board! - Boards::CreateService.new(project, current_user).execute + Boards::CreateService.new(parent, current_user).execute end end end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index fe0d762ccd2..183556a1d6b 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -1,19 +1,18 @@ module Boards module Lists - class CreateService < BaseService + class CreateService < Boards::BaseService def execute(board) List.transaction do - label = available_labels.find(params[:label_id]) + label = available_labels_for(board).find(params[:label_id]) position = next_position(board) - create_list(board, label, position) end end private - def available_labels - LabelsFinder.new(current_user, project_id: project.id).execute + def available_labels_for(board) + LabelsFinder.new(current_user, project_id: parent.id).execute end def next_position(board) diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index f986e05944c..d75c5fd3dc6 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -1,6 +1,6 @@ module Boards module Lists - class DestroyService < BaseService + class DestroyService < Boards::BaseService def execute(list) return false unless list.destroyable? diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb index 939f9bfd068..05d4ab5dbcc 100644 --- a/app/services/boards/lists/generate_service.rb +++ b/app/services/boards/lists/generate_service.rb @@ -1,6 +1,6 @@ module Boards module Lists - class GenerateService < BaseService + class GenerateService < Boards::BaseService def execute(board) return false unless board.lists.movable.empty? @@ -15,11 +15,11 @@ module Boards def create_list(board, params) label = find_or_create_label(params) - Lists::CreateService.new(project, current_user, label_id: label.id).execute(board) + Lists::CreateService.new(parent, current_user, label_id: label.id).execute(board) end def find_or_create_label(params) - ::Labels::FindOrCreateService.new(current_user, project, params).execute + ::Labels::FindOrCreateService.new(current_user, parent, params).execute end def label_params diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb index df2a01a69e5..e57c95294af 100644 --- a/app/services/boards/lists/list_service.rb +++ b/app/services/boards/lists/list_service.rb @@ -1,6 +1,6 @@ module Boards module Lists - class ListService < BaseService + class ListService < Boards::BaseService def execute(board) board.lists.create(list_type: :backlog) unless board.lists.backlog.exists? diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb index f2a68865f7b..7d0730e8332 100644 --- a/app/services/boards/lists/move_service.rb +++ b/app/services/boards/lists/move_service.rb @@ -1,6 +1,6 @@ module Boards module Lists - class MoveService < BaseService + class MoveService < Boards::BaseService def execute(list) @board = list.board @old_position = list.position diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 414c01b2546..d20de9b16a4 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -16,9 +16,9 @@ module Ci protected: project.protected_for?(ref) ) - result = validate(current_user, - ignore_skip_ci: ignore_skip_ci, - save_on_errors: save_on_errors) + result = validate_project_and_git_items || + validate_pipeline(ignore_skip_ci: ignore_skip_ci, + save_on_errors: save_on_errors) return result if result @@ -47,13 +47,13 @@ module Ci private - def validate(triggering_user, ignore_skip_ci:, save_on_errors:) + def validate_project_and_git_items unless project.builds_enabled? return error('Pipeline is disabled') end - unless allowed_to_trigger_pipeline?(triggering_user) - if can?(triggering_user, :create_pipeline, project) + unless allowed_to_trigger_pipeline? + if can?(current_user, :create_pipeline, project) return error("Insufficient permissions for protected ref '#{ref}'") else return error('Insufficient permissions to create a new pipeline') @@ -67,7 +67,9 @@ module Ci unless commit return error('Commit not found') end + end + def validate_pipeline(ignore_skip_ci:, save_on_errors:) unless pipeline.config_processor unless pipeline.ci_yaml_file return error("Missing #{pipeline.ci_yaml_file_path} file") @@ -85,25 +87,25 @@ module Ci end end - def allowed_to_trigger_pipeline?(triggering_user) - if triggering_user - allowed_to_create?(triggering_user) + def allowed_to_trigger_pipeline? + if current_user + allowed_to_create? else # legacy triggers don't have a corresponding user !project.protected_for?(ref) end end - def allowed_to_create?(triggering_user) - access = Gitlab::UserAccess.new(triggering_user, project: project) + def allowed_to_create? + return unless can?(current_user, :create_pipeline, project) - can?(triggering_user, :create_pipeline, project) && - if branch? - access.can_update_branch?(ref) - elsif tag? - access.can_create_tag?(ref) - else - true # Allow it for now and we'll reject when we check ref existence - end + access = Gitlab::UserAccess.new(current_user, project: project) + if branch? + access.can_update_branch?(ref) + elsif tag? + access.can_create_tag?(ref) + else + true # Allow it for now and we'll reject when we check ref existence + end end def update_merge_requests_head_pipeline diff --git a/app/services/concerns/update_visibility_level.rb b/app/services/concerns/update_visibility_level.rb new file mode 100644 index 00000000000..536fcc6acce --- /dev/null +++ b/app/services/concerns/update_visibility_level.rb @@ -0,0 +1,15 @@ +module UpdateVisibilityLevel + def valid_visibility_level_change?(target, new_visibility) + # check that user is allowed to set specified visibility_level + if new_visibility && new_visibility.to_i != target.visibility_level + unless can?(current_user, :change_visibility_level, target) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + + deny_visibility_level(target, new_visibility) + return false + end + end + + true + end +end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index c7c27621085..70e50aa0f12 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -8,15 +8,7 @@ module Groups def execute @group = Group.new(params) - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) - deny_visibility_level(@group) - return @group - end - - if @group.parent && !can?(current_user, :create_subgroup, @group.parent) - @group.parent = nil - @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.') - + unless can_use_visibility_level? && can_create_group? return @group end @@ -39,5 +31,33 @@ module Groups def create_chat_team? Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil? end + + def can_create_group? + if @group.subgroup? + unless can?(current_user, :create_subgroup, @group.parent) + @group.parent = nil + @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.') + + return false + end + else + unless can?(current_user, :create_group) + @group.errors.add(:base, 'You don’t have permission to create groups.') + + return false + end + end + + true + end + + def can_use_visibility_level? + unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) + deny_visibility_level(@group) + return false + end + + true + end end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 1d65c76d282..08e3efb96e3 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -1,18 +1,13 @@ module Groups class UpdateService < Groups::BaseService + include UpdateVisibilityLevel + def execute reject_parent_id! - # check that user is allowed to set specified visibility_level - new_visibility = params[:visibility_level] - if new_visibility && new_visibility.to_i != group.visibility_level - unless can?(current_user, :change_visibility_level, group) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + return false unless valid_visibility_level_change?(group, params[:visibility_level]) - deny_visibility_level(group, new_visibility) - return group - end - end + return false unless valid_share_with_group_lock_change? group.assign_attributes(params) @@ -30,5 +25,19 @@ module Groups def reject_parent_id! params.except!(:parent_id) end + + def valid_share_with_group_lock_change? + return true unless changing_share_with_group_lock? + return true if can?(current_user, :change_share_with_group_lock, group) + + group.errors.add(:share_with_group_lock, s_('GroupSettings|cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group')) + false + end + + def changing_share_with_group_lock? + return false if params[:share_with_group_lock].nil? + + params[:share_with_group_lock] != group.share_with_group_lock + end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index deb4990eb4f..b4ca3966505 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -3,7 +3,7 @@ module Issues include SpamCheckService def execute(issue) - handle_move_between_iids(issue) + handle_move_between_ids(issue) filter_spam_check_params change_issue_duplicate(issue) move_issue_to_new_project(issue) || update(issue) @@ -54,13 +54,13 @@ module Issues end end - def handle_move_between_iids(issue) - return unless params[:move_between_iids] + def handle_move_between_ids(issue) + return unless params[:move_between_ids] - after_iid, before_iid = params.delete(:move_between_iids) + after_id, before_id = params.delete(:move_between_ids) - issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid - issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid + issue_before = get_issue_if_allowed(issue.project, before_id) if before_id + issue_after = get_issue_if_allowed(issue.project, after_id) if after_id issue.move_between(issue_before, issue_after) end @@ -87,8 +87,8 @@ module Issues private - def get_issue_if_allowed(project, iid) - issue = project.issues.find_by(iid: iid) + def get_issue_if_allowed(project, id) + issue = project.issues.find(id) issue if can?(current_user, :update_issue, issue) end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index cf69007bc3b..cb4ffcab778 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -1,7 +1,9 @@ module Projects class UpdateService < BaseService + include UpdateVisibilityLevel + def execute - unless visibility_level_allowed? + unless valid_visibility_level_change?(project, params[:visibility_level]) return error('New visibility level not allowed!') end @@ -28,22 +30,6 @@ module Projects private - def visibility_level_allowed? - # check that user is allowed to set specified visibility_level - new_visibility = params[:visibility_level] - - if new_visibility && new_visibility.to_i != project.visibility_level - unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) - - deny_visibility_level(project, new_visibility) - return false - end - end - - true - end - def renaming_project_with_container_registry_tags? new_path = params[:path] diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 9cdb9935bea..a077b3584b0 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -115,7 +115,7 @@ module QuickActions if issuable.allows_multiple_assignees? issuable.assignees.pluck(:id) + users.map(&:id) else - [users.last.id] + [users.first.id] end end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 4abd2c44b2f..20d90504bd2 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -9,18 +9,17 @@ module TestHooks end def execute + trigger_key = hook.class::TRIGGERS.key(trigger.to_sym) trigger_data_method = "#{trigger}_data" - if !self.respond_to?(trigger_data_method, true) || - !hook.class::TRIGGERS.value?(trigger.to_sym) - + if trigger_key.nil? || !self.respond_to?(trigger_data_method, true) return error('Testing not available for this hook') end error_message = catch(:validation_error) do sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend - return hook.execute(sample_data, trigger) + return hook.execute(sample_data, trigger_key) end error(error_message) diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 2825478926a..cd99e0b90f9 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -19,7 +19,7 @@ class WebHookService def initialize(hook, data, hook_name) @hook = hook @data = data - @hook_name = hook_name + @hook_name = hook_name.to_s end def execute diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a010b4691bf..dbaed1d09fb 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -226,7 +226,17 @@ .help-block 0 for unlimited %fieldset - %legend Continuous Integration + %legend Continuous Integration and Deployment + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :auto_devops_enabled do + = f.check_box :auto_devops_enabled + Enabled Auto DevOps (Beta) for projects by default + .help-block + It will automatically build, test, and deploy applications based on a predefined CI/CD configuration + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') + .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 11bf3f5d323..7981daa0705 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,7 +1,3 @@ -- if current_user.can_create_group? - - content_for :breadcrumbs_extra do - = link_to "New group", new_group_path, class: "btn btn-new" - .top-area %ul.nav-links = nav_link(page: dashboard_groups_path) do @@ -10,8 +6,8 @@ = nav_link(page: explore_groups_path) do = link_to explore_groups_path, title: 'Explore public groups' do Explore public groups - .nav-controls.nav-controls-new-nav + .nav-controls = render 'shared/groups/search_form' = render 'shared/groups/dropdown' - if current_user.can_create_group? - = link_to "New group", new_group_path, class: "btn btn-new visible-xs" + = link_to "New group", new_group_path, class: "btn btn-new" diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index e2a1914ada2..fd2ba9ac1ca 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,10 +1,6 @@ = content_for :flash_message do = render 'shared/project_limit' -- if current_user.can_create_project? - - content_for :breadcrumbs_extra do - = link_to "New project", new_project_path, class: 'btn btn-new' - .top-area.scrolling-tabs-container.inner-page-scroll-tabs .fade-left= icon('angle-left') .fade-right= icon('angle-right') @@ -19,8 +15,8 @@ = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do Explore projects - .nav-controls.nav-controls-new-nav + .nav-controls = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if current_user.can_create_project? - = link_to "New project", new_project_path, class: "btn btn-new visible-xs" + = link_to "New project", new_project_path, class: "btn btn-new" diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index 14c18678ab1..7330f4cb523 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,7 +1,3 @@ -- if current_user - - content_for :breadcrumbs_extra do - = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" - .top-area %ul.nav-links = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do @@ -10,3 +6,7 @@ = nav_link(page: explore_snippets_path) do = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do Explore Snippets + + - if current_user + .nav-controls.hidden-xs + = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index ad0e205a79f..42941acc508 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,14 +4,9 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") -- content_for :breadcrumbs_extra do - = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do - = icon('rss') - = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues - .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls.visible-xs + .nav-controls = link_to params.merge(rss_url_options), class: 'btn has-tooltip', 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/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index ccc74f7cf3d..53cd1130299 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -2,12 +2,9 @@ - page_title "Merge Requests" - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) -- content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests - .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls.visible-xs + .nav-controls = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 9fffdded1a0..f66e2b40d76 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -2,13 +2,10 @@ - page_title 'Milestones' - header_title 'Milestones', dashboard_milestones_path -- content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones - .top-area = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls.visible-xs + .nav-controls = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones .milestones diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 839f23e69fd..0d3308833b7 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -28,17 +28,20 @@ .col-sm-offset-2.col-sm-10 = render 'shared/allow_request_access', form: f - = render 'group_admin_settings', f: f - .form-group - %hr - = f.label :share_with_group_lock, class: 'control-label' do - Share with group lock + %label.control-label + = s_("GroupSettings|Share with group lock") .col-sm-10 .checkbox - = f.check_box :share_with_group_lock - %span.descr Prevent sharing a project with another group within this group + = f.label :share_with_group_lock do + = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group) + %strong + - group_link = link_to @group.name, group_path(@group) + = s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link } + %br + %span.descr= share_with_group_lock_help_text(@group) + = render 'group_admin_settings', f: f .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 13a4b4c90c9..7f411927429 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -9,17 +9,9 @@ = webpack_bundle_tag 'filtered_search' - if group_issues_exists - - content_for :breadcrumbs_extra do - = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues - -- if group_issues_exists .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls.visible-xs + .nav-controls = link_to params.merge(rss_url_options), class: 'btn' do = icon('rss') %span.icon-label diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 9e59a09d459..89165096fe2 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,7 +1,4 @@ - page_title 'Labels' -- if can?(current_user, :admin_label, @group) - - content_for :breadcrumbs_extra do - = link_to "New label", new_group_label_path(@group), class: "btn btn-new" = render "groups/head_issues" @@ -10,7 +7,7 @@ .nav-text Labels can be applied to issues and merge requests. Group labels are available for any project within the group. - .nav-controls.visible-xs + .nav-controls - if can?(current_user, :admin_label, @group) = link_to "New label", new_group_label_path(@group), class: "btn btn-new" diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 0344770e0dd..e56dc1fb9c2 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -4,17 +4,13 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if current_user - - content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests - - if @group_merge_requests.empty? = render 'shared/empty_states/merge_requests', project_select_button: true - else .top-area = render 'shared/issuable/nav', type: :merge_requests - if current_user - .nav-controls.visible-xs + .nav-controls = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 6e7a1af243d..ed582e521c4 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,14 +1,11 @@ - page_title "Milestones" -- if can?(current_user, :admin_milestones, @group) - - content_for :breadcrumbs_extra do - = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" = render "groups/head_issues" .top-area = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls.visible-xs + .nav-controls - if can?(current_user, :admin_milestones, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 65ac8aaa59b..0ca34b276a7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } + %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' = render "layouts/header/default" diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index feffd7707dc..7bd3f5306a2 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -2,7 +2,7 @@ - hide_top_links = @hide_top_links || false %nav.breadcrumbs{ role: "navigation", class: [container, @content_class] } - .breadcrumbs-container{ class: [container, @content_class] } + .breadcrumbs-container - if defined?(@left_sidebar) = button_tag class: 'toggle-mobile-nav', type: 'button' do %span.sr-only Open sidebar @@ -17,6 +17,4 @@ = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after %li %h2.breadcrumbs-sub-title= @breadcrumb_title - - if content_for?(:breadcrumbs_extra) - .breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra = yield :header_content diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 8a39c4d775f..c254ee02dd8 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,5 +1,5 @@ %ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do %a{ href: "#", data: { toggle: "dropdown" } } Projects = custom_icon('caret_down') @@ -22,7 +22,7 @@ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do Snippets - %li.dropdown.hidden-lg + %li.header-more.dropdown.hidden-lg %a{ href: "#", data: { toggle: "dropdown" } } More = custom_icon('caret_down') diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 3b53117deb6..8ab2b686f86 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -14,6 +14,11 @@ Overview %ul.sidebar-sub-level-items + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: { class: "fly-out-top-item" } ) do + = link_to admin_root_path do + %strong.fly-out-top-item-name + #{ _('Overview') } + %li.divider.fly-out-top-item = nav_link(controller: :dashboard, html_options: {class: 'home'}) do = link_to admin_root_path, title: 'Overview' do %span @@ -55,6 +60,11 @@ Monitoring %ul.sidebar-sub-level-items + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do + = link_to admin_conversational_development_index_path do + %strong.fly-out-top-item-name + #{ _('Monitoring') } + %li.divider.fly-out-top-item = nav_link(controller: :system_info) do = link_to admin_system_info_path, title: 'System Info' do %span @@ -82,6 +92,11 @@ = custom_icon('messages') %span.nav-item-name Messages + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_broadcast_messages_path do + %strong.fly-out-top-item-name + #{ _('Messages') } = nav_link(controller: [:hooks, :hook_logs]) do = sidebar_link admin_hooks_path, title: _('Hooks') do @@ -89,6 +104,11 @@ = custom_icon('system_hooks') %span.nav-item-name System Hooks + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" } ) do + = link_to admin_hooks_path do + %strong.fly-out-top-item-name + #{ _('System Hooks') } = nav_link(controller: :applications) do = sidebar_link admin_applications_path, title: _('Applications') do @@ -96,6 +116,11 @@ = custom_icon('applications') %span.nav-item-name Applications + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_applications_path do + %strong.fly-out-top-item-name + #{ _('Applications') } = nav_link(controller: :abuse_reports) do = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do @@ -104,6 +129,12 @@ %span.nav-item-name Abuse Reports %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_broadcast_messages_path do + %strong.fly-out-top-item-name + #{ _('Abuse Reports') } + %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all)) - if akismet_enabled? = nav_link(controller: :spam_logs) do @@ -112,6 +143,11 @@ = custom_icon('spam_logs') %span.nav-item-name Spam Logs + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_spam_logs_path do + %strong.fly-out-top-item-name + #{ _('Spam Logs') } = nav_link(controller: :deploy_keys) do = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do @@ -119,6 +155,11 @@ = custom_icon('key') %span.nav-item-name Deploy Keys + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_deploy_keys_path do + %strong.fly-out-top-item-name + #{ _('Deploy Keys') } = nav_link(controller: :services) do = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do @@ -126,6 +167,11 @@ = custom_icon('service_templates') %span.nav-item-name Service Templates + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :services, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_application_settings_services_path do + %strong.fly-out-top-item-name + #{ _('Service Templates') } = nav_link(controller: :labels) do = sidebar_link admin_labels_path, title: _('Labels') do @@ -133,6 +179,11 @@ = custom_icon('labels') %span.nav-item-name Labels + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_labels_path do + %strong.fly-out-top-item-name + #{ _('Labels') } = nav_link(controller: :appearances) do = sidebar_link admin_appearances_path, title: _('Appearances') do @@ -140,6 +191,11 @@ = custom_icon('appearance') %span.nav-item-name Appearance + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :appearances, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_appearances_path do + %strong.fly-out-top-item-name + #{ _('Appearance') } = nav_link(controller: :application_settings) do = sidebar_link admin_application_settings_path, title: _('Settings') do @@ -147,5 +203,10 @@ = custom_icon('settings') %span.nav-item-name Settings + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :application_settings, html_options: { class: "fly-out-top-item" } ) do + = link_to admin_application_settings_path do + %strong.fly-out-top-item-name + #{ _('Settings') } = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 5a1511b262f..e01dfa7c854 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,3 +1,6 @@ +- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute +- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute + .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } .nav-sidebar-inner-scroll .context-header @@ -15,6 +18,11 @@ Overview %ul.sidebar-sub-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do + = link_to group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Overview') } + %li.divider.fly-out-top-item = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Group details' do %span @@ -30,11 +38,16 @@ .nav-icon-container = custom_icon('issues') %span.nav-item-name - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute Issues %span.badge.count= number_with_delimiter(issues.count) %ul.sidebar-sub-level-items + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do + = link_to issues_group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Issues') } + %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues.count) + %li.divider.fly-out-top-item = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do = link_to issues_group_path(@group), title: 'List' do %span @@ -55,15 +68,25 @@ .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute Merge Requests %span.badge.count= number_with_delimiter(merge_requests.count) + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do + = link_to merge_requests_group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Merge Requests') } + %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count) = nav_link(path: 'group_members#index') do = sidebar_link group_group_members_path(@group), title: _('Members') do .nav-icon-container = custom_icon('members') %span.nav-item-name Members + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do + = link_to merge_requests_group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Members') } - if current_user && can?(current_user, :admin_group, @group) = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do = sidebar_link edit_group_path(@group), title: _('Settings') do @@ -72,6 +95,11 @@ %span.nav-item-name Settings %ul.sidebar-sub-level-items + = nav_link(path: %w[groups#projects groups#edit ci_cd#show], html_options: { class: "fly-out-top-item" } ) do + = link_to edit_group_path(@group) do + %strong.fly-out-top-item-name + #{ _('Settings') } + %li.divider.fly-out-top-item = nav_link(path: 'groups#edit') do = link_to edit_group_path(@group), title: 'General' do %span diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index ccb6d1492f1..4c26d107ea7 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -12,12 +12,22 @@ = custom_icon('profile') %span.nav-item-name Profile + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" } ) do + = link_to profile_path do + %strong.fly-out-top-item-name + #{ _('Profile') } = nav_link(controller: [:accounts, :two_factor_auths]) do = sidebar_link profile_account_path, title: _('Account') do .nav-icon-container = custom_icon('account') %span.nav-item-name Account + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" } ) do + = link_to profile_account_path do + %strong.fly-out-top-item-name + #{ _('Account') } - if current_application_settings.user_oauth_applications? = nav_link(controller: 'oauth/applications') do = sidebar_link applications_profile_path, title: _('Applications') do @@ -25,24 +35,44 @@ = custom_icon('applications') %span.nav-item-name Applications + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do + = link_to applications_profile_path do + %strong.fly-out-top-item-name + #{ _('Applications') } = nav_link(controller: :chat_names) do = sidebar_link profile_chat_names_path, title: _('Chat') do .nav-icon-container = custom_icon('chat') %span.nav-item-name Chat + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_chat_names_path do + %strong.fly-out-top-item-name + #{ _('Chat') } = nav_link(controller: :personal_access_tokens) do = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do .nav-icon-container = custom_icon('access_tokens') %span.nav-item-name Access Tokens + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_personal_access_tokens_path do + %strong.fly-out-top-item-name + #{ _('Access Tokens') } = nav_link(controller: :emails) do = sidebar_link profile_emails_path, title: _('Emails') do .nav-icon-container = custom_icon('emails') %span.nav-item-name Emails + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_emails_path do + %strong.fly-out-top-item-name + #{ _('Emails') } - unless current_user.ldap_user? = nav_link(controller: :passwords) do = sidebar_link edit_profile_password_path, title: _('Password') do @@ -50,36 +80,65 @@ = custom_icon('lock') %span.nav-item-name Password + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" } ) do + = link_to edit_profile_password_path do + %strong.fly-out-top-item-name + #{ _('Password') } = nav_link(controller: :notifications) do = sidebar_link profile_notifications_path, title: _('Notifications') do .nav-icon-container = custom_icon('notifications') %span.nav-item-name Notifications - + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_notifications_path do + %strong.fly-out-top-item-name + #{ _('Notifications') } = nav_link(controller: :keys) do = sidebar_link profile_keys_path, title: _('SSH Keys') do .nav-icon-container = custom_icon('key') %span.nav-item-name SSH Keys + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_keys_path do + %strong.fly-out-top-item-name + #{ _('SSH Keys') } = nav_link(controller: :gpg_keys) do = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do .nav-icon-container = custom_icon('key_2') %span.nav-item-name GPG Keys + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_gpg_keys_path do + %strong.fly-out-top-item-name + #{ _('GPG Keys') } = nav_link(controller: :preferences) do = sidebar_link profile_preferences_path, title: _('Preferences') do .nav-icon-container = custom_icon('preferences') %span.nav-item-name Preferences + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_preferences_path do + %strong.fly-out-top-item-name + #{ _('Preferences') } = nav_link(path: 'profiles#audit_log') do = sidebar_link audit_log_profile_path, title: _('Authentication log') do .nav-icon-container = custom_icon('authentication_log') %span.nav-item-name Authentication log + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" } ) do + = link_to audit_log_profile_path do + %strong.fly-out-top-item-name + #{ _('Authentication Log') } = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 760c4c97c33..9589e81c750 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -16,6 +16,11 @@ Overview %ul.sidebar-sub-level-items + = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do + = link_to project_path(@project) do + %strong.fly-out-top-item-name + #{ _('Overview') } + %li.divider.fly-out-top-item = nav_link(path: 'projects#show') do = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do %span= _('Details') @@ -38,6 +43,11 @@ Repository %ul.sidebar-sub-level-items + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do + = link_to project_tree_path(@project) do + %strong.fly-out-top-item-name + #{ _('Repository') } + %li.divider.fly-out-top-item = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_tree_path(@project) do #{ _('Files') } @@ -90,7 +100,15 @@ = number_with_delimiter(@project.open_issues_count) %ul.sidebar-sub-level-items - = nav_link(controller: :issues) do + = nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do + = link_to project_issues_path(@project) do + %strong.fly-out-top-item-name + #{ _('Issues') } + - if @project.issues_enabled? + %span.badge.count.issue_counter.fly-out-badge + = number_with_delimiter(@project.open_issues_count) + %li.divider.fly-out-top-item + = nav_link(controller: :issues, action: :index) do = link_to project_issues_path(@project), title: 'Issues' do %span List @@ -133,6 +151,13 @@ Merge Requests %span.badge.count.merge_counter.js-merge-counter = number_with_delimiter(@project.open_merge_requests_count) + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do + = link_to project_merge_requests_path(@project) do + %strong.fly-out-top-item-name + #{ _('Merge Requests') } + %span.badge.count.merge_counter.js-merge-counter.fly-out-badge + = number_with_delimiter(@project.open_merge_requests_count) - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do @@ -143,6 +168,11 @@ CI / CD %ul.sidebar-sub-level-items + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts], html_options: { class: "fly-out-top-item" } ) do + = link_to project_pipelines_path(@project) do + %strong.fly-out-top-item-name + #{ _('CI / CD') } + %li.divider.fly-out-top-item - if project_nav_tab? :pipelines = nav_link(path: ['pipelines#index', 'pipelines#show']) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do @@ -180,6 +210,11 @@ = custom_icon('wiki') %span.nav-item-name Wiki + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do + = link_to get_project_wiki_path(@project) do + %strong.fly-out-top-item-name + #{ _('Wiki') } - if project_nav_tab? :snippets = nav_link(controller: :snippets) do @@ -188,6 +223,11 @@ = custom_icon('snippets') %span.nav-item-name Snippets + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do + = link_to project_snippets_path(@project) do + %strong.fly-out-top-item-name + #{ _('Snippets') } - if project_nav_tab? :settings = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do @@ -200,6 +240,11 @@ %ul.sidebar-sub-level-items - can_edit = can?(current_user, :admin_project, @project) - if can_edit + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show], html_options: { class: "fly-out-top-item" } ) do + = link_to edit_project_path(@project) do + %strong.fly-out-top-item-name + #{ _('Settings') } + %li.divider.fly-out-top-item = nav_link(path: %w[projects#edit]) do = link_to edit_project_path(@project), title: 'General' do %span diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 9e7fe556d88..2b72eeab8d6 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -3,6 +3,26 @@ = render 'profiles/head' = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| + .col-lg-4.application-theme + %h4.prepend-top-0 + GitLab navigation theme + %p Customize the appearance of the application header and navigation sidebar. + .col-lg-8.application-theme + - Gitlab::Themes.each do |theme| + = label_tag do + .preview{ class: theme.name.downcase } + .preview-row + .quadrant.one + .quadrant.two + .preview-row + .quadrant.three + .quadrant.four + = f.radio_button :theme_id, theme.id + = theme.name + + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Syntax highlighting theme diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 431ab9d052b..8966dd3fd86 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -1,3 +1,7 @@ +// Remove body class for any previous theme, re-add current one +$('body').removeClass('<%= Gitlab::Themes.body_classes %>') +$('body').addClass('<%= user_application_theme %>') + // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { $('.content-wrapper .container-fluid').removeClass('container-limited') diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index e3effe45d6c..1dd8778f800 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -1,7 +1,7 @@ - form = local_assigns.fetch(:form) .form-group - .checkbox.builds-feature + .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } = form.label :only_allow_merge_if_pipeline_succeeds do = form.check_box :only_allow_merge_if_pipeline_succeeds %strong Only allow merge requests to be merged if the pipeline succeeds diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml index d0fcd55f6c1..6d6bd79bc3c 100644 --- a/app/views/projects/blob/viewers/_route_map.html.haml +++ b/app/views/projects/blob/viewers/_route_map.html.haml @@ -6,4 +6,4 @@ This Route Map is invalid: = viewer.validation_message -= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map') += link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml index 2318cf82f58..a5f73fb0197 100644 --- a/app/views/projects/blob/viewers/_route_map_loading.html.haml +++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml @@ -1,4 +1,4 @@ = icon('spinner spin fw') Validating Route Map… -= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map') += link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment') diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml index 2a5b8b1441e..bb56769bd3f 100644 --- a/app/views/projects/boards/index.html.haml +++ b/app/views/projects/boards/index.html.haml @@ -1 +1 @@ -= render "show" += render "shared/boards/show", board: @boards.first diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml index 2a5b8b1441e..e5b5f6404bb 100644 --- a/app/views/projects/boards/show.html.haml +++ b/app/views/projects/boards/show.html.haml @@ -1 +1 @@ -= render "show" += render "shared/boards/show", board: @board diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 09e3a775d1c..bef96786b73 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -2,6 +2,7 @@ #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, "help-page-path" => help_page_path('ci/quick_start/README'), + "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), } } - content_for :page_specific_javascripts do diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 994119051d2..0a3045604f4 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -66,90 +66,18 @@ %section.settings.sharing-permissions .settings-header %h4 - Sharing and permissions + Permissions %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p Enable or disable certain project features and choose access levels. .settings-content.no-animate{ class: ('expanded' if expanded) } = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| - .form_group.sharing-and-permissions - .row.js-visibility-select - .col-md-8 - .label-light - = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - %span.help-block - .col-md-4.visibility-select-container - = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) - = f.fields_for :project_feature do |feature_fields| - %fieldset.features - .row - .col-md-8.project-feature - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block View and edit files in this project - .col-md-4.js-repo-access-level - = project_feature_access_select(:repository_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-4 - = project_feature_access_select(:merge_requests_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' - %span.help-block Build, test, and deploy your changes - .col-md-4 - = project_feature_access_select(:builds_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-4 - = project_feature_access_select(:snippets_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-4 - = project_feature_access_select(:issues_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-4 - = project_feature_access_select(:wiki_access_level) - .form-group - = render 'shared/allow_request_access', form: f - - if Gitlab.config.lfs.enabled && current_user.admin? - .row.js-lfs-enabled.form-group.sharing-and-permissions - .col-md-8 - = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light' - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - %span.help-block Manages large files such as audio, video and graphics files. - .col-md-4 - .select-wrapper - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } - = icon('chevron-down') - - if Gitlab.config.registry.enabled - .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) + .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" - - %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } + %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) } .settings-header %h4 Merge request settings diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 6fb5aa45166..6f7713124ac 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,7 +1,9 @@ +- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues') + %ul.content-list.issues-list.issuable-list = render partial: "projects/issues/issue", collection: @issues - if @issues.blank? - = render 'shared/empty_states/issues' + = render empty_state_path - if @issues.present? = paginate @issues, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 6fcb5975707..e72c94695bc 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -13,14 +13,11 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") -- content_for :breadcrumbs_extra do - = render "projects/issues/nav_btns" - - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls.visible-xs + .nav-controls = render "projects/issues/nav_btns" = render 'shared/issuable/search_bar', type: :issues diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index ec9e8444ac5..10d07ce8e45 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,10 +3,6 @@ - hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) -- if can?(current_user, :admin_label, @project) - - content_for :breadcrumbs_extra do - = link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" - = render "shared/mr_head" - if @labels.exists? || @prioritized_labels.exists? @@ -18,7 +14,7 @@ Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. - if can_admin_label - .nav-controls.visible-xs + .nav-controls = link_to new_project_label_path(@project), class: "btn btn-new" do New label diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 27c3002366b..2c53891a92d 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -12,16 +12,15 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- content_for :breadcrumbs_extra do - = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path - = render 'projects/last_push' - if @project.merge_requests.exists? %div{ class: container_class } + - if show_auto_devops_callout?(@project) + = render 'shared/auto_devops_callout' .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls.visible-xs + .nav-controls = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 71ec88ef1c1..f3abecdd302 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,20 +1,16 @@ - @no_container = true - page_title 'Milestones' -- if can?(current_user, :admin_milestone, @project) - - content_for :breadcrumbs_extra do - = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' - = render "shared/mr_head" %div{ class: container_class } .top-area = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) - .nav-controls.nav-controls-new-nav + .nav-controls = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: "btn btn-new visible-xs", title: 'New milestone' do + = link_to new_project_milestone_path(@project), class: "btn btn-new", title: 'New milestone' do New milestone .milestones diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index adffd67029a..819392b8f0c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -28,8 +28,8 @@ = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} %div = render 'project_templates', f: f - .second-column - - if import_sources_enabled? + - if import_sources_enabled? + .second-column .project-import .form-group.clearfix = f.label :visibility_level, class: 'label-light' do #the label here seems wrong diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index d9957b54a4d..2b081786b6a 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -7,10 +7,6 @@ - @no_container = true - page_title _("Pipeline Schedules") -- if can?(current_user, :create_pipeline_schedule, @project) - - content_for :breadcrumbs_extra do - = link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' - = render "projects/pipelines/head" %div{ class: container_class } @@ -20,7 +16,7 @@ = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope - if can?(current_user, :create_pipeline_schedule, @project) - .nav-controls.visible-xs + .nav-controls = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do %span= _('New schedule') diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index c1729850cf4..9fc15d20f34 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -2,19 +2,23 @@ - page_title "Pipelines" = render "projects/pipelines/head" -#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), - "css-class" => container_class, - "help-page-path" => help_page_path('ci/quick_start/README'), - "new-pipeline-path" => new_project_pipeline_path(@project), - "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, - "all-path" => project_pipelines_path(@project), - "pending-path" => project_pipelines_path(@project, scope: :pending), - "running-path" => project_pipelines_path(@project, scope: :running), - "finished-path" => project_pipelines_path(@project, scope: :finished), - "branches-path" => project_pipelines_path(@project, scope: :branches), - "tags-path" => project_pipelines_path(@project, scope: :tags), - "has-ci" => @repository.gitlab_ci_yml, - "ci-lint-path" => ci_lint_path } } +%div{ 'class' => container_class } + - if show_auto_devops_callout?(@project) + = render 'shared/auto_devops_callout' -= page_specific_javascript_bundle_tag('common_vue') -= page_specific_javascript_bundle_tag('pipelines') + #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), + "help-page-path" => help_page_path('ci/quick_start/README'), + "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), + "new-pipeline-path" => new_project_pipeline_path(@project), + "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, + "all-path" => project_pipelines_path(@project), + "pending-path" => project_pipelines_path(@project, scope: :pending), + "running-path" => project_pipelines_path(@project, scope: :running), + "finished-path" => project_pipelines_path(@project, scope: :finished), + "branches-path" => project_pipelines_path(@project, scope: :branches), + "tags-path" => project_pipelines_path(@project, scope: :tags), + "has-ci" => @project.has_ci?, + "ci-lint-path" => ci_lint_path } } + + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('pipelines') diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 8bf76b646f7..324cd423ede 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -2,11 +2,42 @@ .col-lg-12 = form_for @project, url: project_pipelines_settings_path(@project) do |f| %fieldset.builds-feature - - unless @repository.gitlab_ci_yml - .form-group - %p Pipelines need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' - %hr + .form-group + %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery. + %h5 Auto DevOps (Beta) + %p + Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration. + = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md') + = f.fields_for :auto_devops_attributes, @auto_devops do |form| + .radio + = form.label :enabled_true do + = form.radio_button :enabled, 'true' + %strong Enable Auto DevOps + %br + %span.descr + The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml + in the project. + .radio + = form.label :enabled_false do + = form.radio_button :enabled, 'false' + %strong Disable Auto DevOps + %br + %span.descr + A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery. + .radio + = form.label :enabled do + = form.radio_button :enabled, nil + %strong + Instance default (status: #{current_application_settings.auto_devops_enabled?}) + %br + %span.descr + Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified. + %br + %p + Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed. + = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' + + %hr .form-group.append-bottom-default = f.label :runners_token, "Runner token", class: 'label-light' = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index f6ca8d5a921..755128af565 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -8,7 +8,7 @@ = form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do .form-group = label_tag :source_project_id, "Project", class: 'control-label' - .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) + .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions = button_tag 'Import project members', class: "btn btn-create" diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index eaf374bcb83..d4f71d023c6 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -6,7 +6,7 @@ - expanded = Rails.env.test? -%section.settings +%section.settings#js-general-pipeline-settings .settings-header %h4 General pipelines settings diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 3f0a24cfe83..d8f5114f4b5 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -27,9 +27,10 @@ = link_to project_tags_path(@project) do #{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - if default_project_view != 'readme' && @repository.readme + - if @repository.readme %li - = link_to _('Readme'), readme_path(@project) + = link_to _('Readme'), + default_project_view != 'readme' ? readme_path(@project) : '#readme' - if @repository.changelog %li @@ -81,5 +82,8 @@ - view_path = default_project_view + - if show_auto_devops_callout?(@project) + = render 'shared/auto_devops_callout' + %div{ class: project_child_container_class(view_path) } = render view_path diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 1803e7f7211..65efc083fdd 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,15 +1,11 @@ - page_title "Snippets" -- if can?(current_user, :create_project_snippet, @project) - - content_for :breadcrumbs_extra do - = link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" - - if current_user .top-area - include_private = @project.team.member?(current_user) || current_user.admin? = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } - .nav-controls.visible-xs + .nav-controls - if can?(current_user, :create_project_snippet, @project) = link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet" diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index 4579a912f39..4daacbe157c 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,5 +1,5 @@ - if readme.rich_viewer - %article.file-holder.readme-holder{ class: ("limited-width-container" unless fluid_layout) } + %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) } .js-file-title.file-title = blob_icon readme.mode, readme.name = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 375e6764add..d84a1fd7ee1 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -14,5 +14,7 @@ = render "projects/commits/head" %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } + - if show_auto_devops_callout?(@project) + = render 'shared/auto_devops_callout' = render 'projects/last_push' = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml new file mode 100644 index 00000000000..2f09c2fec87 --- /dev/null +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -0,0 +1,15 @@ +.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } + .bordered-box.landing.content-block + %button.btn.btn-default.close.js-close-callout{ type: 'button', + 'aria-label' => 'Dismiss Auto DevOps box' } + = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') + .svg-container + = custom_icon('icon_autodevops') + .user-callout-copy + %h4= _('Auto DevOps (Beta)') + %p= _('Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') + %p + #{s_('AutoDevOps|Learn more in the')} + = link_to _('Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer' + + = link_to _('Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout' diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index dc912d800cf..ac2ebb701a5 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,5 +1,5 @@ - if any_projects?(@projects) - .project-item-select-holder.btn-group.pull-right + .project-item-select-holder.btn-group %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } } = icon('spinner spin') = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] diff --git a/app/views/projects/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 303e20e8780..1a50b7d4b69 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -9,7 +9,7 @@ = webpack_bundle_tag 'filtered_search' = webpack_bundle_tag 'boards' - %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" + %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal = render "projects/issues/head" @@ -30,7 +30,7 @@ ":root-path" => "rootPath", ":board-id" => "boardId", ":key" => "_uid" } - = render "projects/boards/components/sidebar" + = render "shared/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_project_issue_path(@project), "milestone-path" => milestones_filter_dropdown_path, diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 64f5f6d7ba0..ce0aa72ab00 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -7,20 +7,26 @@ ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }", "aria-hidden": "true" } - %span.has-tooltip{ "v-if": "list.type !== \"label\"", + %span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"", ":title" => '(list.label ? list.label.description : "")' } {{ list.title }} %span.has-tooltip{ "v-if": "list.type === \"label\"", ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" }, - class: "label color-label title", + class: "label color-label title board-title-text", ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" } {{ list.title }} - .issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } + - if can?(current_user, :admin_list, current_board_parent) + %board-delete{ "inline-template" => true, + ":list" => "list", + "v-if" => "!list.preset && list.id" } + %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } + = icon("trash") + .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' } %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ list.issuesSize }} - - if can?(current_user, :admin_issue, @project) + - if can?(current_user, :admin_list, current_board_parent) %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button", "@click" => "showNewIssueForm", "v-if" => 'list.type !== "closed"', @@ -28,12 +34,7 @@ "title" => "New issue", data: { placement: "top", container: "body" } } = icon("plus", class: "js-no-trigger-collapse") - - if can?(current_user, :admin_list, @project) - %board-delete{ "inline-template" => true, - ":list" => "list", - "v-if" => "!list.preset && list.id" } - %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } - = icon("trash") + %board-list{ "v-if" => 'list.type !== "blank"', ":list" => "list", ":issues" => "list.issues", @@ -42,5 +43,5 @@ ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath", "ref" => "board-list" } - - if can?(current_user, :admin_list, @project) + - if can?(current_user, :admin_list, current_board_parent) %board-blank-state{ "v-if" => 'list.id == "blank"' } diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml index 09d70f658a3..b3f73e96b81 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/shared/boards/components/_sidebar.html.haml @@ -10,18 +10,19 @@ %br/ %span = precede "#" do - {{ issue.id }} + {{ issue.iid }} %a.gutter-toggle.pull-right{ role: "button", href: "#", "@click.prevent" => "closeSidebar", "aria-label" => "Toggle sidebar" } = custom_icon("icon_close", size: 15) .js-issuable-update - = render "projects/boards/components/sidebar/assignee" - = render "projects/boards/components/sidebar/milestone" - = render "projects/boards/components/sidebar/due_date" - = render "projects/boards/components/sidebar/labels" - = render "projects/boards/components/sidebar/notifications" + = render "shared/boards/components/sidebar/assignee" + = render "shared/boards/components/sidebar/milestone" + = render "shared/boards/components/sidebar/due_date" + = render "shared/boards/components/sidebar/labels" + = render "shared/boards/components/sidebar/notifications" %remove-btn{ ":issue" => "issue", + ":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'", ":list" => "list", "v-if" => "canRemove" } diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml index 8d957613be1..3d2e8471a60 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml @@ -2,13 +2,13 @@ %template{ "v-if" => "issue.assignees" } %assignee-title{ ":number-of-assignees" => "issue.assignees.length", ":loading" => "loadingAssignees", - ":editable" => can?(current_user, :admin_issue, @project) } + ":editable" => can_admin_issue? } %assignees.value{ "root-path" => "#{root_url}", ":users" => "issue.assignees", - ":editable" => can?(current_user, :admin_issue, @project), + ":editable" => can_admin_issue?, "@assign-self" => "assignSelf" } - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? .selectbox.hide-collapsed %input.js-vue{ type: "hidden", name: "issue[assignee_ids][]", @@ -20,9 +20,9 @@ ":data-username" => "assignee.username" } .dropdown - dropdown_options = issue_assignees_dropdown_options - %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] }, - ":data-issuable-id" => "issue.id", - ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } + %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data, + ":data-issuable-id" => "issue.iid", + ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" } = dropdown_options[:title] = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml index e8394eab213..db794d6f855 100644 --- a/app/views/projects/boards/components/sidebar/_due_date.html.haml +++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml @@ -1,7 +1,7 @@ .block.due_date .title Due date - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" .value @@ -10,12 +10,12 @@ No due date %span.bold{ "v-if" => "issue.dueDate" } {{ issue.dueDate | due-date }} - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" } \- %a.js-remove-due-date{ href: "#", role: "button" } remove due date - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? .selectbox %input{ type: "hidden", name: "issue[due_date]", @@ -23,7 +23,7 @@ .dropdown %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button', data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" }, - ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } + ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" } %span.dropdown-toggle-text Due date = icon('chevron-down') .dropdown-menu.dropdown-menu-due-date diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 6b389736e8b..1f540bdaf93 100644 --- a/app/views/projects/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -1,7 +1,7 @@ .block.labels .title Labels - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" .value.issuable-show-labels @@ -11,7 +11,7 @@ "v-for" => "label in issue.labels" } %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } {{ label.title }} - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? .selectbox %input{ type: "hidden", name: "issue[label_names][]", @@ -19,12 +19,19 @@ ":value" => "label.id" } .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", - data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: project_labels_path(@project, :json), namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) }, - ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } + data: { toggle: "dropdown", + field_name: "issue[label_names][]", + show_no: "true", + show_any: "true", + project_id: @project&.try(:id), + labels: labels_filter_path(false), + namespace_path: @project.try(:namespace).try(:full_path), + project_path: @project.try(:path) }, + ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" } %span.dropdown-toggle-text Label = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default" - - if can? current_user, :admin_label, @project and @project + - if can?(current_user, :admin_label, current_board_parent) = render partial: "shared/issuable/label_page_create" diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml index a1ddb261ea3..d09c7c218e0 100644 --- a/app/views/projects/boards/components/sidebar/_milestone.html.haml +++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml @@ -1,7 +1,7 @@ .block.milestone .title Milestone - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? = icon("spinner spin", class: "block-loading") = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" .value @@ -9,17 +9,17 @@ None %span.bold.has-tooltip{ "v-if" => "issue.milestone" } {{ issue.milestone.title }} - - if can?(current_user, :admin_issue, @project) + - if can_admin_issue? .selectbox %input{ type: "hidden", ":value" => "issue.milestone.id", name: "issue[milestone_id]", "v-if" => "issue.milestone" } .dropdown - %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), ability_name: "issue", use_id: "true", default_no: "true" }, + %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" }, ":data-selected" => "milestoneTitle", - ":data-issuable-id" => "issue.id", - ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } + ":data-issuable-id" => "issue.iid", + ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" } Milestone = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml index aaddd7e249f..9b989c23cab 100644 --- a/app/views/projects/boards/components/sidebar/_notifications.html.haml +++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml @@ -1,5 +1,5 @@ - if current_user - .block.light.subscription{ ":data-url" => "'#{project_issues_path(@project)}/' + issue.id + '/toggle_subscription'" } + .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" } %span.issuable-header-text.hide-collapsed.pull-left Notifications %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } diff --git a/app/views/shared/boards/index.html.haml b/app/views/shared/boards/index.html.haml new file mode 100644 index 00000000000..2a5b8b1441e --- /dev/null +++ b/app/views/shared/boards/index.html.haml @@ -0,0 +1 @@ += render "show" diff --git a/app/views/shared/boards/show.html.haml b/app/views/shared/boards/show.html.haml new file mode 100644 index 00000000000..2a5b8b1441e --- /dev/null +++ b/app/views/shared/boards/show.html.haml @@ -0,0 +1 @@ += render "show" diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg new file mode 100644 index 00000000000..807ff27bb67 --- /dev/null +++ b/app/views/shared/icons/_icon_autodevops.svg @@ -0,0 +1,54 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="189" height="179" viewBox="0 0 189 179"> + <g fill="none" fill-rule="evenodd"> + <path fill="#FFFFFF" fill-rule="nonzero" d="M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/> + <path fill="#EEEEEE" fill-rule="nonzero" d="M110.160166,51.6956996 C106.846457,51.6956996 104.160166,54.3819911 104.160166,57.6956996 L104.160166,117.6957 C104.160166,121.009408 106.846457,123.6957 110.160166,123.6957 L160.160166,123.6957 C163.473874,123.6957 166.160166,121.009408 166.160166,117.6957 L166.160166,57.6956996 C166.160166,54.3819911 163.473874,51.6956996 160.160166,51.6956996 L110.160166,51.6956996 Z M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/> + <rect width="70" height="80" x="80" y="34" fill="#000000" fill-opacity=".03" transform="rotate(5 115 74)" rx="10"/> + <path fill="#000000" fill-opacity=".03" fill-rule="nonzero" d="M126.028,178 L150,178 C169.972,177.457 186,161.1 186,141 C186,120.565 169.435,104 149,104 C148.007,104 147.023,104.04 146.05,104.116 C141.1,89.51 127.277,79 111,79 C105.71511,78.9924557 100.490375,80.1212986 95.68,82.31 C89.03,72.47 77.77,66 65,66 C44.565,66 28,82.565 28,103 C28,103.7 28.02,104.397 28.058,105.088 C11.944,109.088 0,123.648 0,141 C0,161.435 16.565,178 37,178 C37.324,178 37.646,177.996 37.968,177.988 L126.028,178 Z"/> + <g transform="rotate(5 -300.07 1010.998)"> + <rect width="70" height="80" fill="#FFFFFF" rx="10"/> + <path fill="#EEEEEE" fill-rule="nonzero" d="M10,4 C6.6862915,4 4,6.6862915 4,10 L4,70 C4,73.3137085 6.6862915,76 10,76 L60,76 C63.3137085,76 66,73.3137085 66,70 L66,10 C66,6.6862915 63.3137085,4 60,4 L10,4 Z M10,-2.68673972e-14 L60,-2.68673972e-14 C65.5228475,-2.78819278e-14 70,4.4771525 70,10 L70,70 C70,75.5228475 65.5228475,80 60,80 L10,80 C4.4771525,80 9.15550472e-14,75.5228475 9.08786935e-14,70 L9.08786935e-14,10 C9.02023397e-14,4.4771525 4.4771525,-2.58528666e-14 10,-2.68673972e-14 Z"/> + <rect width="8" height="4" x="14" y="18" fill="#FC6D26" rx="2"/> + <rect width="8" height="4" x="32" y="38" fill="#E1DBF1" rx="2"/> + <rect width="8" height="4" x="45" y="48" fill="#FEE1D3" rx="2"/> + <rect width="8" height="4" x="44" y="38" fill="#FEF0E8" rx="2"/> + <rect width="12" height="4" x="26" y="18" fill="#EFEDF8" rx="2"/> + <rect width="6" height="4" x="15" y="48" fill="#FEF0E8" rx="2"/> + <rect width="6" height="4" x="25" y="48" fill="#FC6D26" rx="2"/> + <rect width="6" height="4" x="35" y="48" fill="#EEEEEE" rx="2"/> + <rect width="12" height="4" x="14" y="28" fill="#EEEEEE" rx="2"/> + <rect width="12" height="4" x="44" y="58" fill="#6B4FBB" rx="2"/> + <rect width="26" height="4" x="30" y="28" fill="#C3B8E3" rx="2"/> + <rect width="26" height="4" x="15" y="58" fill="#FEE1D3" rx="2"/> + <rect width="14" height="4" x="42" y="18" fill="#E1DBF1" rx="2"/> + <rect width="14" height="4" x="14" y="38" fill="#6B4FBB" rx="2"/> + </g> + <g transform="translate(3 67)"> + <path fill="#FFFFFF" fill-rule="nonzero" d="M126.028,112 L150,112 C169.972,111.457 186,95.1 186,75 C186,54.565 169.435,38 149,38 C148.007,38 147.023,38.04 146.05,38.116 C141.1,23.51 127.277,13 111,13 C105.71511,12.9924557 100.490375,14.1212986 95.68,16.31 C89.03,6.47 77.77,0 65,0 C44.565,0 28,16.565 28,37 C28,37.7 28.02,38.397 28.058,39.088 C11.944,43.088 0,57.648 0,75 C0,95.435 16.565,112 37,112 C37.324,112 37.646,111.996 37.968,111.988 L126.028,112 Z"/> + <path fill="#FFFFFF" d="M126.028,110 L149.946,110 C168.876,109.486 184,93.976 184,75 C184,55.67 168.33,40 149,40 C148.064,40 147.133,40.037 146.207,40.11 L144.656,40.232 L144.156,38.758 C139.381,24.67 126.116,15 111,15 C106.001056,14.9926759 101.058995,16.0604828 96.509,18.131 L94.969,18.832 L94.022,17.431 C87.552,7.855 76.774,2 65,2 C45.67,2 30,17.67 30,37 C30,37.661 30.018,38.32 30.055,38.977 L30.147,40.63 L28.54,41.029 C13.06,44.87 2,58.827 2,75 C2,94.33 17.67,110 37,110 C37.306,110 37.612,109.996 37.968,109.988 L126.028,110 Z"/> + <path fill="#EEEEEE" fill-rule="nonzero" d="M149,38 C169.434569,38 186,54.5654305 186,75 C186,95.0477955 170.024944,111.45554 149.946,112 L126.028,112 L38.0129325,111.987495 C37.6348039,111.995992 37.3157048,112 37,112 C16.5654305,112 0,95.4345695 0,75 C0,57.9772494 11.6188339,43.1669444 28.0580557,39.0879359 C28.0192558,38.39805 28,37.7022134 28,37 C28,16.5654305 44.5654305,0 65,0 C77.3556804,0 88.7905015,6.1156181 95.6789703,16.310978 C100.491636,14.1213221 105.717209,12.9922579 111,13 C126.893041,13 140.974134,23.139867 146.049999,38.1155308 C147.031471,38.0388091 148.014765,38 149,38 Z M149.891715,108.000737 C167.750695,107.515818 182,92.8805667 182,75 C182,56.7745695 167.225431,42 149,42 C148.1197,42 147.241142,42.0346799 146.363833,42.1038413 L144.812833,42.2258413 C143.900567,42.2975992 143.055957,41.7410534 142.762001,40.8744692 L142.261844,39.400007 C137.735045,26.0442906 125.175364,17 110.99707,16.9999979 C106.284902,16.9930939 101.626355,17.9996435 97.3375854,19.9512874 L95.7975854,20.6522874 C94.9091924,21.0566793 93.8586575,20.7607079 93.3120297,19.952022 L92.3648004,18.5506827 C86.2175802,9.45241684 76.0227954,4 65,4 C46.7745695,4 32,18.7745695 32,37 C32,37.6282966 32.0172077,38.249659 32.0519095,38.8658592 L32.1439095,40.5188592 C32.1972908,41.4779816 31.5612439,42.3395846 30.6289443,42.5710641 L29.0216479,42.9701376 C14.3638424,46.6071293 4,59.8177208 4,75 C4,93.2254305 18.7745695,108 37,108 C37.2837952,108 37.5733598,107.996363 37.9682725,107.988 L126.02825,108 L149.891715,108.000737 Z"/> + </g> + <g fill-rule="nonzero" transform="rotate(15 -315.035 277.714)"> + <path fill="#FFFFFF" d="M12.275,10.57 C13.986216,9.15630755 15.921048,8.03765363 18,7.26 L18,5.5 C18,2.463 20.47,0 23.493,0 L26.507,0 C27.9648848,0.000530018716 29.3628038,0.580386367 30.3930274,1.61192286 C31.4232511,2.64345935 32.0013267,4.04211574 32,5.5 L32,7.26 C34.098,8.043 36.03,9.17 37.725,10.57 L39.253,9.688 C41.8816141,8.17268496 45.2407537,9.07039379 46.763,11.695 L48.27,14.305 C48.9984289,15.5678669 49.1951495,17.0684426 48.8168566,18.4763972 C48.4385638,19.8843518 47.5162683,21.0842673 46.253,21.812 L44.728,22.693 C44.907,23.769 45,24.873 45,26 C45,27.127 44.907,28.231 44.728,29.307 L46.253,30.187 C48.8800379,31.705769 49.7822744,35.0642181 48.27,37.695 L46.763,40.305 C46.0335844,41.5673849 44.8323832,42.4881439 43.4238487,42.8645658 C42.0153143,43.2409877 40.5149245,43.0422119 39.253,42.312 L37.725,41.43 C36.013784,42.8436924 34.078952,43.9623464 32,44.74 L32,46.5 C32,49.537 29.53,52 26.507,52 L23.493,52 C22.0351152,51.99947 20.6371962,51.4196136 19.6069726,50.3880771 C18.5767489,49.3565406 17.9986733,47.9578843 18,46.5 L18,44.74 C15.921048,43.9623464 13.986216,42.8436924 12.275,41.43 L10.747,42.312 C8.11838594,43.827315 4.75924629,42.9296062 3.237,40.305 L1.73,37.695 C1.00157113,36.4321331 0.804850523,34.9315574 1.18314337,33.5236028 C1.56143621,32.1156482 2.48373172,30.9157327 3.747,30.188 L5.272,29.307 C5.09051204,28.2140265 4.9995366,27.107939 5,26 C5,24.873 5.093,23.769 5.272,22.693 L3.747,21.813 C1.11996213,20.294231 0.217725591,16.9357819 1.73,14.305 L3.237,11.695 C3.96641559,10.4326151 5.16761682,9.51185609 6.57615125,9.13543417 C7.98468568,8.75901226 9.48507553,8.95778814 10.747,9.688 L12.275,10.57 Z"/> + <path fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/> + <g transform="rotate(15 -59.137 82.348)"> + <circle cx="8" cy="8" r="8" fill="#FFFFFF" transform="translate(.035 6.008)"/> + <path fill="#6B4FBB" d="M7.40192379,14.7679492 C2.98364579,14.7679492 -0.598076211,11.1862272 -0.598076211,6.76794919 C-0.598076211,2.34967119 2.98364579,-1.23205081 7.40192379,-1.23205081 C11.8202018,-1.23205081 15.4019238,2.34967119 15.4019238,6.76794919 C15.4019238,11.1862272 11.8202018,14.7679492 7.40192379,14.7679492 Z M7.40192379,10.7679492 C9.61106279,10.7679492 11.4019238,8.97708819 11.4019238,6.76794919 C11.4019238,4.55881019 9.61106279,2.76794919 7.40192379,2.76794919 C5.19278479,2.76794919 3.40192379,4.55881019 3.40192379,6.76794919 C3.40192379,8.97708819 5.19278479,10.7679492 7.40192379,10.7679492 Z"/> + </g> + </g> + <g fill-rule="nonzero" transform="rotate(15 -402.968 460.884)"> + <path fill="#FFFFFF" d="M9.82,8.53730769 C11.1889728,7.39547918 12.7368384,6.49195101 14.4,5.86384615 L14.4,4.44230769 C14.4,1.98934615 16.376,0 18.7944,0 L21.2056,0 C22.3719078,0.00042809204 23.4902431,0.468773604 24.314422,1.30193769 C25.1386009,2.13510179 25.6010613,3.26478579 25.6,4.44230769 L25.6,5.86384615 C27.2784,6.49626923 28.824,7.40653846 30.18,8.53730769 L31.4024,7.82492308 C33.5052912,6.60101478 36.192603,7.32608729 37.4104,9.44596154 L38.616,11.5540385 C39.1987431,12.5740464 39.3561196,13.7860498 39.0534853,14.9232439 C38.750851,16.060438 38.0130146,17.0296006 37.0024,17.6173846 L35.7824,18.3289615 C35.9256,19.1980385 36,20.0897308 36,21 C36,21.9102692 35.9256,22.8019615 35.7824,23.6710385 L37.0024,24.3818077 C39.1040303,25.6085057 39.8258195,28.3210992 38.616,30.4459615 L37.4104,32.5540385 C36.8268675,33.573657 35.8659065,34.317347 34.739079,34.6213801 C33.6122515,34.9254132 32.4119396,34.7648634 31.4024,34.1750769 L30.18,33.4626923 C28.8110272,34.6045208 27.2631616,35.508049 25.6,36.1361538 L25.6,37.5576923 C25.6,40.0106538 23.624,42 21.2056,42 L18.7944,42 C17.6280922,41.9995719 16.5097569,41.5312264 15.685578,40.6980623 C14.8613991,39.8648982 14.3989387,38.7352142 14.4,37.5576923 L14.4,36.1361538 C12.7368384,35.508049 11.1889728,34.6045208 9.82,33.4626923 L8.5976,34.1750769 C6.49470875,35.3989852 3.80739703,34.6739127 2.5896,32.5540385 L1.384,30.4459615 C0.8012569,29.4259536 0.643880418,28.2139502 0.946514692,27.0767561 C1.24914897,25.939562 1.98698538,24.9703994 2.9976,24.3826154 L4.2176,23.6710385 C4.07240963,22.7882521 3.99962928,21.8948738 4,21 C4,20.0897308 4.0744,19.1980385 4.2176,18.3289615 L2.9976,17.6181923 C0.895969702,16.3914943 0.174180473,13.6789008 1.384,11.5540385 L2.5896,9.44596154 C3.17313247,8.42634297 4.13409345,7.682653 5.260921,7.37861991 C6.38774855,7.07458682 7.58806043,7.23513658 8.5976,7.82492308 L9.82,8.53730769 Z"/> + <path fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/> + <g transform="rotate(15 -47.892 66.043)"> + <ellipse cx="6.4" cy="6.462" fill="#FFFFFF" rx="6.4" ry="6.462" transform="translate(.028 4.853)"/> + <path fill="#FC6D26" d="M5.92153903,11.9125743 C2.3834711,11.9125743 -0.478460969,9.0231237 -0.478460969,5.4664205 C-0.478460969,1.9097173 2.3834711,-0.979733345 5.92153903,-0.979733345 C9.45960696,-0.979733345 12.321539,1.9097173 12.321539,5.4664205 C12.321539,9.0231237 9.45960696,11.9125743 5.92153903,11.9125743 Z M5.92153903,8.71257435 C7.6854047,8.71257435 9.12153903,7.26263103 9.12153903,5.4664205 C9.12153903,3.67020997 7.6854047,2.22026666 5.92153903,2.22026666 C4.15767337,2.22026666 2.72153903,3.67020997 2.72153903,5.4664205 C2.72153903,7.26263103 4.15767337,8.71257435 5.92153903,8.71257435 Z"/> + </g> + </g> + <path fill="#000000" fill-opacity=".03" d="M61.6792606,38.251778 C61.8904713,36.8653316 62,35.4454567 62,34 C62,18.536027 49.463973,6 34,6 C18.536027,6 6,18.536027 6,34 C6,49.463973 18.536027,62 34,62 C42.8132237,62 50.6754255,57.9281916 55.8080076,51.5631726 L64.2689981,50.6250607 C64.4699867,50.6027761 64.6664333,50.5501384 64.8516368,50.4689431 C65.8632575,50.0254374 66.3238058,48.8458244 65.8803001,47.8342037 L65.8803001,47.8342037 L61.6792599,38.2517794 Z"/> + <path fill="#FFFFFF" d="M63.6792606,34.251778 C63.8904713,32.8653316 64,31.4454567 64,30 C64,14.536027 51.463973,2 36,2 C20.536027,2 8,14.536027 8,30 C8,45.463973 20.536027,58 36,58 C44.8132237,58 52.6754255,53.9281916 57.8080076,47.5631726 L66.2689981,46.6250607 C66.4699867,46.6027761 66.6664333,46.5501384 66.8516368,46.4689431 C67.8632575,46.0254374 68.3238058,44.8458244 67.8803001,43.8342037 L67.8803001,43.8342037 L63.6792599,34.2517794 Z"/> + <path fill="#EEEEEE" fill-rule="nonzero" d="M69.7120015,43.0311656 C70.5990128,45.0544071 69.6779163,47.4136331 67.6546748,48.3006445 C67.2842678,48.463035 66.8913746,48.5683104 66.4893975,48.6128796 L58.8313193,49.4619687 C53.1777737,56.0908093 44.9077957,60 36,60 C19.4314575,60 6,46.5685425 6,30 C6,13.4314575 19.4314575,0 36,0 C52.5685425,0 66,13.4314575 66,30 C66,31.335699 65.9125851,32.6609639 65.739427,33.9698636 L69.7120015,43.0311656 Z M61.7020717,33.9505738 C61.8999153,32.6518726 62,31.332589 62,30 C62,15.6405965 50.3594035,4 36,4 C21.6405965,4 10,15.6405965 10,30 C10,44.3594035 21.6405965,56 36,56 C43.969518,56 51.3430155,52.3943837 56.251122,46.3077415 L56.7684631,45.6661764 L66.0485988,44.6372417 L61.8475593,35.054816 L61.6147491,34.5237842 L61.7020717,33.9505738 Z"/> + <g fill="#31AF64" fill-rule="nonzero" transform="translate(24 18)"> + <path d="M12.5,26.5 C4.7680135,26.5 -1.5,20.2319865 -1.5,12.5 C-1.5,4.7680135 4.7680135,-1.5 12.5,-1.5 C20.2319865,-1.5 26.5,4.7680135 26.5,12.5 C26.5,20.2319865 20.2319865,26.5 12.5,26.5 Z M12.5,23.5 C18.5751322,23.5 23.5,18.5751322 23.5,12.5 C23.5,6.42486775 18.5751322,1.5 12.5,1.5 C6.42486775,1.5 1.5,6.42486775 1.5,12.5 C1.5,18.5751322 6.42486775,23.5 12.5,23.5 Z"/> + <path d="M11.18,13.81 L9.248,11.878 C8.67483243,11.3054203 7.74616757,11.3054203 7.173,11.878 C6.89709997,12.1525667 6.74198837,12.5257601 6.74198837,12.915 C6.74198837,13.3042399 6.89709997,13.6774333 7.173,13.952 L10.048,16.826 C10.0636337,16.8423622 10.0796378,16.8583663 10.096,16.874 C10.646,17.424 11.526,17.423 12.071,16.879 L17.879,11.071 C18.1403709,10.8085057 18.2866977,10.4528922 18.2857599,10.0824639 C18.2848221,9.71203558 18.1366966,9.35716757 17.874,9.096 C17.6132271,8.83256594 17.2582132,8.68392968 16.8875393,8.68299126 C16.5168653,8.68205285 16.1611034,8.82888967 15.899,9.091 L11.18,13.81 Z"/> + </g> + </g> +</svg> diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index e8feff32d26..ad031e6af80 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -8,20 +8,19 @@ - if show_boards_content .issue-board-dropdown-content %p - Create lists from the labels you use in your project. Issues with that - label will automatically be added to the list. + Create lists from labels. Issues with that label appear in that list. = dropdown_filter(filter_placeholder) = dropdown_content - - if @project && show_footer + - if current_board_parent && show_footer = dropdown_footer do %ul.dropdown-footer-list - - if can?(current_user, :admin_label, @project) + - if can?(current_user, :admin_label, current_board_parent) %li %a.dropdown-toggle-page{ href: "#" } Create new label %li - = link_to project_labels_path(@project), :"data-is-link" => true do - - if show_create && @project && can?(current_user, :admin_label, @project) + = link_to labels_path, :"data-is-link" => true do + - if show_create && can?(current_user, :admin_label, current_board_parent) Manage labels - else View labels diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index e81789ea7a2..161b1c9fd72 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -104,13 +104,13 @@ = icon('times') .filter-dropdown-container - if type == :boards - - if can?(current_user, :admin_list, @project) + - if can?(current_user, :admin_list, board.parent) .dropdown.prepend-left-10#js-add-list - %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) } } + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - - if can?(current_user, :admin_label, @project) + - if can?(current_user, :admin_label, board.parent) = render partial: "shared/issuable/label_page_create" = dropdown_loading #js-add-issues-btn.prepend-left-10 diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 093b2d82813..79e8f8d0e89 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -6,15 +6,15 @@ %script#js-register-u2f-setup{ type: "text/template" } - if current_user.two_factor_otp_enabled? .row.append-bottom-10 - .col-md-3 - %button#js-setup-u2f-device.btn.btn-info Setup new U2F device - .col-md-9 + .col-md-4 + %button#js-setup-u2f-device.btn.btn-info.btn-block Setup new U2F device + .col-md-8 %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. - else .row.append-bottom-10 - .col-md-3 - %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup new U2F device - .col-md-9 + .col-md-4 + %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Setup new U2F device + .col-md-8 %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device. %script#js-register-u2f-in-progress{ type: "text/template" } diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index c95497dfaba..ec65d3ff65e 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -5,6 +5,9 @@ class GitGarbageCollectWorker sidekiq_options retry: false + # Timeout set to 24h + LEASE_TIMEOUT = 86400 + GITALY_MIGRATED_TASKS = { gc: :garbage_collect, full_repack: :repack_full, @@ -13,8 +16,19 @@ class GitGarbageCollectWorker def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) project = Project.find(project_id) - task = task.to_sym + active_uuid = get_lease_uuid(lease_key) + + if active_uuid + return unless active_uuid == lease_uuid + + renew_lease(lease_key, active_uuid) + else + lease_uuid = try_obtain_lease(lease_key) + + return unless lease_uuid + end + task = task.to_sym cmd = command(task) repo_path = project.repository.path_to_repo description = "'#{cmd.join(' ')}' in #{repo_path}" @@ -33,11 +47,27 @@ class GitGarbageCollectWorker # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc ensure - Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? + cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? end private + def try_obtain_lease(key) + ::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain + end + + def renew_lease(key, uuid) + ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew + end + + def cancel_lease(key, uuid) + ::Gitlab::ExclusiveLease.cancel(key, uuid) + end + + def get_lease_uuid(key) + ::Gitlab::ExclusiveLease.get_uuid(key) + end + ## `repository` has to be a Gitlab::Git::Repository def gitaly_call(task, repository) client = Gitlab::GitalyClient::RepositoryService.new(repository) diff --git a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml new file mode 100644 index 00000000000..abb9e33d626 --- /dev/null +++ b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml @@ -0,0 +1,5 @@ +--- +title: Removes Sortable default scope. +merge_request: 13558 +author: +type: fixed diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml new file mode 100644 index 00000000000..4a8d8097169 --- /dev/null +++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml @@ -0,0 +1,5 @@ +--- +title: Decrease Cyclomatic Complexity threshold to 14 +merge_request: 13972 +author: Maxim Rydkin +type: other diff --git a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml new file mode 100644 index 00000000000..fde70c47ca6 --- /dev/null +++ b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml @@ -0,0 +1,5 @@ +--- +title: Redesign project feature permissions settings +merge_request: 14062 +author: +type: changed diff --git a/changelogs/unreleased/34945-readme-div-id.yml b/changelogs/unreleased/34945-readme-div-id.yml new file mode 100644 index 00000000000..c7d26b746b4 --- /dev/null +++ b/changelogs/unreleased/34945-readme-div-id.yml @@ -0,0 +1,5 @@ +--- +title: Add div id to the readme in the project overview +merge_request: 13735 +author: Riccardo Padovani @rpadovani +type: added diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml new file mode 100644 index 00000000000..74aa337a18c --- /dev/null +++ b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml @@ -0,0 +1,5 @@ +--- +title: Add option in preferences to change navigation theme color +merge_request: +author: +type: added diff --git a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml new file mode 100644 index 00000000000..76c9c905590 --- /dev/null +++ b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml @@ -0,0 +1,5 @@ +--- +title: Normalize styles for empty state combo button +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37158-autodevops-banner.yml b/changelogs/unreleased/37158-autodevops-banner.yml new file mode 100644 index 00000000000..f0ffb670102 --- /dev/null +++ b/changelogs/unreleased/37158-autodevops-banner.yml @@ -0,0 +1,5 @@ +--- +title: Created callout for auto devops +merge_request: +author: +type: added diff --git a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml new file mode 100644 index 00000000000..d6d21ac4c51 --- /dev/null +++ b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml @@ -0,0 +1,5 @@ +--- +title: Fix a wrong `X-Gitlab-Event` header when testing webhooks +merge_request: 14108 +author: +type: fixed diff --git a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml new file mode 100644 index 00000000000..6a955f5715f --- /dev/null +++ b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Make blob viewer for rich contents wider for mobile +merge_request: 14011 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml b/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml new file mode 100644 index 00000000000..3a99729fb48 --- /dev/null +++ b/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml @@ -0,0 +1,5 @@ +--- +title: Show notes number more user-friendly in the graph +merge_request: 13949 +author: Vladislav Kaverin +type: changed diff --git a/changelogs/unreleased/docs-confidential-issue.yml b/changelogs/unreleased/docs-confidential-issue.yml new file mode 100644 index 00000000000..841970ef4cf --- /dev/null +++ b/changelogs/unreleased/docs-confidential-issue.yml @@ -0,0 +1,5 @@ +--- +title: Update documentation for confidential issue +merge_request: 14117 +author: +type: other diff --git a/changelogs/unreleased/events-migration-cleanup.yml b/changelogs/unreleased/events-migration-cleanup.yml new file mode 100644 index 00000000000..1e3e843f252 --- /dev/null +++ b/changelogs/unreleased/events-migration-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: Finish migration to the new events setup +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/feature-plantuml-restructured-text.yml b/changelogs/unreleased/feature-plantuml-restructured-text.yml new file mode 100644 index 00000000000..b885029f589 --- /dev/null +++ b/changelogs/unreleased/feature-plantuml-restructured-text.yml @@ -0,0 +1,5 @@ +--- +title: Add documentation for PlantUML in reStructuredText +merge_request: 13900 +author: Markus Koller +type: other diff --git a/changelogs/unreleased/fix-gem-security-updates.yml b/changelogs/unreleased/fix-gem-security-updates.yml deleted file mode 100644 index dce11d08402..00000000000 --- a/changelogs/unreleased/fix-gem-security-updates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade mail and nokogiri gems due to security issues -merge_request: 13662 -author: Markus Koller -type: security diff --git a/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml b/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml new file mode 100644 index 00000000000..ae4df3ee31a --- /dev/null +++ b/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml @@ -0,0 +1,5 @@ +--- +title: Fix stray OR in New Project page +merge_request: 14096 +author: Robin Bobbitt +type: fixed diff --git a/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml new file mode 100644 index 00000000000..90c25d782c8 --- /dev/null +++ b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml @@ -0,0 +1,5 @@ +--- +title: Gitaly feature toggles are on by default in development +merge_request: 13802 +author: +type: other diff --git a/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml new file mode 100644 index 00000000000..ec0d1b245e4 --- /dev/null +++ b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml @@ -0,0 +1,6 @@ +--- +title: '"Share with group lock" now applies to subgroups, but owner can override setting + on subgroups' +merge_request: 13944 +author: +type: changed diff --git a/changelogs/unreleased/sh-add-grape-logging.yml b/changelogs/unreleased/sh-add-grape-logging.yml new file mode 100644 index 00000000000..eaf6cb045d5 --- /dev/null +++ b/changelogs/unreleased/sh-add-grape-logging.yml @@ -0,0 +1,5 @@ +--- +title: Add JSON logger in `log/api_json.log` for Grape API endpoints +merge_request: +author: +type: added diff --git a/changelogs/unreleased/support-additional-colors.yml b/changelogs/unreleased/support-additional-colors.yml new file mode 100644 index 00000000000..5178e159dcf --- /dev/null +++ b/changelogs/unreleased/support-additional-colors.yml @@ -0,0 +1,5 @@ +--- +title: Added support for specific labels and colors +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/uipolish-fix-2factor-warning.yml b/changelogs/unreleased/uipolish-fix-2factor-warning.yml new file mode 100644 index 00000000000..9f55207d309 --- /dev/null +++ b/changelogs/unreleased/uipolish-fix-2factor-warning.yml @@ -0,0 +1,5 @@ +--- +title: Two factor auth messages in settings no longer overlap the button +merge_request: +author: Jedidiah Broadbent +type: fixed diff --git a/changelogs/unreleased/zj-auto-devops-table.yml b/changelogs/unreleased/zj-auto-devops-table.yml new file mode 100644 index 00000000000..f1a004ebd19 --- /dev/null +++ b/changelogs/unreleased/zj-auto-devops-table.yml @@ -0,0 +1,5 @@ +--- +title: Allow users and administrator to configure Auto-DevOps +merge_request: 13923 +author: +type: added diff --git a/changelogs/unreleased/zj-update-rails-template.yml b/changelogs/unreleased/zj-update-rails-template.yml new file mode 100644 index 00000000000..5464f0e3d42 --- /dev/null +++ b/changelogs/unreleased/zj-update-rails-template.yml @@ -0,0 +1,5 @@ +--- +title: Update Rails project template to use Postgresql by default +merge_request: +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e9661090844..cd44f888d3f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,6 +76,13 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace + ## Default theme ID + ## 1 - Indigo + ## 2 - Dark + ## 3 - Light + ## 4 - Blue + ## 5 - Green + # default_theme: 1 # default: 1 ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb index f977104ff9d..1ad9ddca877 100644 --- a/config/initializers/0_inflections.rb +++ b/config/initializers/0_inflections.rb @@ -10,5 +10,10 @@ # end # ActiveSupport::Inflector.inflections do |inflect| - inflect.uncountable %w(award_emoji project_statistics system_note_metadata) + inflect.uncountable %w( + award_emoji + project_statistics + system_note_metadata + project_auto_devops + ) end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7c1ca05a57b..40fbdd3ef9b 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? +Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? diff --git a/config/routes.rb b/config/routes.rb index ce7ab1d20f6..5683725c8a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,19 @@ Rails.application.routes.draw do # Notification settings resources :notification_settings, only: [:create, :update] + # Boards resources shared between group and projects + resources :boards do + resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do + collection do + post :generate + end + + resources :issues, only: [:index, :create, :update] + end + + resources :issues, module: :boards, only: [:index, :update] + end + draw :import draw :uploads draw :explore diff --git a/config/routes/project.rb b/config/routes/project.rb index a15e7f8a344..b36d13888cd 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -343,19 +343,7 @@ constraints(ProjectUrlConstrainer.new) do get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes' - resources :boards, only: [:index, :show] do - scope module: :boards do - resources :issues, only: [:index, :update] - - resources :lists, only: [:index, :create, :update, :destroy] do - collection do - post :generate - end - - resources :issues, only: [:index, :create] - end - end - end + resources :boards, only: [:index, :show, :create, :update, :destroy] resources :todos, only: [:create] diff --git a/db/migrate/20170816234252_add_theme_id_to_users.rb b/db/migrate/20170816234252_add_theme_id_to_users.rb new file mode 100644 index 00000000000..5043f9ec591 --- /dev/null +++ b/db/migrate/20170816234252_add_theme_id_to_users.rb @@ -0,0 +1,10 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddThemeIdToUsers < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :users, :theme_id, :integer, limit: 2 + end +end diff --git a/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb new file mode 100644 index 00000000000..da518d8215c --- /dev/null +++ b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb @@ -0,0 +1,15 @@ +class AddAutoDevopsEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :auto_devops_enabled, :boolean, default: false) + end + + def down + remove_column(:application_settings, :auto_devops_enabled, :boolean) + end +end diff --git a/db/migrate/20170828093725_create_project_auto_dev_ops.rb b/db/migrate/20170828093725_create_project_auto_dev_ops.rb new file mode 100644 index 00000000000..c1bb4f20c1d --- /dev/null +++ b/db/migrate/20170828093725_create_project_auto_dev_ops.rb @@ -0,0 +1,19 @@ +class CreateProjectAutoDevOps < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + create_table :project_auto_devops do |t| + t.belongs_to :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + t.boolean :enabled, default: nil, null: true + t.string :domain + end + end + + def down + drop_table(:project_auto_devops) + end +end diff --git a/db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb b/db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb new file mode 100644 index 00000000000..0dfdc4ed261 --- /dev/null +++ b/db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class StealRemainingEventMigrationJobs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + Gitlab::BackgroundMigration.steal('MigrateEventsToPushEventPayloads') + end + + def down + end +end diff --git a/db/migrate/20170830131015_swap_event_migration_tables.rb b/db/migrate/20170830131015_swap_event_migration_tables.rb new file mode 100644 index 00000000000..5128d1b2fe7 --- /dev/null +++ b/db/migrate/20170830131015_swap_event_migration_tables.rb @@ -0,0 +1,23 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class SwapEventMigrationTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + rename_tables + end + + def down + rename_tables + end + + def rename_tables + rename_table :events, :events_old + rename_table :events_for_migration, :events + rename_table :events_old, :events_for_migration + end +end diff --git a/db/migrate/20170831092813_add_config_source_to_pipelines.rb b/db/migrate/20170831092813_add_config_source_to_pipelines.rb new file mode 100644 index 00000000000..ff51e968abd --- /dev/null +++ b/db/migrate/20170831092813_add_config_source_to_pipelines.rb @@ -0,0 +1,7 @@ +class AddConfigSourceToPipelines < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:ci_pipelines, :config_source, :integer, allow_null: true) + end +end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index be3501c4c2e..5cd9f3198e3 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -7,6 +7,5 @@ class LimitsToMysql < ActiveRecord::Migration change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 - change_column :events, :data, :text, limit: 2147483647 end end diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb index 3a4d6c4916b..9d9f36550e7 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -54,14 +54,14 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration def with_temporary_partial_index if Gitlab::Database.postgresql? - unless index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration) + unless index_exists?(:ci_builds, :id, name: :index_for_ci_builds_retried_migration) execute 'CREATE INDEX CONCURRENTLY index_for_ci_builds_retried_migration ON ci_builds (id) WHERE retried IS NULL;' end end yield - if Gitlab::Database.postgresql? && index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration) + if Gitlab::Database.postgresql? && index_exists?(:ci_builds, :id, name: :index_for_ci_builds_retried_migration) execute 'DROP INDEX CONCURRENTLY index_for_ci_builds_retried_migration' end end diff --git a/db/post_migrate/20170830150306_drop_events_for_migration_table.rb b/db/post_migrate/20170830150306_drop_events_for_migration_table.rb new file mode 100644 index 00000000000..763ee9a810d --- /dev/null +++ b/db/post_migrate/20170830150306_drop_events_for_migration_table.rb @@ -0,0 +1,48 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class DropEventsForMigrationTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class Event < ActiveRecord::Base + include EachBatch + end + + def up + transaction do + drop_table :events_for_migration + end + end + + # rubocop: disable Migration/Datetime + def down + create_table :events_for_migration do |t| + t.string :target_type, index: true + t.integer :target_id, index: true + t.string :title + t.text :data + t.integer :project_id + t.datetime :created_at, index: true + t.datetime :updated_at + t.integer :action, index: true + t.integer :author_id, index: true + + t.index [:project_id, :id] + end + + Event.all.each_batch do |relation| + start_id, stop_id = relation.pluck('MIN(id), MAX(id)').first + + execute <<-EOF.strip_heredoc + INSERT INTO events_for_migration (target_type, target_id, project_id, created_at, updated_at, action, author_id) + SELECT target_type, target_id, project_id, created_at, updated_at, action, author_id + FROM events + WHERE id BETWEEN #{start_id} AND #{stop_id} + EOF + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1c1a5e63bc4..2149f5ad23d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -32,8 +32,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.text "description", null: false t.string "header_logo" t.string "logo" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false t.text "description_html" t.integer "cached_markdown_version" end @@ -101,6 +101,10 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.integer "rsa_key_restriction", default: 0, null: false + t.integer "dsa_key_restriction", default: 0, null: false + t.integer "ecdsa_key_restriction", default: 0, null: false + t.integer "ed25519_key_restriction", default: 0, null: false t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false @@ -125,14 +129,11 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.boolean "prometheus_metrics_enabled", default: false, null: false t.boolean "help_page_hide_commercial_content", default: false t.string "help_page_support_url" - t.integer "performance_bar_allowed_group_id" t.boolean "password_authentication_enabled" - t.boolean "project_export_enabled", default: true, null: false + t.integer "performance_bar_allowed_group_id" t.boolean "hashed_storage_enabled", default: false, null: false - t.integer "rsa_key_restriction", default: 0, null: false - t.integer "dsa_key_restriction", default: 0, null: false - t.integer "ecdsa_key_restriction", default: 0, null: false - t.integer "ed25519_key_restriction", default: 0, null: false + t.boolean "project_export_enabled", default: true, null: false + t.boolean "auto_devops_enabled", default: false, null: false end create_table "audit_events", force: :cascade do |t| @@ -255,6 +256,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree + add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree, opclasses: {"id)"=>"WHERE"} add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree @@ -273,8 +275,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.string "encrypted_value_iv" t.integer "group_id", null: false t.boolean "protected", default: false, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false end add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree @@ -286,8 +288,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.string "encrypted_value_salt" t.string "encrypted_value_iv" t.integer "pipeline_schedule_id", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime_with_timezone "created_at" + t.datetime_with_timezone "updated_at" end add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree @@ -339,6 +341,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "auto_canceled_by_id" t.integer "pipeline_schedule_id" t.integer "source" + t.integer "config_source" t.boolean "protected" end @@ -531,38 +534,19 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree create_table "events", force: :cascade do |t| - t.string "target_type" - t.integer "target_id" - t.string "title" - t.text "data" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "action" - t.integer "author_id" - end - - add_index "events", ["action"], name: "index_events_on_action", using: :btree - add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree - add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree - add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree - add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree - add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree - - create_table "events_for_migration", force: :cascade do |t| t.integer "project_id" t.integer "author_id", null: false t.integer "target_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false t.integer "action", limit: 2, null: false t.string "target_type" end - add_index "events_for_migration", ["action"], name: "index_events_for_migration_on_action", using: :btree - add_index "events_for_migration", ["author_id"], name: "index_events_for_migration_on_author_id", using: :btree - add_index "events_for_migration", ["project_id", "id"], name: "index_events_for_migration_on_project_id_and_id", using: :btree - add_index "events_for_migration", ["target_type", "target_id"], name: "index_events_for_migration_on_target_type_and_target_id", using: :btree + add_index "events", ["action"], name: "index_events_on_action", using: :btree + add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree + add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree + add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree create_table "feature_gates", force: :cascade do |t| t.string "feature_key", null: false @@ -592,8 +576,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree create_table "gpg_keys", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false t.integer "user_id" t.binary "primary_keyid" t.binary "fingerprint" @@ -605,8 +589,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree create_table "gpg_signatures", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false t.integer "project_id" t.integer "gpg_key_id" t.binary "commit_sha" @@ -804,8 +788,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree create_table "merge_request_diff_commits", id: false, force: :cascade do |t| - t.datetime "authored_date" - t.datetime "committed_date" + t.datetime_with_timezone "authored_date" + t.datetime_with_timezone "committed_date" t.integer "merge_request_diff_id", null: false t.integer "relative_order", null: false t.binary "sha", null: false @@ -1126,6 +1110,16 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree + create_table "project_auto_devops", force: :cascade do |t| + t.integer "project_id", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.boolean "enabled" + t.string "domain" + end + + add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree + create_table "project_features", force: :cascade do |t| t.integer "project_id" t.integer "merge_requests_access_level" @@ -1209,6 +1203,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.string "repository_storage", default: "default", null: false t.boolean "request_access_enabled", default: false, null: false t.boolean "has_external_wiki" + t.string "ci_config_path" t.boolean "lfs_enabled" t.text "description_html" t.boolean "only_allow_merge_if_all_discussions_are_resolved" @@ -1216,9 +1211,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "auto_cancel_pending_pipelines", default: 1, null: false t.string "import_jid" t.integer "cached_markdown_version" - t.datetime "last_repository_updated_at" - t.string "ci_config_path" t.text "delete_error" + t.datetime "last_repository_updated_at" t.integer "storage_version", limit: 2 t.boolean "resolve_outdated_diff_discussions" end @@ -1614,6 +1608,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.boolean "notified_of_own_activity" t.string "preferred_language" t.string "rss_token" + t.integer "theme_id", limit: 2 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -1707,9 +1702,8 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade - add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade - add_foreign_key "events_for_migration", "projects", on_delete: :cascade - add_foreign_key "events_for_migration", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade + add_foreign_key "events", "projects", on_delete: :cascade + add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "gpg_keys", "users", on_delete: :cascade add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify @@ -1718,7 +1712,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade - add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade + add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade @@ -1742,6 +1736,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_foreign_key "personal_access_tokens", "users" add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade + add_foreign_key "project_auto_devops", "projects", on_delete: :cascade add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade @@ -1753,7 +1748,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_foreign_key "protected_tag_create_access_levels", "protected_tags", name: "fk_f7dfda8c51", on_delete: :cascade add_foreign_key "protected_tag_create_access_levels", "users" add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade - add_foreign_key "push_event_payloads", "events_for_migration", column: "event_id", name: "fk_36c74129da", on_delete: :cascade + add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index b250fa08382..b3e7c9bd0bf 100644 --- a/doc/README.md +++ b/doc/README.md @@ -24,7 +24,7 @@ plus premium features available in each version: **Enterprise Edition Starter** Shortcuts to GitLab's most visited docs: -| [GitLab CI](ci/README.md) | Other | +| [GitLab CI/CD](ci/README.md) | Other | | :----- | :----- | | [Quick start guide](ci/quick_start/README.md) | [API](api/README.md) | | [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) | @@ -41,6 +41,7 @@ Shortcuts to GitLab's most visited docs: - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/). - [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown). - [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI. +- [Auto DevOps](topics/autodevops/index.md) ### User account @@ -84,7 +85,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i - [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests. - [Issues](user/project/issues/index.md) -- [Issue Board](user/project/issue_board.md) +- [Project issue Board](user/project/issue_board.md) - [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests. - [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles. - [Merge Requests](user/project/merge_requests/index.md) diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index b21817c1fd3..652ca9cf454 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -71,6 +71,15 @@ And in Markdown using fenced code blocks: Alice -> Bob : Go Away ``` +And in reStructuredText using a directive: + +``` +.. plantuml:: + + Bob -> Alice: hello + Alice -> Bob: Go Away +``` + The above blocks will be converted to an HTML img tag with source pointing to the PlantUML instance. If the PlantUML server is correctly configured, this should render a nice diagram instead of the block: @@ -94,4 +103,4 @@ Some parameters can be added to the AsciiDoc block definition: Markdown does not support any parameters and will always use PNG format. -[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537
\ No newline at end of file +[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537 diff --git a/doc/api/keys.md b/doc/api/keys.md index 376ac27df3a..ddcf7830621 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -32,6 +32,7 @@ Parameters: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 2, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": null, diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 8133251dffe..5c0bebbaeb0 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -28,12 +28,14 @@ Example response: [ { "id": 1, + "name": "user1", "path": "user1", "kind": "user", "full_path": "user1" }, { "id": 2, + "name": "group1", "path": "group1", "kind": "group", "full_path": "group1", @@ -42,6 +44,7 @@ Example response: }, { "id": 3, + "name": "bar", "path": "bar", "kind": "group", "full_path": "foo/bar", @@ -77,6 +80,7 @@ Example response: [ { "id": 4, + "name": "twitter", "path": "twitter", "kind": "group", "full_path": "twitter", diff --git a/doc/api/session.md b/doc/api/session.md index f79eac11689..b97e26f34a2 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -39,6 +39,7 @@ Example response: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 1, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": "2015-07-07T07:10:58.392Z", diff --git a/doc/api/users.md b/doc/api/users.md index 9f3e4caf2f4..6d5db16b36a 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -72,6 +72,7 @@ GET /users "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -105,6 +106,7 @@ GET /users "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 3, "projects_limit": 100, @@ -215,6 +217,7 @@ Parameters: "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -341,6 +344,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -387,6 +391,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, diff --git a/doc/ci/README.md b/doc/ci/README.md index 1bf10e34ae7..5cfd82de381 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -44,6 +44,10 @@ digging into specific reference guides. - [User permissions](../user/permissions.md#gitlab-ci) - [Jobs permissions](../user/permissions.md#jobs-permissions) +## Auto DevOps + +- [Auto DevOps](../topics/autodevops/index.md) + ## GitLab CI + Docker Leverage the power of Docker to run your CI pipelines. diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index a128cf69c20..474cb28b9e4 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -1,13 +1,16 @@ # Auto Deploy ->**Notes:** -- [Introduced][mr-8135] in GitLab 8.15. -- Auto deploy is an experimental feature and is not recommended for Production - use at this time. -- As of GitLab 9.1, access to the Container Registry is only available while - the Pipeline is running. Restarting a pod, scaling a service, or other actions - which require on-going access will fail. On-going secure access is planned for - a subsequent release. +> [Introduced][mr-8135] in GitLab 8.15. +> Auto deploy is an experimental feature and is **not recommended for Production use** at this time. + +> As of GitLab 9.1, access to the container registry is only available while the +Pipeline is running. Restarting a pod, scaling a service, or other actions which +require on-going access **will fail**. On-going secure access is planned for a +subsequent release. + +> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the +functionality has been included in [Auto +DevOps](../../topics/autodevops/index.md). Auto deploy is an easy way to configure GitLab CI for the deployment of your application. GitLab Community maintains a list of `.gitlab-ci.yml` @@ -122,4 +125,3 @@ If you have installed GitLab using a different method: [kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project" [container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html [postgresql]: https://www.postgresql.org/ - diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 6e8beceb6fe..fa823ea4721 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -96,7 +96,7 @@ services: - tutum/wordpress:latest ``` -If you don't [specify a service alias](#available-settings-for-services-entry), +If you don't [specify a service alias](#available-settings-for-services), when the job is run, `tutum/wordpress` will be started and you will have access to it from your build container under two hostnames to choose from: diff --git a/doc/ci/environments.md b/doc/ci/environments.md index cbf06afa294..c1362b7bd5b 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -446,8 +446,7 @@ and/or `production`) you can see this information in the merge request itself. ![Environment URLs in merge request](img/environments_link_url_mr.png) -### <a name="route-map"></a>Go directly from source files to public pages on the environment - +### Go directly from source files to public pages on the environment > Introduced in GitLab 8.17. diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index f5d3b524d6e..bac8e972754 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -228,7 +228,8 @@ To make a Runner pick tagged/untagged jobs: ### Be careful with sensitive information -If you can run a job on a Runner, you can get access to any code it runs +With some [Runner Executors](https://docs.gitlab.com/runner/executors/README.html), +if you can run a job on the Runner, you can get access to any code it runs and get the token of the Runner. With shared Runners, this means that anyone that runs jobs on the Runner, can access anyone else's code that runs on the Runner. @@ -237,7 +238,8 @@ In addition, because you can get access to the Runner token, it is possible to create a clone of a Runner and submit false jobs, for example. The above is easily avoided by restricting the usage of shared Runners -on large public GitLab instances and controlling access to your GitLab instance. +on large public GitLab instances, controlling access to your GitLab instance, +and using more secure [Runner Executors](https://docs.gitlab.com/runner/executors/README.html). ### Forks diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 4f20aa070de..c8d23609280 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -311,6 +311,7 @@ A forEach will cause side effects, it will be mutating the array being iterated. #### Alignment 1. Follow these alignment styles for the template method: + 1. With more than one attribute, all attributes should be on a new line: ```javascript // bad <component v-if="bar" @@ -327,9 +328,16 @@ A forEach will cause side effects, it will be mutating the array being iterated. <button class="btn"> Click me </button> + ``` + 1. The tag can be inline if there is only one attribute: + ```javascript + // good + <component bar="bar" /> - // if props fit in one line then keep it on the same line - <component bar="bar" /> + // good + <component + bar="bar" + /> ``` #### Quotes @@ -381,9 +389,12 @@ A forEach will cause side effects, it will be mutating the array being iterated. } ``` -1. Default key should always be provided if the prop is not required: +1. Default key should be provided if the prop is not required. +_Note:_ There are some scenarios where we need to check for the existence of the property. +On those a default key should not be provided. + ```javascript - // bad + // good props: { foo: { type: String, @@ -512,11 +523,11 @@ A forEach will cause side effects, it will be mutating the array being iterated. ``` ### The Javascript/Vue Accord -The goal of this accord is to make sure we are all on the same page. +The goal of this accord is to make sure we are all on the same page. -1. When writing Vue, you may not use jQuery in your application. +1. When writing Vue, you may not use jQuery in your application. 1. If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery. - 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html). + 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html). 1. If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners. 1. We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit). 1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application. diff --git a/doc/development/img/manual_build_docs.png b/doc/development/img/manual_build_docs.png Binary files differnew file mode 100644 index 00000000000..fef767c2a79 --- /dev/null +++ b/doc/development/img/manual_build_docs.png diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index eac9ec2a470..479258f743e 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -103,3 +103,24 @@ If that job fails, read the instructions in the job log for what to do next. Contributors do not need to submit their changes to EE, GitLab Inc. employees on the other hand need to make sure that their changes apply cleanly to both CE and EE. + +## Previewing the changes live + +If you want to preview your changes live, you can use the manual `build-docs` +job in your merge request. + +![Manual trigger a docs build](img/manual_build_docs.png) + +This job will: + +1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs) + project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee` +1. Trigger a pipeline and build the docs site with your changes + +Look for the docs URL at the output of the `build-docs` job. + +>**Note:** +Make sure that you always delete the branch of the merge request you were +working on. If you don't, the remote docs branch won't be removed either, +and the server where the Review Apps are hosted will eventually be out of +disk space. diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index bc75dc1447e..5c128f54a76 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -75,7 +75,7 @@ log_bin_trust_function_creators=1 ### MySQL utf8mb4 support -After installation or upgrade, remember to [convert any new tables](#convert) to `utf8mb4`/`utf8mb4_general_ci`. +After installation or upgrade, remember to [convert any new tables](#tables-and-data-conversion-to-utf8mb4) to `utf8mb4`/`utf8mb4_general_ci`. --- @@ -230,7 +230,6 @@ We need to check, enable and probably convert your existing GitLab DB tables to > Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file. #### Tables and data conversion to utf8mb4 -<a name="convert"></a> Now that you have a persistent MySQL setup, you can safely upgrade tables after setup or upgrade time: diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index a339bc23809..177124c8291 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,9 +1,9 @@ # GitLab Helm Chart -> **Note:** -* > **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). -* Officially supported cloud providers are Google Container Service and Azure Container Service. +> **Note**: +* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). +* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). -The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. For most deployments we recommended the [gitlab-omnibus](gitlab_omnibus.md) chart, +The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment. This chart includes the following: @@ -15,9 +15,11 @@ This chart includes the following: - Optional PostgreSQL deployment using the [PostgreSQL Chart](https://github.com/kubernetes/charts/tree/master/stable/postgresql) (defaults to enabled) - Optional Ingress (defaults to disabled) +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). + ## Prerequisites -- _At least_ 3 GB of RAM available on your cluster, in chunks of 1 GB. 41GB of storage and 2 CPU are also required. +- _At least_ 3 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required. - Kubernetes 1.4+ with Beta APIs enabled - [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure - The ability to point a DNS entry or URL at your GitLab install diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index d7fd8613633..9d1280c3dc6 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -2,13 +2,15 @@ > **Note:** * This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on. * GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these. -* Officially supported cloud providers are Google Container Service and Azure Container Service. +* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). + ## Introduction -This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned as well via [Let's Encrypt](https://letsencrypt.org/). +This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/). The deployment includes: @@ -19,7 +21,13 @@ The deployment includes: - [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) - Persistent Volume Claims for Data, Registry, Postgres, and Redis -A video demonstration of GitLab utilizing this chart [is available](https://about.gitlab.com/handbook/sales/demo/). +### Limitations + +* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported. +* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available. +* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018. + +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). ## Prerequisites @@ -46,7 +54,7 @@ Finally, set the `baseIP` setting to this IP address when [deploying GitLab](#co #### Load Balancer IP -If you do not specify a `baseIP`, an ephemeral IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab: +If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab: `kubectl get svc -w --namespace nginx-ingress nginx` diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index d31c763ed64..5e0d7493b61 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,6 +1,6 @@ # GitLab Runner Helm Chart > **Note:** -Officially supported cloud providers are Google Container Service and Azure Container Service. +These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your Kubernetes cluster. @@ -11,6 +11,8 @@ This chart configures the Runner to: - For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a new pod within the specified namespace to run it. +For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). + ## Prerequisites - Your GitLab Server's API is reachable from the cluster diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index fb6c0c2d263..c799f88ad74 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -1,56 +1,59 @@ # Installing GitLab on Kubernetes -> Officially supported cloud providers are Google Container Service and Azure Container Service. +> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). -The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is +The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is to take advantage of GitLab's Helm charts. [Helm] is a package management tool for Kubernetes, allowing apps to be easily managed via their Charts. A [Chart] is a detailed description of the application including how it should be deployed, upgraded, and configured. -GitLab provides [official Helm Charts](#official-gitlab-helm-charts-recommended) which are the recommended way to run GitLab within Kubernetes. +## Chart Overview -There are also two other sets of charts: -* Our [upcoming cloud native Charts](#upcoming-cloud-native-helm-charts), which are in development but will eventually replace the current official charts. -* [Community contributed charts](#community-contributed-helm-charts). These charts should be considered deprecated, in favor of the official charts. +* **[GitLab-Omnibus](#gitlab-omnibus-chart-recommended)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added. +* **[Upcoming Cloud Native Charts](#upcoming-cloud-native-helm-charts)**: The next generation of charts, currently in development. Will support large deployments, with horizontal scaling of individual GitLab components. +* Other Charts + * [GitLab Runner Chart](#gitlab-runner-chart): For deploying just the GitLab Runner. + * [Advanced GitLab Installation](#advanced-gitlab-installation): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options. + * [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab charts. -## Official GitLab Helm Charts +## GitLab-Omnibus Chart (Recommended) +> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added. -These charts utilize our [GitLab Omnibus Docker images](https://docs.gitlab.com/omnibus/docker/README.html). You can report any issues and feedback related to these charts at -https://gitlab.com/charts/charts.gitlab.io/issues. +This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html). -### Deploying GitLab on Kubernetes -> **Note**: This chart will eventually be replaced by the [cloud native charts](#upcoming-cloud-native-helm-charts), which are presently in development. +Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. -The best way to deploy GitLab on Kubernetes is to use the [gitlab-omnibus](gitlab_omnibus.md) chart. +Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md) -It includes everything needed to run GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and an [Ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being completed. +## Upcoming Cloud Native Charts -### Deploying just the GitLab Runner +GitLab is working towards building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended). -To deploy just the [GitLab Runner](https://docs.gitlab.com/runner/), utilize the [gitlab-runner](gitlab_runner_chart.md) chart. +By offering individual containers and charts, we will be able to provide a number of benefits: +* Easier horizontal scaling of each service, +* Smaller, more efficient images, +* Potential for rolling updates and canaries within a service, +* and plenty more. -It offers a quick way to configure and deploy the Runner on Kubernetes, regardless of where your GitLab server may be running. +This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018. -### Advanced deployment of GitLab -> **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). +## Other Charts -If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the GitLab service along with optional Postgres and Redis. It offers extensive configuration, but requires deep knowledge of Kubernetes and Helm to use. +### GitLab Runner Chart -For most deployments we recommend using our [gitlab-omnibus](gitlab_omnibus.md) chart. +If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/executors/kubernetes.md), it can be deployed with the GitLab Runner chart. -## Upcoming Cloud Native Helm Charts +Learn more about [gitlab-runner chart.](gitlab_runner_chart.md) -GitLab is working towards a building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into it's [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended). +### Advanced GitLab Installation -By offering individual containers and charts, we will be able to provide a number of benefits: -* Easier horizontal scaling of each service -* Smaller more efficient images -* Potential for rolling updates and canaries within a service -* and plenty more. +If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the core GitLab service along with optional Postgres and Redis. It offers extensive configuration, but offers limited functionality out-of-the-box; it's lacking Pages support, the container registry, and Mattermost. It requires deep knowledge of Kubernetes and Helm to use. + +This chart will be deprecated and replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). It's beta quality, and since it is not actively under development, it will never be GA. -This is a large project and will be worked on over the span of multiple releases. For the most up to date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). +Learn more about the [gitlab chart.](gitlab_chart.md) -## Community Contributed Helm Charts +### Community Contributed Charts The community has also [contributed GitLab charts](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ce) to the [Helm Stable Repository](https://github.com/kubernetes/charts#repository-structure). These charts should be considered [deprecated](https://github.com/kubernetes/charts/issues/1138) in favor of the [official Charts](#official-gitlab-helm-charts-recommended). diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png Binary files differnew file mode 100644 index 00000000000..57bd7650a30 --- /dev/null +++ b/doc/topics/autodevops/img/auto_devops_settings.png diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png Binary files differnew file mode 100644 index 00000000000..5661b50841b --- /dev/null +++ b/doc/topics/autodevops/img/auto_monitoring.png diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png Binary files differnew file mode 100644 index 00000000000..b856b81a1d0 --- /dev/null +++ b/doc/topics/autodevops/img/guide_connect_cluster.png diff --git a/doc/topics/autodevops/img/guide_integration.png b/doc/topics/autodevops/img/guide_integration.png Binary files differnew file mode 100644 index 00000000000..723b2619ea2 --- /dev/null +++ b/doc/topics/autodevops/img/guide_integration.png diff --git a/doc/topics/autodevops/img/guide_secret.png b/doc/topics/autodevops/img/guide_secret.png Binary files differnew file mode 100644 index 00000000000..01f5aa49908 --- /dev/null +++ b/doc/topics/autodevops/img/guide_secret.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md new file mode 100644 index 00000000000..babf44d2665 --- /dev/null +++ b/doc/topics/autodevops/index.md @@ -0,0 +1,342 @@ +# Auto DevOps + +> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and +**not recommended for production use**. Access to the Container Registry is only +available while the pipeline is running. Restarting a pod, scaling a service, or +other actions which require on-going access **will fail** even for public +projects. On-going secure access is planned for a subsequent release. + +Auto DevOps brings best practices to your project in an easy and default way. A +typical web project starts with Continuous Integration (CI), then adds automated +deployment to production, and maybe some time in the future adds some kind of +monitoring. With Auto DevOps, every project has a complete workflow, with +no configuration, including: + +- [Auto Build](#auto-build) +- [Auto Test](#auto-test) +- [Auto Code Quality](#auto-code-quality) +- [Auto Review Apps](#auto-review-apps) +- [Auto Deploy](#auto-deploy) +- [Auto Monitoring](#-auto-monitoring) + +## Overview + +You will need [Kubernetes](https://kubernetes.io/) and +[Prometheus](https://prometheus.io/) to make full use of Auto DevOps, but +even projects using only [GitLab Runners](https://docs.gitlab.com/runner/) will +be able to make use of Auto Build, Auto Test, and Auto Code Quality. + +Auto DevOps makes use of an open source tool called +[Herokuish](https://github.com/gliderlabs/herokuish) which uses [Heroku +buildpacks](https://devcenter.heroku.com/articles/buildpacks) to automatically +detect, build, and test applications. Auto DevOps supports all of the languages +and frameworks that are [supported by +Herokuish](https://github.com/gliderlabs/herokuish#buildpacks) such as Ruby, +Rails, Node, PHP, Python, and Java, and [custom buildpacks can be +specified](#using-custom-buildpacks). *GitLab is in no way affiliated with Heroku +or Glider Labs.* + +Projects can [customize](#customizing) the process by specifying [custom +buildpacks](#custom-buildpack), [custom `Dockerfile`s](#custom-dockerfile), +[custom Helm charts](#custom-helm-chart), or even copying the complete CI/CD +configuration into your project to enable staging and canary deployments, and +more. + +## Quick start + +If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) +for using Auto DevOps with GitLab.com and an external Kubernetes cluster on +Google Cloud. + +For self-hosted installations, the easiest way to make use of Auto DevOps is to +install GitLab inside a Kubernetes cluster using the [GitLab-Omnibus Helm +Chart](../../install/kubernetes/gitlab_omnibus.md) which automatically installs +and configures everything you need. + +## Prerequisites + +You will need one or more GitLab Runners, a Kubernetes cluster, and Prometheus +installed in the cluster to make full use of Auto DevOps. If you do not have +Kubernetes or Prometheus installed then Auto Review Apps, Auto Deploy, and Auto +Monitoring will be silently skipped. + +If you are using GitLab outside of Kubernetes, for example with GitLab.com, then +you should take these prerequisites into account: + +1. **Base domain** - You will need a base domain configured with wildcard DNS to + be used by all of your Auto DevOps applications. + +1. **GitLab Runner** - Your Runner needs to be configured to be able to run Docker. + Generally this means using the + [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes + executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with + [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode). + The Runners do not need to be installed in the Kubernetes cluster, but the + Kubernetes executor is easy to use and is automatically autoscaling. + Docker-based Runners can be configured to autoscale as well, using [Docker + Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners + should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner) + for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner) + that are assigned to specific projects. + +1. **Kubernetes** - To enable deploys, you will need Kubernetes 1.5+, with NGINX + ingress and wildcard SSL termination, for example using the + [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) + and [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego) + Helm charts respectively. The [Kubernetes service][kubernetes-service] + integration will need to be enabled for the project, or enabled as a + [default service template](../../user/project/integrations/services_templates.md) + for the entire GitLab installation. + +1. **Prometheus** - To enable Auto Monitoring, you will need Prometheus installed + somewhere (inside or outside your cluster) and configured to scrape your + Kubernetes cluster. To get response metrics (in addition to system metrics), + you need to [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics). + The [Prometheus service](../../user/project/integrations/prometheus.md) + integration needs to be enabled for the project, or enabled as a + [default service template](../../user/project/integrations/services_templates.md) + for the entire GitLab installation. + +## Enabling Auto DevOps + +In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps +section. Select "Enable Auto DevOps", add in your base domain, and save. + +![auto devops settings](img/auto_devops_settings.png) + +## Stages of Auto DevOps + +The following sections describe the stages of Auto DevOps. + +### Auto Build + +Auto Build creates a build of the application in one of two ways: + +- If there is a `Dockerfile`, it will use `docker build` to create a Docker image. +- Otherwise, it will use [Herokuish](https://github.com/gliderlabs/herokuish) + and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) + to automatically detect and build the application into a Docker image. + +Either way, the resulting Docker image is automatically pushed to the +[Container Registry][container-registry], tagged with the commit SHA. + +### Auto Test + +Auto Test automatically tests your application using +[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku +buildpacks](https://devcenter.heroku.com/articles/buildpacks). Auto Test will +analyze your project to detect the language and framework, and run appropriate +tests. Several languages and frameworks are detected automatically, but if your +language is not detected, you may succeed with a [custom +buildpack](#custom-buildpack). + +Auto Test uses tests you already have in your application. If there are no +tests, it's up to you to add them. + +### Auto Code Quality + +Auto Code Quality uses the open source +[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run +static analysis and other code checks on the current code, creating a report +that is uploaded as an artifact. In GitLab EE, differences between the source +and target branches are shown in the merge request widget. *GitLab is in no way +affiliated with Code Climate.* + +### Auto Review Apps + +Auto Review Apps create a [Review App][review-app] for each branch. Review Apps +are temporary application environments based on the branch's code so developers, +designers, QA, product managers, and other reviewers can actually see and +interact with code changes as part of the review process. + +The review app will have a unique URL based on the project name, the branch +name, and a unique number, combined with the Auto DevOps base domain. For +example, `user-project-branch-1234.example.com`. A link to the Review App shows +up in the merge request widget for easy discovery. When the branch is deleted, +for example after the merge request is merged, the Review App will automatically +be deleted. + +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the Kubernetes service is not configured, or if the variable +`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto +DevOps setting), the job will silently be skipped. + +### Auto Deploy + +After a branch or merge request is merged into `master`, Auto Deploy deploys the +application to a `production` environment in the Kubernetes cluster, with a +namespace based on the project name and unique project ID. e.g. `project-4321`. +This is an optional step, since many projects do not have a Kubernetes cluster +available. If the Kubernetes service is not configured, or if the variable +`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto +DevOps setting), the job will silently be skipped. + +Auto Deploy doesn't include deployments to staging or canary by default, but the +Auto DevOps template contains job definitions for these tasks if you want to +enable them. + +### Auto Monitoring + +Once your application is deployed, Auto Monitoring makes it possible to monitor +your application's server and response metrics right out of the box. Auto +Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to +get system metrics such as CPU and memory usage directly from +[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md), +and response metrics such as HTTP error rates, latency, and throughput from the +[NGINX +server](../../user/project/integrations/prometheus_library/nginx_ingress.md). + +* Response Metrics: latency, throughput, error rate +* System Metrics: CPU utilization, memory utilization + +To view the metrics, open the [Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments). + +![Auto Metrics](img/auto_monitoring.png) + +### Configuring Auto Monitoring + +If GitLab has been deployed using the +[omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no +configuration is required. + +If you have installed GitLab using a different method: + +1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster +1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml). +1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. + +## Customizing + +### PostgreSQL Database Support + +In order to support applications that require a database, +[PostgreSQL][postgresql] is provisioned by default. Credentials to access the +database are preconfigured, but can be customized by setting the associated +[variables](#postgresql-variables). These credentials can be used for defining a +`DATABASE_URL` of the format: +`postgres://user:password@postgres-host:postgres-port/postgres-database`. + +PostgreSQL provisioning can be disabled by creating a project variable +`POSTGRES_ENABLED` set to `false`. + +#### PostgreSQL Variables + +Any variables set at the project or group level will override variables set in +the CI/CD configuration. + +1. `POSTGRES_ENABLED: "false"`: disable automatic deployment of PostgreSQL +1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL +1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL +1. `POSTGRES_DB: "my-database"`: use custom database name for PostgreSQL + +### Custom buildpack + +If the automatic buildpack detection fails for your project, or if you want to +use a custom buildpack, you can override the buildpack using a project variable +or a `.buildpack` file in your project: + +- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL + of the buildpack to use. + +- **`.buildpack` file** - Add a file in your project's repo called `.buildpack` + and add the URL of the buildpack to use on a line in the file. If you want to + use multiple buildpacks, you can enter them in, one on each line + + >**Note:** Using multiple buildpacks may break Auto Test. + +### Custom `Dockerfile` + +If your project has a `Dockerfile` in the root of the project repo, Auto DevOps +will build a Docker image based on the Dockerfile rather than using buildpacks. +This can be much faster and result in smaller images, especially if your +Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/). + +### Custom Helm Chart + +Auto DevOps uses Helm to deploy your application to Kubernetes. You can override +the Helm chart used by bundling up a chart into your project repo or by +specifying a project variable. + +**Bundled chart** - If your project has a `chart` directory with a `Chart.yaml` +file in it, Auto DevOps will detect the chart and use it instead of the default +chart. This can be a great way to control exactly how your application is +deployed. + +**Project variable** - Create a project variable `AUTO_DEVOPS_CHART` with the +URL of a custom chart to use. + +### Enable staging, canaries, and more with custom `.gitlab-ci.yml` + +If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the +Auto DevOps template into your project's repo and edit as you see fit. + +From your project home page, click on the `Set up CI` button, or click on the `+` +button and `New file` and pick `.gitlab-ci.yml` as the template type, or view an +existing `.gitlab-ci.yml` file. Then select "Auto DevOps" from the template +dropdown. You will then be able to edit or add any jobs needed. + +For example, if you want deploys to go to a staging environment instead of +directly to a production environment, you can enable the `staging` job by +renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of +the `production` job to turn it into a manual action instead of deploying +automatically. + +## Currently supported languages + +>**Note:** +Not all buildpacks support Auto Test yet, as it's a relatively new +enhancement. All of Heroku's [officially supported +languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages) +support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP, +Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the +multi-buildpack does not. + +As of GitLab 10.0, the supported buildpacks are: + +``` +* heroku-buildpack-multi v1.0.0 +* heroku-buildpack-ruby v168 +* heroku-buildpack-nodejs v99 +* heroku-buildpack-clojure v77 +* heroku-buildpack-python v99 +* heroku-buildpack-java v53 +* heroku-buildpack-gradle v23 +* heroku-buildpack-scala v78 +* heroku-buildpack-play v26 +* heroku-buildpack-php v122 +* heroku-buildpack-go v72 +* heroku-buildpack-erlang fa17af9 +* buildpack-nginx v8 +``` + +## Private Project Support - Experimental + +When a project has been marked as private, GitLab's [Container +Registry][container-registry] requires authentication when downloading +containers. Auto DevOps will automatically provide the required authentication +information to Kubernetes, allowing temporary access to the registry. +Authentication credentials will be valid while the pipeline is running, allowing +for a successful initial deployment. + +After the pipeline completes, Kubernetes will no longer be able to access the +container registry. **Restarting a pod, scaling a service, or other actions which +require on-going access to the registry will fail**. On-going secure access is +planned for a subsequent release. + +## Troubleshooting + +- Auto Build and Auto Test may fail in detecting your language/framework. There + may be no buildpack for your application, or your application may be missing the + key files the buildpack is looking for. For example, for ruby apps, you must + have a `Gemfile` to be properly detected, even though it is possible to write a + Ruby app without a `Gemfile`. Try specifying a [custom + buildpack](#custom-buildpack). +- Auto Test may fail because of a mismatch between testing frameworks. In this + case, you may need to customize your `.gitlab-ci.yml` with your test commands. + +[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 +[kubernetes-service]: ../../user/project/integrations/kubernetes.md +[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../../ci/review_apps/index.md +[container-registry]: ../../user/project/container_registry.md +[postgresql]: https://www.postgresql.org/ diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md new file mode 100644 index 00000000000..f23c9d794b4 --- /dev/null +++ b/doc/topics/autodevops/quick_start_guide.md @@ -0,0 +1,95 @@ +# Auto DevOps: quick start guide + +> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and +**not recommended for production use**. + +This is a step-by-step guide to deploying a project hosted on GitLab.com to +Google Cloud, using Auto DevOps. + +We made a minimal [Ruby +application](https://gitlab.com/gitlab-examples/minimal-ruby-app) to use as an +example for this guide. It contains two files: + +* `server.rb` - our application. It will start an HTTP server on port 5000 and + render "Hello, world!" +* `Dockerfile` - to build our app into a container image. It will use a ruby + base image and run `server.rb` + +## Fork sample project on GitLab.com + +Let’s start by forking our sample application. Go to [the project +page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` +button. Soon you should have a project under your namespace with the necessary +files. + +## Setup your own cluster on Google Container Engine + +If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. + +Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. + +## Connect to Kubernetes cluster + +You need to have the Google Cloud SDK installed. e.g. +On OSX, install [homebrew](https://brew.sh): + +1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask` +2. Install Google Cloud SDK: `brew cask install google-cloud-sdk` +3. Add `kubectl`: `gcloud components install kubectl` +4. Log in: `gcloud auth login` + +Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`. + +![connect to cluster](img/guide_connect_cluster.png) + +## Copy credentials to GitLab.com project + +Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token. + +![connect to cluster](img/guide_secret.png) + +You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you. + +![connect to cluster](img/guide_integration.png) + +For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform. + +## Expose application to the world + +In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address. + +### Set up Ingress controller + +You’ll need to make sure you have an ingress controller. If you don’t have one, do: + +```sh +brew install kubernetes-helm +helm init +helm install --name ruby-app stable/nginx-ingress +``` + +This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that. + +### Point DNS at Cluster IP + +Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running: + +```sh +kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' +``` + +Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>`. + +Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP. + +## Set up Auto DevOps + +In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save. + +![auto devops settings](img/auto_devops_settings.png) + +Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab. + +Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!” + +[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 diff --git a/doc/topics/index.md b/doc/topics/index.md index ad388dff822..b51f24b02e4 100644 --- a/doc/topics/index.md +++ b/doc/topics/index.md @@ -7,6 +7,7 @@ you through better understanding GitLab's concepts through our regular docs, and, when available, through articles (guides, tutorials, technical overviews, blog posts) and videos. +- [Auto DevOps](autodevops/index.md) - [Authentication](authentication/index.md) - [Continuous Integration (GitLab CI)](../ci/README.md) - [Git](git/index.md) diff --git a/doc/user/group/img/share_with_group_lock.png b/doc/user/group/img/share_with_group_lock.png Binary files differindex 8df41bf9465..c0f25389eaf 100644 --- a/doc/user/group/img/share_with_group_lock.png +++ b/doc/user/group/img/share_with_group_lock.png diff --git a/doc/user/group/index.md b/doc/user/group/index.md index fbc05261a32..db0242f1324 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -168,8 +168,7 @@ GitLab administrators can use the admin interface to move any project to any nam You can [share your projects with a group](../project/members/share_project_with_groups.md) and give your group members access to the project all at once. -Alternatively, with [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/), -you can [lock the sharing with group feature](#share-with-group-lock-ees-eep). +Alternatively, you can [lock the sharing with group feature](#share-with-group-lock). ## Manage group memberships via LDAP @@ -191,10 +190,28 @@ access further configurations for your group. #### Enforce 2FA to group members -Add a secury layer to your group by +Add a security layer to your group by [enforcing two-factor authentication (2FA)](../../security/two_factor_authentication.md#enforcing-2fa-for-all-users-in-a-group) to all group members. +#### Share with group lock + +Prevent projects in a group from [sharing +a project with another group](../project/members/share_project_with_groups.md). +This allows for tighter control over project access. + +For example, consider you have two distinct teams (Group A and Group B) +working together in a project. +To inherit the group membership, you share the project between the +two groups A and B. **Share with group lock** prevents any project within +the group from being shared with another group. By doing so, you +guarantee only the right group members have access to that projects. + +To enable this feature, navigate to the group settings page. Select +**Share with group lock** and **Save the group**. + +![Checkbox for share with group lock](img/share_with_group_lock.png) + #### Member Lock (EES/EEP) Available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/), @@ -203,15 +220,6 @@ level of members in group. Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#member-lock-ees-eep). -#### Share with group lock (EES/EEP) - -In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/) -it is possible to prevent projects in a group from [sharing -a project with another group](../project/members/share_project_with_groups.md). -This allows for tighter control over project access. - -Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep). - ### Advanced settings - **Projects**: view all projects within that group, add members to each project, diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 629d69d8aea..5c615daf464 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -120,6 +120,11 @@ If a project is private, credentials will need to be provided for authorization. The preferred way to do this, is by using [personal access tokens][pat]. The minimal scope needed is `read_registry`. +Example of using a personal access token: +``` +docker login registry.example.com -u <your_username> -p <your_personal_access_token> +``` + ## Troubleshooting the GitLab Container Registry ### Basic Troubleshooting diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md index 1760b182114..0bf1f396f9d 100644 --- a/doc/user/project/issues/confidential_issues.md +++ b/doc/user/project/issues/confidential_issues.md @@ -9,7 +9,7 @@ keep security vulnerabilities private or prevent surprises from leaking out. ## Making an issue confidential -You can make an issue confidential either by creating a new issue or editing +You can make an issue confidential during issue creation or by editing an existing one. When you create a new issue, a checkbox right below the text area is available @@ -19,11 +19,19 @@ confidential checkbox and hit **Save changes**. ![Creating a new confidential issue](img/confidential_issues_create.png) -## Making an issue non-confidential +## Modifying issue confidentiality -To make an issue non-confidential, all you have to do is edit it and unmark -the confidential checkbox. Once you save the issue, it will gain the default -visibility level you have chosen for your project. +There are two ways to change an issue's confidentiality. + +The first way is to edit the issue and mark/unmark the confidential checkbox. +Once you save the issue, it will change the confidentiality of the issue. + +The second way is to locate the Confidentiality section in the sidebar and click +**Edit**. A popup should appear and give you the option to turn on or turn off confidentiality. + +| Turn off confidentiality | Turn on confidentiality | +| :-----------: | :----------: | +| ![Turn off confidentiality](img/turn_off_confidentiality.png) | ![Turn on confidentiality](img/turn_on_confidentiality.png) | Every change from regular to confidential and vice versa, is indicated by a system note in the issue's comments. @@ -49,6 +57,12 @@ issue you are commenting on is confidential. ![Confidential issue page](img/confidential_issues_issue_page.png) +There is also an indicator on the sidebar denoting confidentiality. + +| Confidential issue | Not confidential issue | +| :-----------: | :----------: | +| ![Sidebar confidential issue](img/sidebar_confidential_issue.png) | ![Sidebar not confidential issue](img/sidebar_not_confidential_issue.png) | + ## Permissions and access to confidential issues There are two kinds of level access for confidential issues. The general rule diff --git a/doc/user/project/issues/img/confidential_issues_index_page.png b/doc/user/project/issues/img/confidential_issues_index_page.png Binary files differindex e4b492a2769..f3efe0ce04e 100755..100644 --- a/doc/user/project/issues/img/confidential_issues_index_page.png +++ b/doc/user/project/issues/img/confidential_issues_index_page.png diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png Binary files differindex f04ec8ff32b..0f5c774d258 100755..100644 --- a/doc/user/project/issues/img/confidential_issues_issue_page.png +++ b/doc/user/project/issues/img/confidential_issues_issue_page.png diff --git a/doc/user/project/issues/img/sidebar_confidential_issue.png b/doc/user/project/issues/img/sidebar_confidential_issue.png Binary files differnew file mode 100755 index 00000000000..d99a1ca756e --- /dev/null +++ b/doc/user/project/issues/img/sidebar_confidential_issue.png diff --git a/doc/user/project/issues/img/sidebar_not_confidential_issue.png b/doc/user/project/issues/img/sidebar_not_confidential_issue.png Binary files differnew file mode 100755 index 00000000000..2e6cbbc5b3a --- /dev/null +++ b/doc/user/project/issues/img/sidebar_not_confidential_issue.png diff --git a/doc/user/project/issues/img/turn_off_confidentiality.png b/doc/user/project/issues/img/turn_off_confidentiality.png Binary files differnew file mode 100644 index 00000000000..248ae6522d6 --- /dev/null +++ b/doc/user/project/issues/img/turn_off_confidentiality.png diff --git a/doc/user/project/issues/img/turn_on_confidentiality.png b/doc/user/project/issues/img/turn_on_confidentiality.png Binary files differnew file mode 100644 index 00000000000..fac4c833699 --- /dev/null +++ b/doc/user/project/issues/img/turn_on_confidentiality.png diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index 25e5b897825..f5c748a03b3 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -22,7 +22,7 @@ To share 'Project Acme' with the 'Engineering' group, go to the project settings Then select the 'Share with group' tab by clicking it. -Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it. +Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it. ![share project with groups tab](img/share_project_with_groups_tab.png) @@ -34,11 +34,10 @@ After sharing 'Project Acme' with 'Engineering', the project will be listed on t In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. -## Share project with group lock (EES/EEP) +## Share project with group lock -In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/) -it is possible to prevent projects in a group from [sharing +It is possible to prevent projects in a group from [sharing a project with another group](../members/share_project_with_groups.md). This allows for tighter control over project access. -Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep). +Learn more about [Share with group lock](../../group/index.html#share-with-group-lock). diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index 20aadb8f7ff..dfe43c6b691 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -113,7 +113,7 @@ started: 1. Use the following command to list the private GPG key you just created: ``` - gpg --list-secret-keys mr@robot.sh + gpg --list-secret-keys --keyid-format 0xLONG mr@robot.sh ``` Replace `mr@robot.sh` with the email address you entered above. @@ -167,7 +167,7 @@ key to use. 1. Use the following command to list the private GPG key you just created: ``` - gpg --list-secret-keys mr@robot.sh + gpg --list-secret-keys --keyid-format 0xLONG mr@robot.sh ``` Replace `mr@robot.sh` with the email address you entered above. diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 97cca3007b1..23b1c61cd16 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -28,17 +28,18 @@ with all their related data and be moved into a new GitLab instance. ## Version history -| GitLab version | Import/Export version | -| -------- | -------- | -| 9.4.0 to current | 0.1.8 | -| 9.2.0 | 0.1.7 | -| 8.17.0 | 0.1.6 | -| 8.13.0 | 0.1.5 | -| 8.12.0 | 0.1.4 | -| 8.10.3 | 0.1.3 | -| 8.10.0 | 0.1.2 | -| 8.9.5 | 0.1.1 | -| 8.9.0 | 0.1.0 | +| GitLab version | Import/Export version | +| ---------------- | --------------------- | +| 10.0 to current | 0.2.0 | +| 9.4.0 | 0.1.8 | +| 9.2.0 | 0.1.7 | +| 8.17.0 | 0.1.6 | +| 8.13.0 | 0.1.5 | +| 8.12.0 | 0.1.4 | +| 8.10.3 | 0.1.3 | +| 8.10.0 | 0.1.2 | +| 8.9.5 | 0.1.1 | +| 8.9.0 | 0.1.0 | > The table reflects what GitLab version we updated the Import/Export version at. > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3) diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index e9ee1abc6c1..c0b8a87f038 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -4,7 +4,7 @@ A separate system for documentation called Wiki, is built right into each GitLab project. It is enabled by default on all new projects and you can find it under **Wiki** in your project. -Wikis are very convenient if you don't want to keep you documentation in your +Wikis are very convenient if you don't want to keep your documentation in your repository, but you do want to keep it in the same project where your code resides. diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index 20edcf75ff1..818bbb50d0e 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -47,7 +47,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I click new milestone button' do - page.within('.breadcrumbs') do + page.within('.nav-controls') do click_link "New milestone" end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 3b8d9af96c1..f88738b4c61 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -37,7 +37,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I goto the Merge Requests page' do page.within '.nav-sidebar' do - click_link "Merge Requests" + first(:link, "Merge Requests").click end end @@ -52,7 +52,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I visit the forks page of the "Shop" project' do - @project = Project.where(name: 'Shop').last + @project = Project.where(name: 'Shop').first visit project_forks_path(@project) end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index f7dd4fc21e9..b9460f5b534 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -62,7 +62,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click link "New issue"' do - page.within '.breadcrumbs' do + page.within '#content-body' do page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue') end end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index 307902a887e..16a2d4a6f93 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -16,7 +16,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I click link "New Milestone"' do - page.within('.breadcrumbs') do + page.within('.nav-controls') do click_link "New milestone" end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 3c3bffd7223..0d49a4ab90d 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -14,7 +14,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click link "New Merge Request"' do - page.within '.breadcrumbs' do + page.within '.nav-controls' do page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 0a89c1baf20..3a762be8f1f 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -6,7 +6,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'change project settings' do fill_in 'project_name_edit', with: 'NewName' - select 'Disabled', from: 'project_project_feature_attributes_issues_access_level' end step 'I save project' do diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 96b7ba7549f..2a1e6b2bce8 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -23,7 +23,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "New snippet"' do - page.within '.breadcrumbs' do + page.within '.nav-controls' do first(:link, "New snippet").click end end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 243a0f54f7f..f6445b57ec0 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -218,7 +218,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps # Wiki step 'I go to wiki page' do - click_link "Wiki" + first(:link, "Wiki").click expect(current_path).to eq project_wiki_path(@project, "home") end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 2bb21a798aa..104d024fee2 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -11,7 +11,7 @@ module SharedActiveTab end def ensure_active_sub_tab(content) - expect(find('.sidebar-sub-level-items > li.active')).to have_content(content) + expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content) end def ensure_active_sub_nav(content) @@ -23,7 +23,7 @@ module SharedActiveTab end step 'no other sub tabs should be active' do - expect(page).to have_selector('.sidebar-sub-level-items > li.active', count: 1) + expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1) end step 'no other sub navs should be active' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 605c9a3ab71..96cc0745e97 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -89,7 +89,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_project_path(@project) expect(page).to have_content("Project name") - expect(page).to have_content("Sharing and permissions") + expect(page).to have_content("Permissions") end def current_project diff --git a/features/steps/user.rb b/features/steps/user.rb index 59385a6ab59..321c1e942d5 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -17,14 +17,9 @@ class Spinach::Features::User < Spinach::FeatureSteps Issues::CreateService.new(project, user, issue_params).execute # Push code contribution - push_params = { - project: project, - action: Event::PUSHED, - author_id: user.id, - data: { commit_count: 3 } - } - - Event.create(push_params) + event = create(:push_event, project: project, author: user) + + create(:push_event_payload, event: event, commit_count: 3) end step 'I should see contributed projects' do @@ -38,6 +33,6 @@ class Spinach::Features::User < Spinach::FeatureSteps end def contributed_project - @contributed_project ||= create(:project, :public) + @contributed_project ||= create(:project, :public, :empty_repo) end end diff --git a/lib/api/api.rb b/lib/api/api.rb index d9a62bffb6d..ee4e1688e12 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -2,6 +2,17 @@ module API class API < Grape::API include APIGuard + LOG_FILENAME = Rails.root.join("log", "api_json.log") + + use GrapeLogging::Middleware::RequestLogger, + logger: Logger.new(LOG_FILENAME), + formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, + include: [ + GrapeLogging::Loggers::Response.new, + GrapeLogging::Loggers::FilterParameters.new, + GrapeLogging::Loggers::ClientEnv.new + ] + allow_access_with_scope :api prefix :api diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 0b45621ce7b..d7138b2f2fe 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -20,7 +20,7 @@ module API use :pagination end get do - messages = BroadcastMessage.all + messages = BroadcastMessage.all.order_id_desc present paginate(messages), with: Entities::BroadcastMessage end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 81da2cf820b..52c49e5caa9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -45,7 +45,7 @@ module API expose :confirmed_at expose :last_activity_on expose :email - expose :color_scheme_id, :projects_limit, :current_sign_in_at + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project @@ -557,7 +557,7 @@ module API end class Event < Grape::Entity - expose :title, :project_id, :action_name + expose :project_id, :action_name expose :target_id, :target_iid, :target_type, :author_id expose :target_title expose :created_at diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 31a918eda60..e817dcbbc4b 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -74,7 +74,12 @@ module API use :optional_params end post do - authorize! :create_group + parent_group = find_group!(params[:parent_id]) if params[:parent_id].present? + if parent_group + authorize! :create_subgroup, parent_group + else + authorize! :create_group + end group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8b03df65ae4..00dbc2aee7a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -93,7 +93,7 @@ module API end def find_group(id) - if id =~ /^\d+$/ + if id.to_s =~ /^\d+$/ Group.find_by(id: id) else Group.find_by_full_path(id) diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index c3affcc6c6b..95ef8f42954 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -21,7 +21,7 @@ module API get ":id/merge_requests/:merge_request_iid/versions" do merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff + present paginate(merge_request.merge_request_diffs.order_id_desc), with: Entities::MergeRequestDiff end desc 'Get a single merge request diff version' do diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index ef09d9505d2..c570eace862 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -28,7 +28,7 @@ module API end def list_milestones_for(parent) - milestones = parent.milestones + milestones = parent.milestones.order_id_desc milestones = Milestone.filter_by_state(milestones, params[:state]) milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present? milestones = filter_by_search(milestones, params[:search]) if params[:search] diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index ac47a713966..c928ce5265b 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -31,7 +31,7 @@ module API end class Event < Grape::Entity - expose :title, :project_id, :action_name + expose :project_id, :action_name expose :target_id, :target_type, :author_id expose :target_title expose :created_at diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb index 35f462e907b..22866fc2845 100644 --- a/lib/api/v3/merge_request_diffs.rb +++ b/lib/api/v3/merge_request_diffs.rb @@ -20,7 +20,7 @@ module API get ":id/merge_requests/:merge_request_id/versions" do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff + present merge_request.merge_request_diffs.order_id_desc, with: ::API::Entities::MergeRequestDiff end desc 'Get a single merge request diff version' do diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb index 4c7061d4939..9be4cf9d22a 100644 --- a/lib/api/v3/milestones.rb +++ b/lib/api/v3/milestones.rb @@ -34,6 +34,7 @@ module API milestones = user_project.milestones milestones = filter_milestones_state(milestones, params[:state]) milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present? + milestones = milestones.order_id_desc present paginate(milestones), with: ::API::Entities::Milestone end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 74df246bdfe..7c260b8d910 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -120,7 +120,7 @@ module API get do authenticate! - present_projects current_user.authorized_projects, + present_projects current_user.authorized_projects.order_id_desc, with: ::API::V3::Entities::ProjectWithAccess end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 2d6e8ffc90f..9923ec4e870 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -5,6 +5,7 @@ module Banzai # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist. class SanitizationFilter < HTML::Pipeline::SanitizationFilter UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze + TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/ def whitelist whitelist = super @@ -24,7 +25,8 @@ module Banzai # Only push these customizations once return if customized?(whitelist[:transformers]) - # Allow table alignment + # Allow table alignment; we whitelist specific style properties in a + # transformer below whitelist[:attributes]['th'] = %w(style) whitelist[:attributes]['td'] = %w(style) @@ -43,6 +45,9 @@ module Banzai whitelist[:elements].push('abbr') whitelist[:attributes]['abbr'] = %w(title) + # Disallow `name` attribute globally + whitelist[:attributes][:all].delete('name') + # Allow any protocol in `a` elements... whitelist[:protocols].delete('a') @@ -52,6 +57,9 @@ module Banzai # Remove `rel` attribute from `a` elements whitelist[:transformers].push(self.class.remove_rel) + # Remove any `style` properties not required for table alignment + whitelist[:transformers].push(self.class.remove_unsafe_table_style) + whitelist end @@ -81,6 +89,21 @@ module Banzai end end end + + def remove_unsafe_table_style + lambda do |env| + node = env[:node] + + return unless node.name == 'th' || node.name == 'td' + return unless node.has_attribute?('style') + + if node['style'] =~ TABLE_ALIGNMENT_PATTERN + node['style'] = "text-align: #{$~[:alignment]}" + else + node.remove_attribute('style') + end + end + end end end end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 3784f6c4947..3f7b42456af 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -25,6 +25,12 @@ module Gitlab end EOS + def self.get_uuid(key) + Gitlab::Redis::SharedState.with do |redis| + redis.get(redis_shared_state_key(key)) || false + end + end + def self.cancel(key, uuid) Gitlab::Redis::SharedState.with do |redis| redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_shared_state_key(key)], argv: [uuid]) @@ -35,10 +41,10 @@ module Gitlab "gitlab:exclusive_lease:#{key}" end - def initialize(key, timeout:) + def initialize(key, uuid: nil, timeout:) @redis_shared_state_key = self.class.redis_shared_state_key(key) @timeout = timeout - @uuid = SecureRandom.uuid + @uuid = uuid || SecureRandom.uuid end # Try to obtain the lease. Return lease UUID on success, diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 9a5f4f598b2..a3dc2cd0b60 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -70,21 +70,41 @@ module Gitlab params['gitaly_token'].presence || Gitlab.config.gitaly['token'] end - def self.feature_enabled?(feature, status: MigrationStatus::OPT_IN) + # Evaluates whether a feature toggle is on or off + def self.feature_enabled?(feature_name, status: MigrationStatus::OPT_IN) + # Disabled features are always off! return false if status == MigrationStatus::DISABLED - feature = Feature.get("gitaly_#{feature}") + feature = Feature.get("gitaly_#{feature_name}") - # If the feature hasn't been set, turn it on if it's opt-out - return status == MigrationStatus::OPT_OUT unless Feature.persisted?(feature) + # If the feature has been set, always evaluate + if Feature.persisted?(feature) + if feature.percentage_of_time_value > 0 + # Probabilistically enable this feature + return Random.rand() * 100 < feature.percentage_of_time_value + end + + return feature.enabled? + end - if feature.percentage_of_time_value > 0 - # Probabilistically enable this feature - return Random.rand() * 100 < feature.percentage_of_time_value + # If the feature has not been set, the default depends + # on it's status + case status + when MigrationStatus::OPT_OUT + true + when MigrationStatus::OPT_IN + opt_into_all_features? + else + false end + end - feature.enabled? + # opt_into_all_features? returns true when the current environment + # is one in which we opt into features automatically + def self.opt_into_all_features? + Rails.env.development? || ENV["GITALY_FEATURE_DEFAULT_ON"] == "1" end + private_class_method :opt_into_all_features? def self.migrate(feature, status: MigrationStatus::OPT_IN) is_enabled = feature_enabled?(feature, status: status) diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb new file mode 100644 index 00000000000..1e1fdabca93 --- /dev/null +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -0,0 +1,19 @@ +module Gitlab + module GrapeLogging + module Formatters + class LogrageWithTimestamp + def call(severity, datetime, _, data) + time = data.delete :time + attributes = { + time: datetime.utc.iso8601(3), + severity: severity, + duration: time[:total], + db: time[:db], + view: time[:view] + }.merge(data) + ::Lograge.formatter.call(attributes) + "\n" + end + end + end + end +end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 3470a09eaf0..50ee879129c 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -3,7 +3,7 @@ module Gitlab extend self # For every version update, the version history in import_export.md has to be kept up to date. - VERSION = '0.1.8'.freeze + VERSION = '0.2.0'.freeze FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ec73846d844..2171c6c7bbb 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -50,6 +50,7 @@ project_tree: - :push_event_payload - :stages - :statuses + - :auto_devops - :triggers - :pipeline_schedules - :services @@ -142,4 +143,4 @@ methods: events: - :action push_event_payload: - - :action
\ No newline at end of file + - :action diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 20580459046..380b336395d 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -14,6 +14,7 @@ module Gitlab create_access_levels: 'ProtectedTag::CreateAccessLevel', labels: :project_labels, priorities: :label_priorities, + auto_devops: :project_auto_devops, label: :project_label }.freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze @@ -69,7 +70,6 @@ module Gitlab reset_tokens! remove_encrypted_attributes! - @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] set_st_diff_commits if @relation_name == :merge_request_diff set_diff if @relation_name == :merge_request_diff_files end diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 6023fa1820f..f42168c720e 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -3,6 +3,10 @@ module Gitlab module Middleware class Go + include ActionView::Helpers::TagHelper + + PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze + def initialize(app) @app = app end @@ -10,17 +14,20 @@ module Gitlab def call(env) request = Rack::Request.new(env) - if go_request?(request) - render_go_doc(request) - else - @app.call(env) - end + render_go_doc(request) || @app.call(env) end private def render_go_doc(request) - body = go_body(request) + return unless go_request?(request) + + path = project_path(request) + return unless path + + body = go_body(path) + return unless body + response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' }) response.finish end @@ -29,11 +36,13 @@ module Gitlab request["go-get"].to_i == 1 && request.env["PATH_INFO"].present? end - def go_body(request) - project_url = URI.join(Gitlab.config.gitlab.url, project_path(request)) + def go_body(path) + project_url = URI.join(Gitlab.config.gitlab.url, path) import_prefix = strip_url(project_url.to_s) - "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n" + meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git" + head_tag = content_tag :head, meta_tag + content_tag :html, head_tag end def strip_url(url) @@ -44,6 +53,10 @@ module Gitlab path_info = request.env["PATH_INFO"] path_info.sub!(/^\//, '') + project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX) + return unless project_path_match + path = project_path_match[1] + # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`. # In a traditional project with a single namespace, this would denote repo # `namespace/project` with subpath `path1/path2/../pathN`, but with nested @@ -51,7 +64,7 @@ module Gitlab # `path2/../pathN`, for example. # We find all potential project paths out of the path segments - path_segments = path_info.split('/') + path_segments = path.split('/') simple_project_path = path_segments.first(2).join('/') # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 894bd5efae5..7c02c9c5c48 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -26,6 +26,7 @@ module Gitlab apple-touch-icon.png assets autocomplete + boards ci dashboard deploy.html diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb new file mode 100644 index 00000000000..d43eff5ba4a --- /dev/null +++ b/lib/gitlab/themes.rb @@ -0,0 +1,84 @@ +module Gitlab + # Module containing GitLab's application theme definitions and helper methods + # for accessing them. + module Themes + extend self + + # Theme ID used when no `default_theme` configuration setting is provided. + APPLICATION_DEFAULT = 1 + + # Struct class representing a single Theme + Theme = Struct.new(:id, :name, :css_class) + + # All available Themes + THEMES = [ + Theme.new(1, 'Indigo', 'ui_indigo'), + Theme.new(2, 'Dark', 'ui_dark'), + Theme.new(3, 'Light', 'ui_light'), + Theme.new(4, 'Blue', 'ui_blue'), + Theme.new(5, 'Green', 'ui_green') + ].freeze + + # Convenience method to get a space-separated String of all the theme + # classes that might be applied to the `body` element + # + # Returns a String + def body_classes + THEMES.collect(&:css_class).uniq.join(' ') + end + + # Get a Theme by its ID + # + # If the ID is invalid, returns the default Theme. + # + # id - Integer ID + # + # Returns a Theme + def by_id(id) + THEMES.detect { |t| t.id == id } || default + end + + # Returns the number of defined Themes + def count + THEMES.size + end + + # Get the default Theme + # + # Returns a Theme + def default + by_id(default_id) + end + + # Iterate through each Theme + # + # Yields the Theme object + def each(&block) + THEMES.each(&block) + end + + # Get the Theme for the specified user, or the default + # + # user - User record + # + # Returns a Theme + def for_user(user) + if user + by_id(user.theme_id) + else + default + end + end + + private + + def default_id + @default_id ||= begin + id = Gitlab.config.gitlab.default_theme.to_i + theme_ids = THEMES.map(&:id) + + theme_ids.include?(id) ? id : APPLICATION_DEFAULT + end + end + end +end diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake index dd1825c8a9e..44074397c05 100644 --- a/lib/tasks/gitlab/import_export.rake +++ b/lib/tasks/gitlab/import_export.rake @@ -9,5 +9,16 @@ namespace :gitlab do task data: :environment do puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true) end + + desc 'GitLab | Bumps the Import/Export version for test_project_export.tar.gz' + task bump_test_version: :environment do + Dir.mktmpdir do |tmp_dir| + system("tar -zxf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} > /dev/null") + File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w') + system("tar -zcvf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} . > /dev/null") + end + + puts "Updated to #{Gitlab::ImportExport.version}" + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e5cf2aeb513..bd3d5b7c839 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -987,6 +987,24 @@ msgstr "" msgid "Push Rules" msgstr "" +msgid "ProjectsDropdown|Loading projects" +msgstr "" + +msgid "ProjectsDropdown|Sorry, no projects matched your search" +msgstr "" + +msgid "ProjectsDropdown|Projects you visit often will appear here" +msgstr "" + +msgid "ProjectsDropdown|Search your projects" +msgstr "" + +msgid "ProjectsDropdown|Something went wrong on our end" +msgstr "" + +msgid "ProjectsDropdown|This feature requires browser localStorage support" +msgstr "" + msgid "Push events" msgstr "" diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs new file mode 100755 index 00000000000..44f832ed3e6 --- /dev/null +++ b/scripts/trigger-build-docs @@ -0,0 +1,80 @@ +#!/usr/bin/env ruby + +require 'gitlab' + +# +# Give the remote branch a different name than the current one +# in order to avoid conflicts +# +@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee" +GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze + +# +# Configure credentials to be used with gitlab gem +# +Gitlab.configure do |config| + config.endpoint = 'https://gitlab.com/api/v4' + config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs +end + +# +# Dummy way to find out in which repo we are, CE or EE +# +def ee? + File.exist?('CHANGELOG-EE.md') +end + +# +# Create a remote branch in gitlab-docs +# +def create_remote_branch + Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master') + puts "Remote branch '#{@docs_branch}' created" +rescue Gitlab::Error::BadRequest + puts "Remote branch '#{@docs_branch}' already exists" +end + +# +# Remove a remote branch in gitlab-docs +# +def remove_remote_branch + Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch) + puts "Remote branch '#{@docs_branch}' deleted" +end + +# +# Trigger a pipeline in gitlab-docs +# +def trigger_pipeline + # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml + param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE' + + # The review app URL + app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}" + + # Create the pipeline + puts "=> Triggering a pipeline..." + pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] }) + + puts "=> Pipeline created:" + puts "" + puts "https://gitlab.com/gitlab-com/gitlab-docs/pipelines/#{pipeline.id}" + puts "" + puts "=> Preview your changes live at:" + puts "" + puts app_url + puts "" +end + +# +# When the first argument is deploy then create the branch and trigger pipeline +# When it is 'stop', it deleted the remote branch. That way, we ensure there +# are no stale remote branches and the Review server doesn't fill. +# +case ARGV[0] +when 'deploy' + create_remote_branch + trigger_pipeline +when 'cleanup' + remove_remote_branch +end diff --git a/scripts/trigger-build b/scripts/trigger-build-omnibus index dcda70d7ed8..dcda70d7ed8 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build-omnibus diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 3f6c1092163..dfa06c78d46 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::Boards::IssuesController do +describe Boards::IssuesController do let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } @@ -133,6 +133,22 @@ describe Projects::Boards::IssuesController do expect(response).to have_http_status(404) end end + + context 'with invalid board id' do + it 'returns a not found 404 response' do + create_issue user: user, board: 999, list: list1, title: 'New issue' + + expect(response).to have_http_status(404) + end + end + + context 'with invalid list id' do + it 'returns a not found 404 response' do + create_issue user: user, board: board, list: 999, title: 'New issue' + + expect(response).to have_http_status(404) + end + end end context 'with unauthorized user' do @@ -146,17 +162,15 @@ describe Projects::Boards::IssuesController do def create_issue(user:, board:, list:, title:) sign_in(user) - post :create, namespace_id: project.namespace.to_param, - project_id: project, - board_id: board.to_param, + post :create, board_id: board.to_param, list_id: list.to_param, - issue: { title: title }, + issue: { title: title, project_id: project.id }, format: :json end end describe 'PATCH update' do - let(:issue) { create(:labeled_issue, project: project, labels: [planning]) } + let!(:issue) { create(:labeled_issue, project: project, labels: [planning]) } context 'with valid params' do it 'returns a successful 200 response' do @@ -186,7 +200,7 @@ describe Projects::Boards::IssuesController do end it 'returns a not found 404 response for invalid issue id' do - move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id + move user: user, board: board, issue: double(id: 999), from_list_id: list1.id, to_list_id: list2.id expect(response).to have_http_status(404) end @@ -210,9 +224,9 @@ describe Projects::Boards::IssuesController do sign_in(user) patch :update, namespace_id: project.namespace.to_param, - project_id: project, + project_id: project.id, board_id: board.to_param, - id: issue.to_param, + id: issue.id, from_list_id: from_list_id, to_list_id: to_list_id, format: :json diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index 65beec16307..b11fce0fa58 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::Boards::ListsController do +describe Boards::ListsController do let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index c2ada8c8df7..b0564e27a68 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -2,9 +2,133 @@ require 'rails_helper' describe GroupsController do let(:user) { create(:user) } + let(:admin) { create(:admin) } let(:group) { create(:group, :public) } let(:project) { create(:project, namespace: group) } let!(:group_member) { create(:group_member, group: group, user: user) } + let!(:owner) { group.add_owner(create(:user)).user } + let!(:master) { group.add_master(create(:user)).user } + let!(:developer) { group.add_developer(create(:user)).user } + let!(:guest) { group.add_guest(create(:user)).user } + + shared_examples 'member with ability to create subgroups' do + it 'renders the new page' do + sign_in(member) + + get :new, parent_id: group.id + + expect(response).to render_template(:new) + end + end + + shared_examples 'member without ability to create subgroups' do + it 'renders the 404 page' do + sign_in(member) + + get :new, parent_id: group.id + + expect(response).not_to render_template(:new) + expect(response.status).to eq(404) + end + end + + describe 'GET #new' do + context 'when creating subgroups', :nested_groups do + [true, false].each do |can_create_group_status| + context "and can_create_group is #{can_create_group_status}" do + before do + User.where(id: [admin, owner, master, developer, guest]).update_all(can_create_group: can_create_group_status) + end + + [:admin, :owner].each do |member_type| + context "and logged in as #{member_type.capitalize}" do + it_behaves_like 'member with ability to create subgroups' do + let(:member) { send(member_type) } + end + end + end + + [:guest, :developer, :master].each do |member_type| + context "and logged in as #{member_type.capitalize}" do + it_behaves_like 'member without ability to create subgroups' do + let(:member) { send(member_type) } + end + end + end + end + end + end + end + + describe 'POST #create' do + context 'when creating subgroups', :nested_groups do + [true, false].each do |can_create_group_status| + context "and can_create_group is #{can_create_group_status}" do + context 'and logged in as Owner' do + it 'creates the subgroup' do + owner.update_attribute(:can_create_group, can_create_group_status) + sign_in(owner) + + post :create, group: { parent_id: group.id, path: 'subgroup' } + + expect(response).to be_redirect + expect(response.body).to match(%r{http://test.host/#{group.path}/subgroup}) + end + end + + context 'and logged in as Developer' do + it 'renders the new template' do + developer.update_attribute(:can_create_group, can_create_group_status) + sign_in(developer) + + previous_group_count = Group.count + + post :create, group: { parent_id: group.id, path: 'subgroup' } + + expect(response).to render_template(:new) + expect(Group.count).to eq(previous_group_count) + end + end + end + end + end + + context 'when creating a top level group' do + before do + sign_in(developer) + end + + context 'and can_create_group is enabled' do + before do + developer.update_attribute(:can_create_group, true) + end + + it 'creates the Group' do + original_group_count = Group.count + + post :create, group: { path: 'subgroup' } + + expect(Group.count).to eq(original_group_count + 1) + expect(response).to be_redirect + end + end + + context 'and can_create_group is disabled' do + before do + developer.update_attribute(:can_create_group, false) + end + + it 'does not create the Group' do + original_group_count = Group.count + + post :create, group: { path: 'subgroup' } + + expect(Group.count).to eq(original_group_count) + expect(response).to render_template(:new) + end + end + end + end describe 'GET #index' do context 'as a user' do diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index a5f544b4f92..a66b4ab0902 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,7 +25,8 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '1' ) patch :update, user: params, format: format @@ -40,7 +41,8 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '2' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(prefs) diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb index 2f75bf12cd7..b5298b2f969 100644 --- a/spec/factories/milestones.rb +++ b/spec/factories/milestones.rb @@ -7,6 +7,7 @@ FactoryGirl.define do group nil project_id nil group_id nil + parent nil end trait :active do @@ -26,6 +27,9 @@ FactoryGirl.define do milestone.project = evaluator.project elsif evaluator.project_id milestone.project_id = evaluator.project_id + elsif evaluator.parent + id = evaluator.parent.id + evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id else milestone.project = create(:project) end diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb new file mode 100644 index 00000000000..8d124dc2381 --- /dev/null +++ b/spec/factories/project_auto_devops.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :project_auto_devops do + project + enabled true + domain "example.com" + end +end diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb index 5ff791fc36a..1215908f5ea 100644 --- a/spec/features/admin/admin_active_tab_spec.rb +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -14,8 +14,8 @@ RSpec.describe 'admin active tab' do shared_examples 'page has active sub tab' do |title| it "activates #{title} sub tab" do - expect(page).to have_selector('.sidebar-sub-level-items li.active', count: 1) - expect(page.find('.sidebar-sub-level-items li.active')).to have_content(title) + expect(page).to have_selector('.sidebar-sub-level-items > li.active', count: 2) + expect(page.all('.sidebar-sub-level-items > li.active')[1]).to have_content(title) end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 563818e8761..c490dce7ab0 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -48,7 +48,7 @@ feature 'Admin updates settings' do end scenario 'Change Slack Notifications Service template settings' do - click_link 'Service Templates' + first(:link, 'Service Templates').click click_link 'Slack notifications' fill_in 'Webhook', with: 'http://localhost' fill_in 'Username', with: 'test_user' diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 3e6a27eafd8..dfeba722ac6 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -288,8 +288,6 @@ describe 'Copy as GFM', js: true do 'SanitizationFilter', <<-GFM.strip_heredoc - <a name="named-anchor"></a> - <sub>sub</sub> <dl> diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb index ebc3d196118..facb67ae787 100644 --- a/spec/features/dashboard/issues_filter_spec.rb +++ b/spec/features/dashboard/issues_filter_spec.rb @@ -50,7 +50,7 @@ feature 'Dashboard Issues filtering', :js do it 'updates atom feed link' do visit_issues(milestone_title: '', assignee_id: user.id) - link = find('.breadcrumbs a[title="Subscribe"]') + link = find('.nav-controls a[title="Subscribe"]') params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 71de9f04653..0613c158c54 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -84,25 +84,11 @@ feature 'Dashboard Projects' do end context 'last push widget' do - let(:push_event_data) do - { - before: Gitlab::Git::BLANK_SHA, - after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e', - ref: 'refs/heads/feature', - user_id: user.id, - user_name: user.name, - repository: { - name: project.name, - url: 'localhost/rubinius', - description: '', - homepage: 'localhost/rubinius', - private: true - } - } - end - let!(:push_event) { create(:event, :pushed, data: push_event_data, project: project, author: user) } - before do + event = create(:push_event, project: project, author: user) + + create(:push_event_payload, event: event, ref: 'feature', action: :created) + visit dashboard_projects_path end diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb index 1f3c7fd3859..10389a74703 100644 --- a/spec/features/groups/members/request_access_spec.rb +++ b/spec/features/groups/members/request_access_spec.rb @@ -51,7 +51,7 @@ feature 'Groups > Members > Request access' do expect(group.requesters.exists?(user_id: user)).to be_truthy - click_link 'Members' + first(:link, 'Members').click page.within('.content') do expect(page).not_to have_content(user.name) diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb new file mode 100644 index 00000000000..8842d1391aa --- /dev/null +++ b/spec/features/groups/share_lock_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +feature 'Group share with group lock' do + given(:root_owner) { create(:user) } + given(:root_group) { create(:group) } + + background do + root_group.add_owner(root_owner) + sign_in(root_owner) + end + + context 'with a subgroup', :nested_groups do + given!(:subgroup) { create(:group, parent: root_group) } + + context 'when enabling the parent group share with group lock' do + scenario 'the subgroup share with group lock becomes enabled' do + visit edit_group_path(root_group) + check 'group_share_with_group_lock' + + click_on 'Save group' + + expect(subgroup.reload.share_with_group_lock?).to be_truthy + end + end + + context 'when disabling the parent group share with group lock (which was already enabled)' do + background do + visit edit_group_path(root_group) + check 'group_share_with_group_lock' + click_on 'Save group' + end + + context 'and the subgroup share with group lock is enabled' do + scenario 'the subgroup share with group lock does not change' do + visit edit_group_path(root_group) + uncheck 'group_share_with_group_lock' + + click_on 'Save group' + + expect(subgroup.reload.share_with_group_lock?).to be_truthy + end + end + + context 'but the subgroup share with group lock is disabled' do + background do + visit edit_group_path(subgroup) + uncheck 'group_share_with_group_lock' + click_on 'Save group' + end + + scenario 'the subgroup share with group lock does not change' do + visit edit_group_path(root_group) + uncheck 'group_share_with_group_lock' + + click_on 'Save group' + + expect(subgroup.reload.share_with_group_lock?).to be_falsey + end + end + end + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 11db1105d91..5c284a1fe5f 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -577,7 +577,7 @@ describe 'Issues' do it 'redirects to signin then back to new issue after signin' do visit project_issues_path(project) - page.within '.breadcrumbs' do + page.within '.nav-controls' do click_link 'New issue' end diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index d3b1d1f7be3..17f914c9c17 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -1,20 +1,21 @@ require 'rails_helper' feature 'Project edit', js: true do + let(:admin) { create(:admin) } let(:user) { create(:user) } let(:project) { create(:project) } - before do - project.team << [user, :master] - sign_in(user) + context 'feature visibility' do + before do + project.team << [user, :master] + sign_in(user) - visit edit_project_path(project) - end + visit edit_project_path(project) + end - context 'feature visibility' do context 'merge requests select' do it 'hides merge requests section' do - select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level') + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click expect(page).to have_selector('.merge-requests-feature', visible: false) end @@ -30,7 +31,7 @@ feature 'Project edit', js: true do context 'builds select' do it 'hides builds select section' do - select('Disabled', from: 'project_project_feature_attributes_builds_access_level') + find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click expect(page).to have_selector('.builds-feature', visible: false) end @@ -44,4 +45,18 @@ feature 'Project edit', js: true do end end end + + context 'LFS enabled setting' do + before do + sign_in(admin) + end + + it 'displays the correct elements' do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + visit edit_project_path(project) + + expect(page).to have_content('Git Large File Storage') + expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true) + end + end end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 1c59e57c0a4..af7ad365546 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -10,26 +10,23 @@ feature 'Environments page', :js do sign_in(user) end - given!(:environment) { } - given!(:deployment) { } - given!(:action) { } - - before do - visit_environments(project) - end - describe 'page tabs' do - scenario 'shows "Available" and "Stopped" tab with links' do + it 'shows "Available" and "Stopped" tab with links' do + visit_environments(project) + expect(page).to have_link('Available') expect(page).to have_link('Stopped') end describe 'with one available environment' do - given(:environment) { create(:environment, project: project, state: :available) } + before do + create(:environment, project: project, state: :available) + end describe 'in available tab page' do it 'should show one environment' do - visit project_environments_path(project, scope: 'available') + visit_environments(project, scope: 'available') + expect(page).to have_css('.environments-container') expect(page.all('.environment-name').length).to eq(1) end @@ -37,7 +34,8 @@ feature 'Environments page', :js do describe 'in stopped tab page' do it 'should show no environments' do - visit project_environments_path(project, scope: 'stopped') + visit_environments(project, scope: 'stopped') + expect(page).to have_css('.environments-container') expect(page).to have_content('You don\'t have any environments right now') end @@ -45,11 +43,14 @@ feature 'Environments page', :js do end describe 'with one stopped environment' do - given(:environment) { create(:environment, project: project, state: :stopped) } + before do + create(:environment, project: project, state: :stopped) + end describe 'in available tab page' do it 'should show no environments' do - visit project_environments_path(project, scope: 'available') + visit_environments(project, scope: 'available') + expect(page).to have_css('.environments-container') expect(page).to have_content('You don\'t have any environments right now') end @@ -57,7 +58,8 @@ feature 'Environments page', :js do describe 'in stopped tab page' do it 'should show one environment' do - visit project_environments_path(project, scope: 'stopped') + visit_environments(project, scope: 'stopped') + expect(page).to have_css('.environments-container') expect(page.all('.environment-name').length).to eq(1) end @@ -66,86 +68,84 @@ feature 'Environments page', :js do end context 'without environments' do - scenario 'does show no environments' do - expect(page).to have_content('You don\'t have any environments right now.') + before do + visit_environments(project) end - scenario 'does show 0 as counter for environments in both tabs' do + it 'does not show environments and counters are set to zero' do + expect(page).to have_content('You don\'t have any environments right now.') + expect(page.find('.js-available-environments-count').text).to eq('0') expect(page.find('.js-stopped-environments-count').text).to eq('0') end end - describe 'when showing the environment' do - given(:environment) { create(:environment, project: project) } - - scenario 'does show environment name' do - expect(page).to have_link(environment.name) - end - - scenario 'does show number of available and stopped environments' do - expect(page.find('.js-available-environments-count').text).to eq('1') - expect(page.find('.js-stopped-environments-count').text).to eq('0') + describe 'environments table' do + given!(:environment) do + create(:environment, project: project, state: :available) end - context 'without deployments' do - scenario 'does show no deployments' do - expect(page).to have_content('No deployments yet') + context 'when there are no deployments' do + before do + visit_environments(project) end - context 'for available environment' do - given(:environment) { create(:environment, project: project, state: :available) } + it 'shows environments names and counters' do + expect(page).to have_link(environment.name) - scenario 'does not shows stop button' do - expect(page).not_to have_selector('.stop-env-link') - end + expect(page.find('.js-available-environments-count').text).to eq('1') + expect(page.find('.js-stopped-environments-count').text).to eq('0') end - context 'for stopped environment' do - given(:environment) { create(:environment, project: project, state: :stopped) } + it 'does not show deployments' do + expect(page).to have_content('No deployments yet') + end - scenario 'does not shows stop button' do - expect(page).not_to have_selector('.stop-env-link') - end + it 'does not show stip button when environment is not stoppable' do + expect(page).not_to have_selector('.stop-env-link') end end - context 'with deployments' do + context 'when there are deployments' do given(:project) { create(:project, :repository) } - given(:deployment) do + given!(:deployment) do create(:deployment, environment: environment, sha: project.commit.id) end - scenario 'does show deployment SHA' do - expect(page).to have_link(deployment.short_sha) - end + it 'shows deployment SHA and internal ID' do + visit_environments(project) - scenario 'does show deployment internal id' do + expect(page).to have_link(deployment.short_sha) expect(page).to have_content(deployment.iid) end - context 'with build and manual actions' do - given(:pipeline) { create(:ci_pipeline, project: project) } - given(:build) { create(:ci_build, pipeline: pipeline) } + context 'when builds and manual actions are present' do + given!(:pipeline) { create(:ci_pipeline, project: project) } + given!(:build) { create(:ci_build, pipeline: pipeline) } - given(:action) do + given!(:action) do create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') end - given(:deployment) do + given!(:deployment) do create(:deployment, environment: environment, deployable: build, sha: project.commit.id) end - scenario 'does show a play button' do + before do + visit_environments(project) + end + + it 'shows a play button' do find('.js-dropdown-play-icon-container').click + expect(page).to have_content(action.name.humanize) end - scenario 'does allow to play manual action', js: true do + it 'allows to play a manual action', js: true do expect(action).to be_manual find('.js-dropdown-play-icon-container').click @@ -155,19 +155,19 @@ feature 'Environments page', :js do .not_to change { Ci::Pipeline.count } end - scenario 'does show build name and id' do + it 'shows build name and id' do expect(page).to have_link("#{build.name} ##{build.id}") end - scenario 'does not show stop button' do + it 'shows a stop button' do expect(page).not_to have_selector('.stop-env-link') end - scenario 'does not show external link button' do + it 'does not show external link button' do expect(page).not_to have_css('external-url') end - scenario 'does not show terminal button' do + it 'does not show terminal button' do expect(page).not_to have_terminal_button end @@ -176,7 +176,7 @@ feature 'Environments page', :js do given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - scenario 'does show an external link button' do + it 'shows an external link button' do expect(page).to have_link(nil, href: environment.external_url) end end @@ -192,34 +192,34 @@ feature 'Environments page', :js do on_stop: 'close_app') end - scenario 'does show stop button' do + it 'shows a stop button' do expect(page).to have_selector('.stop-env-link') end - context 'for reporter' do + context 'when user is a reporter' do let(:role) { :reporter } - scenario 'does not show stop button' do + it 'does not show stop button' do expect(page).not_to have_selector('.stop-env-link') end end end - context 'with terminal' do + context 'when kubernetes terminal is available' do let(:project) { create(:kubernetes_project, :test_repo) } context 'for project master' do let(:role) { :master } - scenario 'it shows the terminal button' do + it 'shows the terminal button' do expect(page).to have_terminal_button end end - context 'for developer' do + context 'when user is a developer' do let(:role) { :developer } - scenario 'does not show terminal button' do + it 'does not show terminal button' do expect(page).not_to have_terminal_button end end @@ -228,59 +228,77 @@ feature 'Environments page', :js do end end - scenario 'does have a New environment button' do + it 'does have a new environment button' do + visit_environments(project) + expect(page).to have_link('New environment') end - describe 'when creating a new environment' do + describe 'creating a new environment' do before do visit_environments(project) end - context 'when logged as developer' do - before do - within(".top-area") do - click_link 'New environment' - end - end + context 'user is a developer' do + given(:role) { :developer } - context 'for valid name' do - before do - fill_in('Name', with: 'production') - click_on 'Save' - end + scenario 'developer creates a new environment with a valid name' do + within(".top-area") { click_link 'New environment' } + fill_in('Name', with: 'production') + click_on 'Save' - scenario 'does create a new pipeline' do - expect(page).to have_content('production') - end + expect(page).to have_content('production') end - context 'for invalid name' do - before do - fill_in('Name', with: 'name,with,commas') - click_on 'Save' - end + scenario 'developer creates a new environmetn with invalid name' do + within(".top-area") { click_link 'New environment' } + fill_in('Name', with: 'name,with,commas') + click_on 'Save' - scenario 'does show errors' do - expect(page).to have_content('Name can contain only letters') - end + expect(page).to have_content('Name can contain only letters') end end - context 'when logged as reporter' do + context 'user is a reporter' do given(:role) { :reporter } - scenario 'does not have a New environment link' do + scenario 'reporters tries to create a new environment' do expect(page).not_to have_link('New environment') end end end + describe 'environments folders' do + before do + create(:environment, project: project, + name: 'staging/review-1', + state: :available) + create(:environment, project: project, + name: 'staging/review-2', + state: :available) + end + + scenario 'users unfurls an environment folder' do + visit_environments(project) + + expect(page).not_to have_content 'review-1' + expect(page).not_to have_content 'review-2' + expect(page).to have_content 'staging 2' + + within('.folder-row') do + find('.folder-name', text: 'staging').click + end + + expect(page).to have_content 'review-1' + expect(page).to have_content 'review-2' + end + end + def have_terminal_button have_link(nil, href: terminal_project_environment_path(project, environment)) end - def visit_environments(project) - visit project_environments_path(project) + def visit_environments(project, **opts) + visit project_environments_path(project, **opts) end end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 24691629063..57722276d79 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -19,21 +19,16 @@ describe 'Edit Project Settings' do it 'toggles visibility' do visit edit_project_path(project) - select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" + # disable by clicking toggle + toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do click_button 'Save changes' end wait_for_requests expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") - select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" - page.within('.sharing-permissions') do - click_button 'Save changes' - end - wait_for_requests - expect(page).to have_selector(".shortcuts-#{shortcut_name}") - - select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" + # re-enable by clicking toggle again + toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do click_button 'Save changes' end @@ -176,19 +171,19 @@ describe 'Edit Project Settings' do end it "disables repository related features" do - select "Disabled", from: "project_project_feature_attributes_repository_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') page.within('.sharing-permissions') do click_button "Save changes" end - expect(find(".sharing-permissions")).to have_selector("select.disabled", count: 2) + expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2) end it "shows empty features project homepage" do - select "Disabled", from: "project_project_feature_attributes_repository_access_level" - select "Disabled", from: "project_project_feature_attributes_issues_access_level" - select "Disabled", from: "project_project_feature_attributes_wiki_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') + toggle_feature_off('project[project_feature_attributes][issues_access_level]') + toggle_feature_off('project[project_feature_attributes][wiki_access_level]') page.within('.sharing-permissions') do click_button "Save changes" @@ -201,9 +196,9 @@ describe 'Edit Project Settings' do end it "hides project activity tabs" do - select "Disabled", from: "project_project_feature_attributes_repository_access_level" - select "Disabled", from: "project_project_feature_attributes_issues_access_level" - select "Disabled", from: "project_project_feature_attributes_wiki_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') + toggle_feature_off('project[project_feature_attributes][issues_access_level]') + toggle_feature_off('project[project_feature_attributes][wiki_access_level]') page.within('.sharing-permissions') do click_button "Save changes" @@ -222,7 +217,7 @@ describe 'Edit Project Settings' do # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272 it "hides comments activity tab only on disabled issues, merge requests and repository" do - select "Disabled", from: "project_project_feature_attributes_issues_access_level" + toggle_feature_off('project[project_feature_attributes][issues_access_level]') save_changes_and_check_activity_tab do expect(page).to have_content("Comments") @@ -230,7 +225,7 @@ describe 'Edit Project Settings' do visit edit_project_path(project) - select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level" + toggle_feature_off('project[project_feature_attributes][merge_requests_access_level]') save_changes_and_check_activity_tab do expect(page).to have_content("Comments") @@ -238,7 +233,7 @@ describe 'Edit Project Settings' do visit edit_project_path(project) - select "Disabled", from: "project_project_feature_attributes_repository_access_level" + toggle_feature_off('project[project_feature_attributes][repository_access_level]') save_changes_and_check_activity_tab do expect(page).not_to have_content("Comments") @@ -275,4 +270,12 @@ describe 'Edit Project Settings' do expect(page).not_to have_selector('.project-stats') end end + + def toggle_feature_off(feature_name) + find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click + end + + def toggle_feature_on(feature_name) + find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click + end end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb deleted file mode 100644 index 5195d027a9f..00000000000 --- a/spec/features/projects/group_links_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -feature 'Project group links', :js do - include Select2Helper - - let(:master) { create(:user) } - let(:project) { create(:project) } - let!(:group) { create(:group) } - - background do - project.add_master(master) - sign_in(master) - end - - context 'setting an expiration date for a group link' do - before do - visit project_settings_members_path(project) - - click_on 'share-with-group-tab' - - select2 group.id, from: '#link_group_id' - fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') - page.find('body').click - find('.btn-create').trigger('click') - end - - it 'shows the expiration time with a warning class' do - page.within('.project-members-groups') do - expect(page).to have_content('Expires in 4 days') - expect(page).to have_selector('.text-warning') - end - end - end - - context 'nested group project' do - let!(:nested_group) { create(:group, parent: group) } - let!(:another_group) { create(:group) } - let!(:project) { create(:project, namespace: nested_group) } - - background do - group.add_master(master) - another_group.add_master(master) - end - - it 'does not show ancestors', :nested_groups do - visit project_settings_members_path(project) - - click_on 'share-with-group-tab' - click_link 'Search for a group' - - page.within '.select2-drop' do - expect(page).to have_content(another_group.name) - expect(page).not_to have_content(group.name) - end - end - end - - describe 'the groups dropdown' do - before do - group_two = create(:group) - group.add_owner(master) - group_two.add_owner(master) - - visit project_settings_members_path(project) - execute_script 'GroupsSelect.PER_PAGE = 1;' - open_select2 '#link_group_id' - end - - it 'should infinitely scroll' do - expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1) - - scroll_select2_to_bottom('.select2-drop .select2-results:visible') - - expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2) - end - end -end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index ad2db1a34f4..e5c7781a096 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -18,7 +18,7 @@ feature 'Import/Export - project import integration test', js: true do context 'when selecting the namespace' do let(:user) { create(:admin) } - let!(:namespace) { create(:namespace, name: 'asd', owner: user) } + let!(:namespace) { user.namespace } let(:project_path) { 'test-project-path' + SecureRandom.hex } context 'prefilled the path' do @@ -66,12 +66,11 @@ feature 'Import/Export - project import integration test', js: true do end scenario 'invalid project' do - namespace = create(:namespace, name: 'asdf', owner: user) - project = create(:project, namespace: namespace) + project = create(:project, namespace: user.namespace) visit new_project_path - select2(namespace.id, from: '#project_namespace_id') + select2(user.namespace.id, from: '#project_namespace_id') fill_in :project_path, with: project.name, visible: true click_link 'GitLab export' attach_file('file', file) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex e03e7b88174..9614c72cdc3 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb index 1c348b987d4..9950272af08 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/groups_with_access_list_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Members > Anonymous user sees members', js: true do +feature 'Projects > Members > Groups with access list', js: true do let(:user) { create(:user) } let(:group) { create(:group, :public) } let(:project) { create(:project, :public) } @@ -13,7 +13,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do visit project_settings_members_path(project) end - it 'updates group access level' do + scenario 'updates group access level' do click_button @group_link.human_access page.within '.dropdown-menu' do @@ -27,7 +27,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do expect(first('.group_member')).to have_content('Guest') end - it 'updates expiry date' do + scenario 'updates expiry date' do tomorrow = Date.today + 3 fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F") @@ -38,7 +38,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do end end - it 'deletes group link' do + scenario 'deletes group link' do page.within(first('.group_member')) do find('.btn-remove').click end @@ -47,8 +47,8 @@ feature 'Projects > Members > Anonymous user sees members', js: true do expect(page).not_to have_selector('.group_member') end - context 'search' do - it 'finds no results' do + context 'search in existing members (yes, this filters the groups list as well)' do + scenario 'finds no results' do page.within '.member-search-form' do fill_in 'search', with: 'testing 123' find('.member-search-btn').click @@ -57,7 +57,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do expect(page).not_to have_selector('.group_member') end - it 'finds results' do + scenario 'finds results' do page.within '.member-search-form' do fill_in 'search', with: group.name find('.member-search-btn').click diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb new file mode 100644 index 00000000000..3b368f8e25d --- /dev/null +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -0,0 +1,191 @@ +require 'spec_helper' + +feature 'Project > Members > Share with Group', :js do + include Select2Helper + include ActionView::Helpers::DateHelper + + let(:master) { create(:user) } + + describe 'Share with group lock' do + shared_examples 'the project can be shared with groups' do + scenario 'the "Share with group" tab exists' do + visit project_settings_members_path(project) + expect(page).to have_selector('#share-with-group-tab') + end + end + + shared_examples 'the project cannot be shared with groups' do + scenario 'the "Share with group" tab does not exist' do + visit project_settings_members_path(project) + expect(page).to have_selector('#add-member-tab') + expect(page).not_to have_selector('#share-with-group-tab') + end + end + + context 'for a project in a root group' do + let!(:group_to_share_with) { create(:group) } + let(:project) { create(:project, namespace: create(:group)) } + + background do + project.add_master(master) + sign_in(master) + end + + context 'when the group has "Share with group lock" disabled' do + it_behaves_like 'the project can be shared with groups' + + scenario 'the project can be shared with another group' do + visit project_settings_members_path(project) + + click_on 'share-with-group-tab' + + select2 group_to_share_with.id, from: '#link_group_id' + page.find('body').click + find('.btn-create').trigger('click') + + page.within('.project-members-groups') do + expect(page).to have_content(group_to_share_with.name) + end + end + end + + context 'when the group has "Share with group lock" enabled' do + before do + project.namespace.update_column(:share_with_group_lock, true) + end + + it_behaves_like 'the project cannot be shared with groups' + end + end + + context 'for a project in a subgroup', :nested_groups do + let!(:group_to_share_with) { create(:group) } + let(:root_group) { create(:group) } + let(:subgroup) { create(:group, parent: root_group) } + let(:project) { create(:project, namespace: subgroup) } + + background do + project.add_master(master) + sign_in(master) + end + + context 'when the root_group has "Share with group lock" disabled' do + context 'when the subgroup has "Share with group lock" disabled' do + it_behaves_like 'the project can be shared with groups' + end + + context 'when the subgroup has "Share with group lock" enabled' do + before do + subgroup.update_column(:share_with_group_lock, true) + end + + it_behaves_like 'the project cannot be shared with groups' + end + end + + context 'when the root_group has "Share with group lock" enabled' do + before do + root_group.update_column(:share_with_group_lock, true) + end + + context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do + it_behaves_like 'the project can be shared with groups' + end + + context 'when the subgroup has "Share with group lock" enabled' do + before do + subgroup.update_column(:share_with_group_lock, true) + end + + it_behaves_like 'the project cannot be shared with groups' + end + end + end + end + + describe 'setting an expiration date for a group link' do + let(:project) { create(:project) } + let!(:group) { create(:group) } + + around do |example| + Timecop.freeze { example.run } + end + + before do + project.add_master(master) + sign_in(master) + + visit project_settings_members_path(project) + + click_on 'share-with-group-tab' + + select2 group.id, from: '#link_group_id' + + fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d') + page.find('body').click + find('.btn-create').trigger('click') + end + + scenario 'the group link shows the expiration time with a warning class' do + page.within('.project-members-groups') do + # Using distance_of_time_in_words_to_now because it is not the same as + # subtraction, and this way avoids time zone issues as well + expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at) + expect(page).to have_content(expires_in_text) + expect(page).to have_selector('.text-warning') + end + end + end + + describe 'the groups dropdown' do + context 'with multiple groups to choose from' do + let(:project) { create(:project) } + + background do + project.add_master(master) + sign_in(master) + + create(:group).add_owner(master) + create(:group).add_owner(master) + + visit project_settings_members_path(project) + execute_script 'GroupsSelect.PER_PAGE = 1;' + open_select2 '#link_group_id' + end + + it 'should infinitely scroll' do + expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1) + + scroll_select2_to_bottom('.select2-drop .select2-results:visible') + + expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2) + end + end + + context 'for a project in a nested group' do + let(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } + let!(:group_to_share_with) { create(:group) } + let!(:project) { create(:project, namespace: nested_group) } + + background do + project.add_master(master) + sign_in(master) + group.add_master(master) + group_to_share_with.add_master(master) + end + + scenario 'the groups dropdown does not show ancestors', :nested_groups do + visit project_settings_members_path(project) + + click_on 'share-with-group-tab' + click_link 'Search for a group' + + page.within '.select2-drop' do + expect(page).to have_content(group_to_share_with.name) + expect(page).not_to have_content(group.name) + end + end + end + end +end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 104ce08d9f3..b1ec556bf16 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -19,8 +19,8 @@ feature 'Project settings > Merge Requests', :js do expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') - select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" within('.sharing-permissions-form') do + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click click_on('Save changes') end @@ -39,8 +39,8 @@ feature 'Project settings > Merge Requests', :js do expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') - select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" within('.sharing-permissions-form') do + find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click click_on('Save changes') end @@ -60,8 +60,8 @@ feature 'Project settings > Merge Requests', :js do expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') - select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" within('.sharing-permissions-form') do + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click click_on('Save changes') end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 232d796a200..975d204e75e 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -41,5 +41,15 @@ feature "Pipelines settings" do checkbox = find_field('project_auto_cancel_pending_pipelines') expect(checkbox).to be_checked end + + scenario 'update auto devops settings' do + fill_in('project_auto_devops_attributes_domain', with: 'test.com') + page.choose('project_auto_devops_attributes_enabled_false') + click_on 'Save changes' + + expect(page.status_code).to eq(200) + expect(project.auto_devops).to be_present + expect(project.auto_devops).not_to be_enabled + end end end diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index 1756c7d00fe..37ee6255bd1 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -11,19 +11,19 @@ feature 'Visibility settings', js: true do end scenario 'project visibility select is available' do - visibility_select_container = find('.js-visibility-select') + visibility_select_container = find('.project-visibility-setting') - expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s - expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.' + expect(visibility_select_container.find('select').value).to eq project.visibility_level.to_s + expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.' end scenario 'project visibility description updates on change' do - visibility_select_container = find('.js-visibility-select') - visibility_select = visibility_select_container.find('.visibility-select') + visibility_select_container = find('.project-visibility-setting') + visibility_select = visibility_select_container.find('select') visibility_select.select('Private') expect(visibility_select.value).to eq '0' - expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.' + expect(visibility_select_container).to have_content 'Access must be granted explicitly to each user.' end end @@ -37,11 +37,10 @@ feature 'Visibility settings', js: true do end scenario 'project visibility is locked' do - visibility_select_container = find('.js-visibility-select') + visibility_select_container = find('.project-visibility-setting') - expect(visibility_select_container).not_to have_select '.visibility-select' - expect(visibility_select_container).to have_content 'Public' - expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.' + expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled' + expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.' end end end diff --git a/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb b/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb new file mode 100644 index 00000000000..a17e65cc5b9 --- /dev/null +++ b/spec/features/projects/user_browses_a_tree_with_a_folder_containing_only_a_folder.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +# This is a regression test for https://gitlab.com/gitlab-org/gitlab-ce/issues/37569 +describe 'User browses a tree with a folder containing only a folder' do + let(:project) { create(:project, :empty_repo) } + let(:user) { project.creator } + + before do + # We need to disable the tree.flat_path provided by Gitaly to reproduce the issue + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + + project.repository.create_dir(user, 'foo/bar', branch_name: 'master', message: 'Add the foo/bar folder') + sign_in(user) + visit(project_tree_path(project, project.repository.root_ref)) + end + + it 'shows the nested folder on a single row' do + expect(page).to have_content('foo/bar') + end +end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index db3fcc23475..9f285e28535 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -15,7 +15,7 @@ describe GroupMembersFinder, '#execute' do result = described_class.new(group).execute - expect(result.to_a).to eq([member3, member2, member1]) + expect(result.to_a).to match_array([member3, member2, member1]) end it 'returns members for nested group', :nested_groups do @@ -27,6 +27,6 @@ describe GroupMembersFinder, '#execute' do result = described_class.new(nested_group).execute - expect(result.to_a).to eq([member4, member3, member1]) + expect(result.to_a).to match_array([member1, member3, member4]) end end diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index 300ba8422e8..7bb1f45322e 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -17,6 +17,6 @@ describe MembersFinder, '#execute' do result = described_class.new(project, user2).execute - expect(result.to_a).to eq([member3, member2, member1]) + expect(result.to_a).to match_array([member1, member2, member3]) end end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index ff86437fdd5..e1f62508933 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -8,10 +8,15 @@ "properties" : { "id": { "type": "integer" }, "iid": { "type": "integer" }, + "project_id": { "type": ["integer", "null"] }, "title": { "type": "string" }, "confidential": { "type": "boolean" }, "due_date": { "type": ["date", "null"] }, "relative_position": { "type": "integer" }, + "project": { + "id": { "type": "integer" }, + "path": { "type": "string" } + }, "labels": { "type": "array", "items": { @@ -34,6 +39,7 @@ "type": "string", "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" }, + "type": { "type": "string" }, "title": { "type": "string" }, "priority": { "type": ["integer", "null"] } }, diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index 6181b3ccc86..e6c1d9c9d84 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -19,6 +19,7 @@ "organization", "last_sign_in_at", "confirmed_at", + "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb new file mode 100644 index 00000000000..b6d892548ef --- /dev/null +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe AutoDevopsHelper do + set(:project) { create(:project) } + set(:user) { create(:user) } + + describe '.show_auto_devops_callout?' do + let(:allowed) { true } + + before do + allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed } + allow(helper).to receive(:current_user) { user } + end + + subject { helper.show_auto_devops_callout?(project) } + + context 'when all conditions are met' do + it { is_expected.to eq(true) } + end + + context 'when dismissed' do + before do + helper.request.cookies[:auto_devops_settings_dismissed] = 'true' + end + + it { is_expected.to eq(false) } + end + + context 'when user cannot admin project' do + let(:allowed) { false } + + it { is_expected.to eq(false) } + end + + context 'when auto devops is enabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is explicitly enabled for project' do + before do + project.create_auto_devops!(enabled: true) + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is explicitly disabled for project' do + before do + project.create_auto_devops!(enabled: false) + end + + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index 7179185285c..4b6c7c33e5b 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -12,6 +12,17 @@ describe CommitsHelper do expect(helper.commit_author_link(commit)) .not_to include('onmouseover="alert(1)"') end + + it 'escapes the author name' do + user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>') + + commit = double(author: user, author_name: '', author_email: '') + + expect(helper.commit_author_link(commit)) + .to include('Foo <script>') + expect(helper.commit_author_link(commit, avatar: true)) + .to include('commit-author-name', 'Foo <script>') + end end describe 'commit_committer_link' do @@ -25,6 +36,17 @@ describe CommitsHelper do expect(helper.commit_committer_link(commit)) .not_to include('onmouseover="alert(1)"') end + + it 'escapes the commiter name' do + user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>') + + commit = double(committer: user, committer_name: '', committer_email: '') + + expect(helper.commit_committer_link(commit)) + .to include('Foo <script>') + expect(helper.commit_committer_link(commit, avatar: true)) + .to include('commit-committer-name', 'Foo <script>') + end end describe '#view_on_environment_button' do diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 05f969904f5..36031ac1a28 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -95,4 +95,97 @@ describe GroupsHelper do .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m) end end + + # rubocop:disable Layout/SpaceBeforeComma + describe '#share_with_group_lock_help_text', :nested_groups do + let!(:root_group) { create(:group) } + let!(:subgroup) { create(:group, parent: root_group) } + let!(:sub_subgroup) { create(:group, parent: subgroup) } + let(:root_owner) { create(:user) } + let(:sub_owner) { create(:user) } + let(:sub_sub_owner) { create(:user) } + let(:possible_help_texts) do + { + default_help: "This setting will be applied to all subgroups unless overridden by a group owner", + ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/, + ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/, + ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/ + } + end + let(:possible_linked_ancestors) do + { + root_group: root_group, + subgroup: subgroup + } + end + let(:users) do + { + root_owner: root_owner, + sub_owner: sub_owner, + sub_sub_owner: sub_sub_owner + } + end + subject { helper.share_with_group_lock_help_text(sub_subgroup) } + + where(:root_share_with_group_locked, :subgroup_share_with_group_locked, :sub_subgroup_share_with_group_locked, :current_user, :help_text, :linked_ancestor) do + [ + [false , false , false , :root_owner , :default_help , nil], + [false , false , false , :sub_owner , :default_help , nil], + [false , false , false , :sub_sub_owner , :default_help , nil], + [false , false , true , :root_owner , :default_help , nil], + [false , false , true , :sub_owner , :default_help , nil], + [false , false , true , :sub_sub_owner , :default_help , nil], + [false , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :subgroup], + [false , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup], + [false , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup], + [false , true , true , :root_owner , :ancestor_locked_but_you_can_override , :subgroup], + [false , true , true , :sub_owner , :ancestor_locked_but_you_can_override , :subgroup], + [false , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :subgroup], + [true , false , false , :root_owner , :default_help , nil], + [true , false , false , :sub_owner , :default_help , nil], + [true , false , false , :sub_sub_owner , :default_help , nil], + [true , false , true , :root_owner , :default_help , nil], + [true , false , true , :sub_owner , :default_help , nil], + [true , false , true , :sub_sub_owner , :default_help , nil], + [true , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :root_group], + [true , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :root_group], + [true , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :root_group], + [true , true , true , :root_owner , :ancestor_locked_but_you_can_override , :root_group], + [true , true , true , :sub_owner , :ancestor_locked_so_ask_the_owner , :root_group], + [true , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :root_group] + ] + end + + with_them do + before do + root_group.add_owner(root_owner) + subgroup.add_owner(sub_owner) + sub_subgroup.add_owner(sub_sub_owner) + + root_group.update_column(:share_with_group_lock, true) if root_share_with_group_locked + subgroup.update_column(:share_with_group_lock, true) if subgroup_share_with_group_locked + sub_subgroup.update_column(:share_with_group_lock, true) if sub_subgroup_share_with_group_locked + + allow(helper).to receive(:current_user).and_return(users[current_user]) + allow(helper).to receive(:can?) + .with(users[current_user], :change_share_with_group_lock, subgroup) + .and_return(Ability.allowed?(users[current_user], :change_share_with_group_lock, subgroup)) + + ancestor = possible_linked_ancestors[linked_ancestor] + if ancestor + allow(helper).to receive(:can?) + .with(users[current_user], :read_group, ancestor) + .and_return(Ability.allowed?(users[current_user], :read_group, ancestor)) + allow(helper).to receive(:can?) + .with(users[current_user], :admin_group, ancestor) + .and_return(Ability.allowed?(users[current_user], :admin_group, ancestor)) + end + end + + it 'has the correct help text with correct ancestor links' do + expect(subject).to match(possible_help_texts[help_text]) + expect(subject).to match(possible_linked_ancestors[linked_ancestor].name) unless help_text == :default_help + end + end + end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index a04c87b08eb..8b8080563d3 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe PreferencesHelper do - describe 'dashboard_choices' do + describe '#dashboard_choices' do it 'raises an exception when defined choices may be missing' do expect(User).to receive(:dashboards).and_return(foo: 'foo') expect { helper.dashboard_choices }.to raise_error(RuntimeError) @@ -26,7 +26,33 @@ describe PreferencesHelper do end end - describe 'user_color_scheme' do + describe '#user_application_theme' do + context 'with a user' do + it "returns user's theme's css_class" do + stub_user(theme_id: 3) + + expect(helper.user_application_theme).to eq 'ui_light' + end + + it 'returns the default when id is invalid' do + stub_user(theme_id: Gitlab::Themes.count + 5) + + allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1) + + expect(helper.user_application_theme).to eq 'ui_indigo' + end + end + + context 'without a user' do + it 'returns the default theme' do + stub_user + + expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class + end + end + end + + describe '#user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do allow(helper).to receive(:current_user) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index d1efa318d14..49cb7c954b4 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -191,10 +191,31 @@ describe ProjectsHelper do end end - describe 'link_to_member' do - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } - let(:user) { create(:user) } + describe '#link_to_member_avatar' do + let(:user) { build_stubbed(:user) } + let(:expected) { double } + + before do + expect(helper).to receive(:avatar_icon).with(user, 16).and_return(expected) + end + + it 'returns image tag for member avatar' do + expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16"], alt: "" }) + + helper.link_to_member_avatar(user) + end + + it 'returns image tag with avatar class' do + expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16", "any-avatar-class"], alt: "" }) + + helper.link_to_member_avatar(user, avatar_class: "any-avatar-class") + end + end + + describe '#link_to_member' do + let(:group) { build_stubbed(:group) } + let(:project) { build_stubbed(:project, group: group) } + let(:user) { build_stubbed(:user) } describe 'using the default options' do it 'returns an HTML link to the user' do diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 47baf83512f..2ee3792dd65 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -1,4 +1,5 @@ /* global BoardService */ +/* global mockBoardService */ import Vue from 'vue'; import '~/boards/stores/boards_store'; import boardBlankState from '~/boards/components/board_blank_state'; @@ -12,7 +13,7 @@ describe('Boards blank state', () => { const Comp = Vue.extend(boardBlankState); gl.issueBoards.BoardsStore.create(); - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService(); spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => { if (fail) { diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 447b244c71f..83b13b06dc1 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -4,6 +4,7 @@ /* global listObj */ /* global boardsMockInterceptor */ /* global BoardService */ +/* global mockBoardService */ import Vue from 'vue'; import '~/boards/models/assignee'; @@ -14,13 +15,13 @@ import '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card'; import './mock_data'; -describe('Issue card', () => { +describe('Board card', () => { let vm; beforeEach((done) => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); gl.issueBoards.BoardsStore.detail.issue = {}; diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index a89be911667..6bd00943a8f 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -3,6 +3,7 @@ /* global List */ /* global listObj */ /* global ListIssue */ +/* global mockBoardService */ import Vue from 'vue'; import _ from 'underscore'; import Sortable from 'vendor/Sortable'; @@ -24,7 +25,7 @@ describe('Board list component', () => { document.body.appendChild(el); Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); gl.IssueBoardsApp = new Vue(); @@ -32,6 +33,7 @@ describe('Board list component', () => { const list = new List(listObj); const issue = new ListIssue({ title: 'Testing', + id: 1, iid: 1, confidential: false, labels: [], diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index eac2eecb6bc..02e6692dda8 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -2,6 +2,7 @@ /* global BoardService */ /* global List */ /* global listObj */ +/* global mockBoardService */ import Vue from 'vue'; import boardNewIssue from '~/boards/components/board_new_issue'; @@ -35,7 +36,7 @@ describe('Issue boards new issue form', () => { const BoardNewIssueComp = Vue.extend(boardNewIssue); Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); gl.IssueBoardsApp = new Vue(); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 5ea160b7790..9e5b0bd3efe 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -4,6 +4,7 @@ /* global listObj */ /* global listObjDuplicate */ /* global ListIssue */ +/* global mockBoardService */ import Vue from 'vue'; import Cookies from 'js-cookie'; @@ -20,7 +21,7 @@ import './mock_data'; describe('Store', () => { beforeEach(() => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => { @@ -78,7 +79,7 @@ describe('Store', () => { it('persists new list', (done) => { gl.issueBoards.BoardsStore.new({ title: 'Test', - type: 'label', + list_type: 'label', label: { id: 1, title: 'Testing', @@ -210,6 +211,7 @@ describe('Store', () => { it('moves issue in list', (done) => { const issue = new ListIssue({ title: 'Testing', + id: 2, iid: 2, confidential: false, labels: [], diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index c4e8966ad6c..8dacac20cad 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -1,7 +1,9 @@ +/* global mockBoardService */ import Vue from 'vue'; import '~/boards/services/board_service'; import '~/boards/components/board'; import '~/boards/models/list'; +import '../mock_data'; describe('Board component', () => { let vm; @@ -13,8 +15,12 @@ describe('Board component', () => { el = document.createElement('div'); document.body.appendChild(el); - // eslint-disable-next-line no-undef - gl.boardService = new BoardService('/', '/', 1); + gl.boardService = mockBoardService({ + boardsEndpoint: '/', + listsEndpoint: '/', + bulkUpdatePath: '/', + boardId: 1, + }); vm = new gl.issueBoards.Board({ propsData: { diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 47aaa57e6b9..7d430ec35e2 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -37,6 +37,7 @@ describe('Issue card component', () => { list = listObj; issue = new ListIssue({ title: 'Testing', + id: 1, iid: 1, confidential: false, labels: [list.label], @@ -238,65 +239,63 @@ describe('Issue card component', () => { }); describe('labels', () => { - describe('exists', () => { - beforeEach((done) => { - component.issue.addLabel(label1); + beforeEach((done) => { + component.issue.addLabel(label1); - Vue.nextTick(() => done()); - }); + Vue.nextTick(() => done()); + }); - it('renders list label', () => { - expect( - component.$el.querySelectorAll('.label').length, - ).toBe(2); + it('renders list label', () => { + expect( + component.$el.querySelectorAll('.label').length, + ).toBe(2); + }); + + it('renders label', () => { + const nodes = []; + component.$el.querySelectorAll('.label').forEach((label) => { + nodes.push(label.title); }); - it('renders label', () => { - const nodes = []; - component.$el.querySelectorAll('.label').forEach((label) => { - nodes.push(label.title); - }); + expect( + nodes.includes(label1.description), + ).toBe(true); + }); - expect( - nodes.includes(label1.description), - ).toBe(true); - }); + it('sets label description as title', () => { + expect( + component.$el.querySelector('.label').getAttribute('title'), + ).toContain(label1.description); + }); - it('sets label description as title', () => { - expect( - component.$el.querySelector('.label').getAttribute('title'), - ).toContain(label1.description); + it('sets background color of button', () => { + const nodes = []; + component.$el.querySelectorAll('.label').forEach((label) => { + nodes.push(label.style.backgroundColor); }); - it('sets background color of button', () => { - const nodes = []; - component.$el.querySelectorAll('.label').forEach((label) => { - nodes.push(label.style.backgroundColor); - }); + expect( + nodes.includes(label1.color), + ).toBe(true); + }); - expect( - nodes.includes(label1.color), - ).toBe(true); - }); + it('does not render label if label does not have an ID', (done) => { + component.issue.addLabel(new ListLabel({ + title: 'closed', + })); - it('does not render label if label does not have an ID', (done) => { - component.issue.addLabel(new ListLabel({ - title: 'closed', - })); + Vue.nextTick() + .then(() => { + expect( + component.$el.querySelectorAll('.label').length, + ).toBe(2); + expect( + component.$el.textContent, + ).not.toContain('closed'); - Vue.nextTick() - .then(() => { - expect( - component.$el.querySelectorAll('.label').length, - ).toBe(2); - expect( - component.$el.textContent, - ).not.toContain('closed'); - - done(); - }) - .catch(done.fail); - }); + done(); + }) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index cd1497bc5e6..022d286d5df 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle */ /* global BoardService */ /* global ListIssue */ +/* global mockBoardService */ import Vue from 'vue'; import '~/lib/utils/url_utility'; @@ -16,11 +17,12 @@ describe('Issue model', () => { let issue; beforeEach(() => { - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService(); gl.issueBoards.BoardsStore.create(); issue = new ListIssue({ title: 'Testing', + id: 1, iid: 1, confidential: false, labels: [{ diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index db50829a276..d4627223a12 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle */ /* global boardsMockInterceptor */ /* global BoardService */ +/* global mockBoardService */ /* global List */ /* global ListIssue */ /* global listObj */ @@ -22,7 +23,9 @@ describe('List model', () => { beforeEach(() => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); + gl.boardService = mockBoardService({ + bulkUpdatePath: '/test/issue-boards/board/1/lists', + }); gl.issueBoards.BoardsStore.create(); list = new List(listObj); @@ -92,6 +95,7 @@ describe('List model', () => { const listDup = new List(listObjDuplicate); const issue = new ListIssue({ title: 'Testing', + id: _.random(10000), iid: _.random(10000), confidential: false, labels: [list.label, listDup.label], @@ -118,6 +122,7 @@ describe('List model', () => { for (let i = 0; i < 30; i += 1) { list.issues.push(new ListIssue({ title: 'Testing', + id: _.random(10000) + i, iid: _.random(10000) + i, confidential: false, labels: [list.label], @@ -137,7 +142,7 @@ describe('List model', () => { it('does not increase page number if issue count is less than the page size', () => { list.issues.push(new ListIssue({ title: 'Testing', - iid: _.random(10000), + id: _.random(10000), confidential: false, labels: [list.label], assignees: [], @@ -156,7 +161,7 @@ describe('List model', () => { spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({ json() { return { - iid: 42, + id: 42, }; }, })); @@ -165,14 +170,14 @@ describe('List model', () => { it('adds new issue to top of list', (done) => { list.issues.push(new ListIssue({ title: 'Testing', - iid: _.random(10000), + id: _.random(10000), confidential: false, labels: [list.label], assignees: [], })); const dummyIssue = new ListIssue({ title: 'new issue', - iid: _.random(10000), + id: _.random(10000), confidential: false, labels: [list.label], assignees: [], diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index a64c3964ee3..0a93086985e 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,3 +1,4 @@ +/* global BoardService */ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ const listObj = { @@ -28,19 +29,19 @@ const listObjDuplicate = { const BoardsMockData = { 'GET': { - '/test/issue-boards/board/1/lists{/id}/issues': { + '/test/boards/1{/id}/issues': { issues: [{ title: 'Testing', + id: 1, iid: 1, confidential: false, labels: [], assignees: [], }], - size: 1 } }, 'POST': { - '/test/issue-boards/board/1/lists{/id}': listObj + '/test/boards/1{/id}': listObj }, 'PUT': { '/test/issue-boards/board/1/lists{/id}': {} @@ -58,7 +59,22 @@ const boardsMockInterceptor = (request, next) => { })); }; +const mockBoardService = (opts = {}) => { + const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/board'; + const listsEndpoint = opts.listsEndpoint || '/test/boards/1'; + const bulkUpdatePath = opts.bulkUpdatePath || ''; + const boardId = opts.boardId || '1'; + + return new BoardService({ + boardsEndpoint, + listsEndpoint, + bulkUpdatePath, + boardId, + }); +}; + window.listObj = listObj; window.listObjDuplicate = listObjDuplicate; window.BoardsMockData = BoardsMockData; window.boardsMockInterceptor = boardsMockInterceptor; +window.mockBoardService = mockBoardService; diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 32e6d04df9f..7eecb58a4c3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -18,6 +18,7 @@ describe('Modal store', () => { issue = new ListIssue({ title: 'Testing', + id: 1, iid: 1, confidential: false, labels: [], @@ -25,6 +26,7 @@ describe('Modal store', () => { }); issue2 = new ListIssue({ title: 'Testing', + id: 1, iid: 2, confidential: false, labels: [], diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index a34cadec0ab..454f187ccbc 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -29,6 +29,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); }); @@ -64,6 +65,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); }); @@ -115,6 +117,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); element.appendChild(this.component.$el); @@ -136,6 +139,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 16ae649ee60..f209328dee1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -411,4 +411,26 @@ describe('Filtered Search Manager', () => { expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); }); }); + + describe('getAllParams', () => { + beforeEach(() => { + this.paramsArr = ['key=value', 'otherkey=othervalue']; + + initializeManager(); + }); + + it('correctly modifies params when custom modifier is passed', () => { + const modifedParams = manager.getAllParams.call({ + modifyUrlParams: paramsArr => paramsArr.reverse(), + }, [].concat(this.paramsArr)); + + expect(modifedParams[0]).toBe(this.paramsArr[1]); + }); + + it('does not modify params when no custom modifier is passed', () => { + const modifedParams = manager.getAllParams.call({}, this.paramsArr); + + expect(modifedParams[1]).toBe(this.paramsArr[1]); + }); + }); }); diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 4588bf3d971..f8b37c0edde 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -34,6 +34,8 @@ describe('Fly out sidebar navigation', () => { document.body.innerHTML = ''; breakpointSize = 'lg'; mousePos.length = 0; + + setSidebar(null); }); describe('calculateTop', () => { @@ -242,6 +244,32 @@ describe('Fly out sidebar navigation', () => { ).toBe('block'); }); + it('shows collapsed only sub-items if icon only sidebar', () => { + const subItems = el.querySelector('.sidebar-sub-level-items'); + const sidebar = document.createElement('div'); + sidebar.classList.add('sidebar-icons-only'); + subItems.classList.add('is-fly-out-only'); + + setSidebar(sidebar); + + showSubLevelItems(el); + + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); + }); + + it('does not show collapsed only sub-items if icon only sidebar', () => { + const subItems = el.querySelector('.sidebar-sub-level-items'); + subItems.classList.add('is-fly-out-only'); + + showSubLevelItems(el); + + expect( + subItems.style.display, + ).not.toBe('block'); + }); + it('sets transform of sub-items', () => { const subItems = el.querySelector('.sidebar-sub-level-items'); showSubLevelItems(el); @@ -283,10 +311,6 @@ describe('Fly out sidebar navigation', () => { }); describe('canShowActiveSubItems', () => { - afterEach(() => { - setSidebar(null); - }); - it('returns true by default', () => { expect( canShowActiveSubItems(el), diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 0c8c4d2cea6..60a452f2223 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -118,7 +118,7 @@ describe('Issue', function() { this.$triggeredButton = $btn; - this.$projectIssuesCounter = $('.issue_counter'); + this.$projectIssuesCounter = $('.issue_counter').first(); this.$projectIssuesCounter.text('1,001'); this.issueStateDeferred = new jQuery.Deferred(); diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js index da2fbd26e23..2571b7ef869 100644 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ b/spec/javascripts/monitoring/graph/legend_spec.js @@ -28,7 +28,7 @@ const defaultValuesComponent = { currentDataIndex: 0, }; -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight, defaultValuesComponent.graphHeightOffset); @@ -89,13 +89,12 @@ describe('GraphLegend', () => { expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); }); - it('contains text to signal the usage, title and time', () => { + it('contains text to signal the usage, title and time with multiple time series', () => { const component = createComponent(defaultValuesComponent); const titles = component.$el.querySelectorAll('.legend-metric-title'); - expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1); - expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1); - expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1); + expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); + expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel); }); diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/graph_path_spec.js index d39db945e17..a4844636d09 100644 --- a/spec/javascripts/monitoring/monitoring_paths_spec.js +++ b/spec/javascripts/monitoring/graph_path_spec.js @@ -1,10 +1,10 @@ import Vue from 'vue'; -import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue'; +import GraphPath from '~/monitoring/components/graph_path.vue'; import createTimeSeries from '~/monitoring/utils/multiple_time_series'; import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data'; const createComponent = (propsData) => { - const Component = Vue.extend(MonitoringPaths); + const Component = Vue.extend(GraphPath); return new Component({ propsData, @@ -13,22 +13,23 @@ const createComponent = (propsData) => { const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120); +const firstTimeSeries = timeSeries[0]; describe('Monitoring Paths', () => { it('renders two paths to represent a line and the area underneath it', () => { const component = createComponent({ - generatedLinePath: timeSeries[0].linePath, - generatedAreaPath: timeSeries[0].areaPath, - lineColor: '#ccc', - areaColor: '#fff', + generatedLinePath: firstTimeSeries.linePath, + generatedAreaPath: firstTimeSeries.areaPath, + lineColor: firstTimeSeries.lineColor, + areaColor: firstTimeSeries.areaColor, }); const metricArea = component.$el.querySelector('.metric-area'); const metricLine = component.$el.querySelector('.metric-line'); - expect(metricArea.getAttribute('fill')).toBe('#fff'); - expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath); - expect(metricLine.getAttribute('stroke')).toBe('#ccc'); - expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath); + expect(metricArea.getAttribute('fill')).toBe('#8fbce8'); + expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath); + expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); + expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 3d399f2bb95..7ceab657464 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -6346,7 +6346,13 @@ export const singleRowMetricsMultipleSeries = [ } ] }, - ] + ], + 'when': [ + { + 'value': 'hundred(s)', + 'color': 'green', + }, + ], } ] }, diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js index 3daf6bf82df..7e44a9ade9e 100644 --- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js +++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js @@ -2,16 +2,17 @@ import createTimeSeries from '~/monitoring/utils/multiple_time_series'; import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data'; const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120); +const firstTimeSeries = timeSeries[0]; describe('Multiple time series', () => { it('createTimeSeries returned array contains an object for each element', () => { - expect(typeof timeSeries[0].linePath).toEqual('string'); - expect(typeof timeSeries[0].areaPath).toEqual('string'); - expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function'); - expect(typeof timeSeries[0].areaColor).toEqual('string'); - expect(typeof timeSeries[0].lineColor).toEqual('string'); - expect(timeSeries[0].values instanceof Array).toEqual(true); + expect(typeof firstTimeSeries.linePath).toEqual('string'); + expect(typeof firstTimeSeries.areaPath).toEqual('string'); + expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function'); + expect(typeof firstTimeSeries.areaColor).toEqual('string'); + expect(typeof firstTimeSeries.lineColor).toEqual('string'); + expect(firstTimeSeries.values instanceof Array).toEqual(true); }); it('createTimeSeries returns an array', () => { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 8c5ad8914b0..3e791a31604 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -770,6 +770,20 @@ import '~/notes'; expect($tempNote.prop('nodeName')).toEqual('LI'); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); + + it('should return a escaped user name', () => { + const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>'; + const $tempNote = this.notes.createPlaceholderNote({ + formContent: sampleComment, + uniqueId, + isDiscussionNote: false, + currentUsername, + currentUserFullname: currentUserFullnameXSS, + currentUserAvatar, + }); + const $tempNoteHeader = $tempNote.find('.note-header'); + expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo <script>alert("XSS")</script>'); + }); }); describe('createPlaceholderSystemNote', () => { diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 3c4b20a5f06..256fdbe743c 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -16,6 +16,7 @@ describe('Pipeline Url Component', () => { path: 'foo', flags: {}, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -30,6 +31,7 @@ describe('Pipeline Url Component', () => { path: 'foo', flags: {}, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -50,6 +52,7 @@ describe('Pipeline Url Component', () => { path: '/', }, }, + autoDevopsHelpPath: 'foo', }; const component = new PipelineUrlComponent({ @@ -73,6 +76,7 @@ describe('Pipeline Url Component', () => { path: 'foo', flags: {}, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -91,6 +95,7 @@ describe('Pipeline Url Component', () => { stuck: true, }, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -98,4 +103,26 @@ describe('Pipeline Url Component', () => { expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); }); + + it('should render a badge for autodevops', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: { + latest: true, + yaml_errors: true, + stuck: true, + auto_devops: true, + }, + }, + autoDevopsHelpPath: 'foo', + }, + }).$mount(); + + expect( + component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(), + ).toEqual('Auto DevOps'); + }); }); diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 7ce39dca112..d7456a48bc1 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -9,7 +9,7 @@ describe('Pipelines Table Row', () => { el: document.querySelector('.test-dom-element'), propsData: { pipeline, - service: {}, + autoDevopsHelpPath: 'foo', }, }).$mount(); }; diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index 3afe89c8db4..4f5eb42eb35 100644 --- a/spec/javascripts/pipelines/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -22,6 +22,7 @@ describe('Pipelines Table', () => { component = new PipelinesTableComponent({ propsData: { pipelines: [], + autoDevopsHelpPath: 'foo', }, }).$mount(); }); @@ -47,6 +48,7 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [], + autoDevopsHelpPath: 'foo', }, }).$mount(); expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); @@ -58,6 +60,7 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [pipeline], + autoDevopsHelpPath: 'foo', }, }).$mount(); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js index 59fc2dedba5..67f8a8946c2 100644 --- a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js +++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js @@ -43,7 +43,7 @@ describe('ProjectsListSearchComponent', () => { expect(vm.listEmptyMessage).toBe('Something went wrong on our end.'); vm.searchFailed = false; - expect(vm.listEmptyMessage).toBe('No projects matched your query'); + expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search'); }); }); }); diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js index f2a23e33325..24d8a00b254 100644 --- a/spec/javascripts/projects_dropdown/components/search_spec.js +++ b/spec/javascripts/projects_dropdown/components/search_spec.js @@ -94,7 +94,7 @@ describe('SearchComponent', () => { expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy(); expect(inputEl).not.toBe(null); - expect(inputEl.getAttribute('placeholder')).toBe('Search projects'); + expect(inputEl.getAttribute('placeholder')).toBe('Search your projects'); expect(vm.$el.querySelector('.search-icon')).toBeDefined(); }); }); diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js index 28d0c7dcd99..69cb93bd850 100644 --- a/spec/javascripts/user_callout_spec.js +++ b/spec/javascripts/user_callout_spec.js @@ -33,4 +33,17 @@ describe('UserCallout', function () { this.userCalloutBtn.click(); expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); }); + + describe('Sets cookie with setCalloutPerProject', () => { + beforeEach(() => { + spyOn(Cookies, 'set').and.callFake(() => {}); + document.querySelector('.user-callout').setAttribute('data-project-path', 'foo/bar'); + this.userCallout = new UserCallout({ setCalloutPerProject: true }); + }); + + it('sets a cookie when the user clicks the close button', () => { + this.userCalloutBtn.click(); + expect(Cookies.set).toHaveBeenCalledWith('user_callout_dismissed', 'true', Object({ expires: 365, path: 'foo/bar' })); + }); + }); }); diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 35a32a46eff..01ceb21dfaa 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -49,7 +49,7 @@ describe Banzai::Filter::SanitizationFilter do instance = described_class.new('Foo') 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 4 + expect(instance.whitelist[:transformers].size).to eq 5 end it 'sanitizes `class` attribute from all elements' do @@ -63,8 +63,8 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq %q{<span>def</span>} end - it 'allows `style` attribute on table elements' do - html = <<-HTML.strip_heredoc + it 'allows `text-align` property in `style` attribute on table elements' do + html = <<~HTML <table> <tr><th style="text-align: center">Head</th></tr> <tr><td style="text-align: right">Body</th></tr> @@ -77,6 +77,20 @@ describe Banzai::Filter::SanitizationFilter do expect(doc.at_css('td')['style']).to eq 'text-align: right' end + it 'disallows other properties in `style` attribute on table elements' do + html = <<~HTML + <table> + <tr><th style="text-align: foo">Head</th></tr> + <tr><td style="position: fixed; height: 50px; width: 50px; background: red; z-index: 999; font-size: 36px; text-align: center">Body</th></tr> + </table> + HTML + + doc = filter(html) + + expect(doc.at_css('th')['style']).to be_nil + expect(doc.at_css('td')['style']).to eq 'text-align: center' + end + it 'allows `span` elements' do exp = act = %q{<span>Hello</span>} expect(filter(act).to_html).to eq exp @@ -87,6 +101,18 @@ describe Banzai::Filter::SanitizationFilter do expect(filter(act).to_html).to eq exp end + it 'disallows the `name` attribute globally' do + html = <<~HTML + <img name="getElementById" src=""> + <span name="foo" class="bar">Hi</span> + HTML + + doc = filter(html) + + expect(doc.at_css('img')).not_to have_attribute('name') + expect(doc.at_css('span')).not_to have_attribute('name') + end + it 'allows `summary` elements' do exp = act = '<summary>summary line</summary>' expect(filter(act).to_html).to eq exp diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index c56b08b18a2..cb52d971047 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event do +describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do describe '#commit_title' do it 'returns nil when there are no commits' do expect(described_class.new.commit_title).to be_nil @@ -215,9 +215,17 @@ end # to a specific version of the database where said table is still present. # describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do + let(:user_class) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + end + end + let(:migration) { described_class.new } - let(:project) { create(:project_empty_repo) } - let(:author) { create(:user) } + let(:user_class) { table(:users) } + let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } + let(:namespace) { create(:namespace, owner: author) } + let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } # We can not rely on FactoryGirl as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index c1ed47cf64a..7322a326b01 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -47,6 +47,18 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do end end + describe '.get_uuid' do + it 'gets the uuid if lease with the key associated exists' do + uuid = described_class.new(unique_key, timeout: 3600).try_obtain + + expect(described_class.get_uuid(unique_key)).to eq(uuid) + end + + it 'returns false if the lease does not exist' do + expect(described_class.get_uuid(unique_key)).to be false + end + end + describe '.cancel' do it 'can cancel a lease' do uuid = new_lease(unique_key) diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 921e786a55c..a9b861fcff2 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -102,6 +102,22 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) end end + + context "when a feature is not persisted" do + it 'returns false when opt_into_all_features is off' do + allow(Feature).to receive(:persisted?).and_return(false) + allow(described_class).to receive(:opt_into_all_features?).and_return(false) + + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) + end + + it 'returns true when the override is on' do + allow(Feature).to receive(:persisted?).and_return(false) + allow(described_class).to receive(:opt_into_all_features?).and_return(true) + + expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) + end + end end context 'when the feature_status is OPT_OUT' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index beed4e77e8b..3fb8edeb701 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -256,6 +256,7 @@ project: - environments - deployments - project_feature +- auto_devops - pages_domains - authorized_users - project_authorizations diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 331b7cf2fea..1115fb218d6 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -75,8 +75,6 @@ "id": 487, "target_type": "Milestone", "target_id": 1, - "title": null, - "data": null, "project_id": 46, "created_at": "2016-06-14T15:02:04.418Z", "updated_at": "2016-06-14T15:02:04.418Z", @@ -364,8 +362,6 @@ "id": 487, "target_type": "Milestone", "target_id": 1, - "title": null, - "data": null, "project_id": 46, "created_at": "2016-06-14T15:02:04.418Z", "updated_at": "2016-06-14T15:02:04.418Z", @@ -2311,8 +2307,6 @@ "id": 487, "target_type": "Milestone", "target_id": 1, - "title": null, - "data": null, "project_id": 46, "created_at": "2016-06-14T15:02:04.418Z", "updated_at": "2016-06-14T15:02:04.418Z", @@ -2336,8 +2330,6 @@ "id": 240, "target_type": "Milestone", "target_id": 20, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:04.593Z", "updated_at": "2016-06-14T15:02:04.593Z", @@ -2348,8 +2340,6 @@ "id": 60, "target_type": "Milestone", "target_id": 20, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:04.593Z", "updated_at": "2016-06-14T15:02:04.593Z", @@ -2373,8 +2363,6 @@ "id": 241, "target_type": "Milestone", "target_id": 19, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:04.585Z", "updated_at": "2016-06-14T15:02:04.585Z", @@ -2385,41 +2373,6 @@ "id": 59, "target_type": "Milestone", "target_id": 19, - "title": null, - "data": { - "object_kind": "push", - "before": "0000000000000000000000000000000000000000", - "after": "de990aa15829d0ab182ad5a55b4c527846c0d39c", - "ref": "refs/heads/removable-group-owner", - "checkout_sha": "de990aa15829d0ab182ad5a55b4c527846c0d39c", - "message": null, - "user_id": 273486, - "user_name": "James Lopez", - "user_email": "james@jameslopez.es", - "project_id": 562317, - "repository": { - "name": "GitLab Community Edition", - "url": "git@gitlab.com:james11/gitlab-ce.git", - "description": "Version Control on your Server. See http://gitlab.org/gitlab-ce/ and the README for more information", - "homepage": "https://gitlab.com/james11/gitlab-ce", - "git_http_url": "https://gitlab.com/james11/gitlab-ce.git", - "git_ssh_url": "git@gitlab.com:james11/gitlab-ce.git", - "visibility_level": 20 - }, - "commits": [ - { - "id": "de990aa15829d0ab182ad5a55b4c527846c0d39c", - "message": "fixed last group owner issue and added test\\n", - "timestamp": "2015-10-29T16:10:27+00:00", - "url": "https://gitlab.com/james11/gitlab-ce/commit/de990aa15829d0ab182ad5a55b4c527846c0d39c", - "author": { - "name": "James Lopez", - "email": "james.lopez@vodafone.com" - } - } - ], - "total_commits_count": 1 - }, "project_id": 5, "created_at": "2016-06-14T15:02:04.585Z", "updated_at": "2016-06-14T15:02:04.585Z", @@ -2947,8 +2900,6 @@ "id": 221, "target_type": "MergeRequest", "target_id": 27, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:36.703Z", "updated_at": "2016-06-14T15:02:36.703Z", @@ -2959,8 +2910,6 @@ "id": 187, "target_type": "MergeRequest", "target_id": 27, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:36.703Z", "updated_at": "2016-06-14T15:02:36.703Z", @@ -3230,8 +3179,6 @@ "id": 222, "target_type": "MergeRequest", "target_id": 26, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:36.496Z", "updated_at": "2016-06-14T15:02:36.496Z", @@ -3242,8 +3189,6 @@ "id": 186, "target_type": "MergeRequest", "target_id": 26, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:36.496Z", "updated_at": "2016-06-14T15:02:36.496Z", @@ -3513,8 +3458,6 @@ "id": 223, "target_type": "MergeRequest", "target_id": 15, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:25.262Z", "updated_at": "2016-06-14T15:02:25.262Z", @@ -3525,8 +3468,6 @@ "id": 175, "target_type": "MergeRequest", "target_id": 15, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:25.262Z", "updated_at": "2016-06-14T15:02:25.262Z", @@ -4202,8 +4143,6 @@ "id": 224, "target_type": "MergeRequest", "target_id": 14, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:25.113Z", "updated_at": "2016-06-14T15:02:25.113Z", @@ -4214,8 +4153,6 @@ "id": 174, "target_type": "MergeRequest", "target_id": 14, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:25.113Z", "updated_at": "2016-06-14T15:02:25.113Z", @@ -4274,9 +4211,7 @@ { "id": 529, "target_type": "Note", - "target_id": 2521, - "title": "test levels", - "data": null, + "target_id": 793, "project_id": 4, "created_at": "2016-07-07T14:35:12.128Z", "updated_at": "2016-07-07T14:35:12.128Z", @@ -4749,8 +4684,6 @@ "id": 225, "target_type": "MergeRequest", "target_id": 13, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:24.636Z", "updated_at": "2016-06-14T15:02:24.636Z", @@ -4761,8 +4694,6 @@ "id": 173, "target_type": "MergeRequest", "target_id": 13, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:24.636Z", "updated_at": "2016-06-14T15:02:24.636Z", @@ -5247,8 +5178,6 @@ "id": 226, "target_type": "MergeRequest", "target_id": 12, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:24.253Z", "updated_at": "2016-06-14T15:02:24.253Z", @@ -5259,8 +5188,6 @@ "id": 172, "target_type": "MergeRequest", "target_id": 12, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:24.253Z", "updated_at": "2016-06-14T15:02:24.253Z", @@ -5506,8 +5433,6 @@ "id": 227, "target_type": "MergeRequest", "target_id": 11, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:23.865Z", "updated_at": "2016-06-14T15:02:23.865Z", @@ -5518,8 +5443,6 @@ "id": 171, "target_type": "MergeRequest", "target_id": 11, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:23.865Z", "updated_at": "2016-06-14T15:02:23.865Z", @@ -6195,8 +6118,6 @@ "id": 228, "target_type": "MergeRequest", "target_id": 10, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:23.660Z", "updated_at": "2016-06-14T15:02:23.660Z", @@ -6207,8 +6128,6 @@ "id": 170, "target_type": "MergeRequest", "target_id": 10, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:23.660Z", "updated_at": "2016-06-14T15:02:23.660Z", @@ -6478,8 +6397,6 @@ "id": 229, "target_type": "MergeRequest", "target_id": 9, - "title": null, - "data": null, "project_id": 36, "created_at": "2016-06-14T15:02:22.927Z", "updated_at": "2016-06-14T15:02:22.927Z", @@ -6490,8 +6407,6 @@ "id": 169, "target_type": "MergeRequest", "target_id": 9, - "title": null, - "data": null, "project_id": 5, "created_at": "2016-06-14T15:02:22.927Z", "updated_at": "2016-06-14T15:02:22.927Z", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index d664d371028..efe11ca794a 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -57,10 +57,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(Ci::Pipeline.where(ref: nil)).not_to be_empty end - it 'restores the correct event with symbolised data' do - expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty - end - it 'preserves updated_at on issues' do issue = Issue.where(description: 'Aliquam enim illo et possimus.').first @@ -80,7 +76,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'event at forth level of the tree' do - let(:event) { Event.where(title: 'test levels').first } + let(:event) { Event.where(action: 6).first } it 'restores the event' do expect(event).not_to be_nil diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 122b8ee0314..899d17d97c2 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -29,8 +29,6 @@ Event: - id - target_type - target_id -- title -- data - project_id - created_at - updated_at @@ -225,6 +223,7 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id +- config_source - protected Ci::Stage: - id @@ -468,3 +467,10 @@ Timelog: - user_id - created_at - updated_at +ProjectAutoDevops: +- id +- enabled +- domain +- project_id +- created_at +- updated_at diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 6af1564da19..cab662819ac 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -79,12 +79,28 @@ describe Gitlab::Middleware::Go do it_behaves_like 'a nested project' end + context 'with a subpackage that is not a valid project path' do + let(:path) { "#{project.full_path}/---subpackage" } + + it_behaves_like 'a nested project' + end + context 'without subpackages' do let(:path) { project.full_path } it_behaves_like 'a nested project' end end + + context 'with a bogus path' do + let(:path) { "http:;url=http://www.example.com'http-equiv='refresh'x='?go-get=1" } + + it 'skips go-import generation' do + expect(app).to receive(:call).and_return('no-go') + + go + end + end end def go @@ -100,7 +116,7 @@ describe Gitlab::Middleware::Go do def expect_response_with_path(response, path) expect(response[0]).to eq(200) expect(response[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n" + expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>} expect(response[2].body).to eq([expected_body]) end end diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 00000000000..ecacea6bb35 --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Themes, lib: true do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_indigo') + expect(css).to include(' ui_dark ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Indigo' + expect(described_class.by_id(3).name).to eq 'Light' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + end + end +end diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb index 1396d12e5a9..759e77ac9db 100644 --- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns') describe ConvertCustomNotificationSettingsToColumns, :migration do + let(:user_class) { table(:users) } + let(:settings_params) do [ { level: 0, events: [:new_note] }, # disabled, single event @@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = { user_id: user.id, level: params[:level], events: events } notification_setting = described_class::NotificationSetting.create(create_params) @@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = events.merge(user_id: user.id, level: params[:level]) notification_setting = described_class::NotificationSetting.create(create_params) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f921545668d..c7a9eabdf06 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -5,6 +5,7 @@ describe ApplicationSetting do it { expect(setting).to be_valid } it { expect(setting.uuid).to be_present } + it { expect(setting).to have_db_column(:auto_devops_enabled) } describe 'validations' do let(:http) { 'http://example.com' } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 08d22f166e4..c2c9f1c12d1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1688,6 +1688,30 @@ describe Ci::Build do { key: 'secret', value: 'value', public: false }]) end end + + context 'when using auto devops' do + context 'and is enabled' do + before do + project.create_auto_devops!(enabled: true, domain: 'example.com') + end + + it "includes AUTO_DEVOPS_DOMAIN" do + is_expected.to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + + context 'and is disabled' do + before do + project.create_auto_devops!(enabled: false, domain: 'example.com') + end + + it "includes AUTO_DEVOPS_DOMAIN" do + is_expected.not_to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + end end describe 'state transition: any => [:pending]' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 84656ffe0b9..95da97b7bc5 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -799,14 +799,118 @@ describe Ci::Pipeline, :mailer do end end + describe '#set_config_source' do + context 'on object initialisation' do + context 'when pipelines does not contain needed data' do + let(:pipeline) do + Ci::Pipeline.new + end + + it 'defines source to be unknown' do + expect(pipeline).to be_unknown_source + end + end + + context 'when pipeline contains all needed data' do + let(:pipeline) do + Ci::Pipeline.new( + project: project, + sha: '1234', + ref: 'master', + source: :push) + end + + context 'when the repository has a config file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') + end + + it 'defines source to be from repository' do + expect(pipeline).to be_repository_source + end + + context 'when loading an object' do + let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) } + + it 'does not redefine the source' do + # force to overwrite the source + pipeline.unknown_source! + + expect(new_pipeline).to be_unknown_source + end + end + end + + context 'when the repository does not have a config file' do + let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } + + context 'auto devops enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + allow(project).to receive(:ci_config_path) { 'custom' } + end + + it 'defines source to be auto devops' do + subject + + expect(pipeline).to be_auto_devops_source + end + end + end + end + end + end + describe '#ci_yaml_file' do - it 'reports error if the file is not found' do - allow(pipeline.project).to receive(:ci_config_path) { 'custom' } + let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } + + context 'the source is unknown' do + before do + pipeline.unknown_source! + end + + it 'returns the configuration if found' do + allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') + + expect(pipeline.ci_yaml_file).to be_a(String) + expect(pipeline.ci_yaml_file).not_to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil + end + + it 'sets yaml errors if not found' do + expect(pipeline.ci_yaml_file).to be_nil + expect(pipeline.yaml_errors) + .to start_with('Failed to load CI/CD config file') + end + end + + context 'the source is the repository' do + before do + pipeline.repository_source! + end - pipeline.ci_yaml_file + it 'returns the configuration if found' do + allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') - expect(pipeline.yaml_errors) - .to eq('Failed to load CI/CD config file at custom') + expect(pipeline.ci_yaml_file).to be_a(String) + expect(pipeline.ci_yaml_file).not_to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil + end + end + + context 'when the source is auto_devops_source' do + before do + stub_application_setting(auto_devops_enabled: true) + pipeline.auto_devops_source! + end + + it 'finds the implied config' do + expect(pipeline.ci_yaml_file).to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil + end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index ea8512a5eae..25e5d155894 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -54,6 +54,28 @@ describe Environment do end end + describe '#folder_name' do + context 'when it is inside a folder' do + subject(:environment) do + create(:environment, name: 'staging/review-1') + end + + it 'returns a top-level folder name' do + expect(environment.folder_name).to eq 'staging' + end + end + + context 'when the environment if a top-level item itself' do + subject(:environment) do + create(:environment, name: 'production') + end + + it 'returns an environment name' do + expect(environment.folder_name).to eq 'production' + end + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index f55c161c821..aa7a8342a4c 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -11,7 +11,6 @@ describe Event do it { is_expected.to respond_to(:author_email) } it { is_expected.to respond_to(:issue_title) } it { is_expected.to respond_to(:merge_request_title) } - it { is_expected.to respond_to(:commits) } end describe 'Callbacks' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 69286eff984..81d5ab7a6d3 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -404,6 +404,118 @@ describe Namespace do let!(:project1) { create(:project_empty_repo, namespace: group) } let!(:project2) { create(:project_empty_repo, namespace: child) } - it { expect(group.all_projects.to_a).to eq([project2, project1]) } + it { expect(group.all_projects.to_a).to match_array([project2, project1]) } + end + + describe '#share_with_group_lock with subgroups', :nested_groups do + context 'when creating a subgroup' do + let(:subgroup) { create(:group, parent: root_group )} + + context 'under a parent with "Share with group lock" enabled' do + let(:root_group) { create(:group, share_with_group_lock: true) } + + it 'enables "Share with group lock" on the subgroup' do + expect(subgroup.share_with_group_lock).to be_truthy + end + end + + context 'under a parent with "Share with group lock" disabled' do + let(:root_group) { create(:group) } + + it 'does not enable "Share with group lock" on the subgroup' do + expect(subgroup.share_with_group_lock).to be_falsey + end + end + end + + context 'when enabling the parent group "Share with group lock"' do + let(:root_group) { create(:group) } + let!(:subgroup) { create(:group, parent: root_group )} + + it 'the subgroup "Share with group lock" becomes enabled' do + root_group.update!(share_with_group_lock: true) + + expect(subgroup.reload.share_with_group_lock).to be_truthy + end + end + + context 'when disabling the parent group "Share with group lock" (which was already enabled)' do + let(:root_group) { create(:group, share_with_group_lock: true) } + + context 'and the subgroup "Share with group lock" is enabled' do + let(:subgroup) { create(:group, parent: root_group, share_with_group_lock: true )} + + it 'the subgroup "Share with group lock" does not change' do + root_group.update!(share_with_group_lock: false) + + expect(subgroup.reload.share_with_group_lock).to be_truthy + end + end + + context 'but the subgroup "Share with group lock" is disabled' do + let(:subgroup) { create(:group, parent: root_group )} + + it 'the subgroup "Share with group lock" does not change' do + root_group.update!(share_with_group_lock: false) + + expect(subgroup.reload.share_with_group_lock?).to be_falsey + end + end + end + + # Note: Group transfers are not yet implemented + context 'when a group is transferred into a root group' do + context 'when the root group "Share with group lock" is enabled' do + let(:root_group) { create(:group, share_with_group_lock: true) } + + context 'when the subgroup "Share with group lock" is enabled' do + let(:subgroup) { create(:group, share_with_group_lock: true )} + + it 'the subgroup "Share with group lock" does not change' do + subgroup.parent = root_group + subgroup.save! + + expect(subgroup.share_with_group_lock).to be_truthy + end + end + + context 'when the subgroup "Share with group lock" is disabled' do + let(:subgroup) { create(:group)} + + it 'the subgroup "Share with group lock" becomes enabled' do + subgroup.parent = root_group + subgroup.save! + + expect(subgroup.share_with_group_lock).to be_truthy + end + end + end + + context 'when the root group "Share with group lock" is disabled' do + let(:root_group) { create(:group) } + + context 'when the subgroup "Share with group lock" is enabled' do + let(:subgroup) { create(:group, share_with_group_lock: true )} + + it 'the subgroup "Share with group lock" does not change' do + subgroup.parent = root_group + subgroup.save! + + expect(subgroup.share_with_group_lock).to be_truthy + end + end + + context 'when the subgroup "Share with group lock" is disabled' do + let(:subgroup) { create(:group)} + + it 'the subgroup "Share with group lock" does not change' do + subgroup.parent = root_group + subgroup.save! + + expect(subgroup.share_with_group_lock).to be_falsey + end + end + end + end end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb new file mode 100644 index 00000000000..ca13af4d73e --- /dev/null +++ b/spec/models/project_auto_devops_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe ProjectAutoDevops do + set(:project) { build(:project) } + + it { is_expected.to belong_to(:project) } + + it { is_expected.to respond_to(:created_at) } + it { is_expected.to respond_to(:updated_at) } + + describe 'variables' do + let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) } + + context 'when domain is defined' do + let(:domain) { 'example.com' } + + it 'returns AUTO_DEVOPS_DOMAIN' do + expect(auto_devops.variables).to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1f7c6a82b91..48fc77423ff 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -53,6 +53,7 @@ describe Project do it { is_expected.to have_one(:import_data).class_name('ProjectImportData') } it { is_expected.to have_one(:last_event).class_name('Event') } it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) } + it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') } it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:builds) } @@ -2508,4 +2509,177 @@ describe Project do end end end + + describe '#has_ci?' do + set(:project) { create(:project) } + let(:repository) { double } + + before do + expect(project).to receive(:repository) { repository } + end + + context 'when has .gitlab-ci.yml' do + before do + expect(repository).to receive(:gitlab_ci_yml) { 'content' } + end + + it "CI is available" do + expect(project).to have_ci + end + end + + context 'when there is no .gitlab-ci.yml' do + before do + expect(repository).to receive(:gitlab_ci_yml) { nil } + end + + it "CI is not available" do + expect(project).not_to have_ci + end + + context 'when auto devops is enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it "CI is available" do + expect(project).to have_ci + end + end + end + end + + describe '#auto_devops_enabled?' do + set(:project) { create(:project) } + + subject { project.auto_devops_enabled? } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it 'auto devops is implicitly enabled' do + expect(project.auto_devops).to be_nil + expect(project).to be_auto_devops_enabled + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it "auto devops is enabled" do + expect(project).to be_auto_devops_enabled + end + end + + context 'when explicitly disabled' do + before do + create(:project_auto_devops, project: project, enabled: false) + end + + it "auto devops is disabled" do + expect(project).not_to be_auto_devops_enabled + end + end + end + + context 'when disabled in settings' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it 'auto devops is implicitly disabled' do + expect(project.auto_devops).to be_nil + expect(project).not_to be_auto_devops_enabled + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it "auto devops is enabled" do + expect(project).to be_auto_devops_enabled + end + end + end + end + + describe '#has_auto_devops_implicitly_disabled?' do + set(:project) { create(:project) } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end + + context 'when disabled in settings' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it 'auto devops is implicitly disabled' do + expect(project).to have_auto_devops_implicitly_disabled + end + + context 'when explicitly disabled' do + before do + create(:project_auto_devops, project: project, enabled: false) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end + end + end + + context '#auto_devops_variables' do + set(:project) { create(:project) } + + subject { project.auto_devops_variables } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + context 'when domain is empty' do + before do + create(:project_auto_devops, project: project, domain: nil) + end + + it 'variables are empty' do + is_expected.to be_empty + end + end + + context 'when domain is configured' do + before do + create(:project_auto_devops, project: project, domain: 'example.com') + end + + it "variables are not empty" do + is_expected.not_to be_empty + end + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index abf732e60bf..73a1e47149c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -716,6 +716,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end @@ -726,6 +727,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(1) end end @@ -1517,7 +1519,7 @@ describe User do developer_project = create(:project) { |p| p.add_developer(user) } master_project = create(:project) { |p| p.add_master(user) } - expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project]) + expect(user.projects_where_can_admin_issues.to_a).to match_array([master_project, developer_project, reporter_project]) expect(user.can?(:admin_issue, master_project)).to eq(true) expect(user.can?(:admin_issue, developer_project)).to eq(true) expect(user.can?(:admin_issue, reporter_project)).to eq(true) diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 7f832bfa563..b186a78e44a 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -24,8 +24,8 @@ describe GroupPolicy do :admin_namespace, :admin_group_member, :change_visibility_level, - :create_subgroup - ] + (Gitlab::Database.postgresql? ? :create_subgroup : nil) + ].compact end before do @@ -242,4 +242,94 @@ describe GroupPolicy do end end end + + describe 'change_share_with_group_lock' do + context 'when the current_user owns the group' do + let(:current_user) { owner } + + context 'when the group share_with_group_lock is enabled' do + let(:group) { create(:group, share_with_group_lock: true, parent: parent) } + + context 'when the parent group share_with_group_lock is enabled' do + context 'when the group has a grandparent' do + let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) } + + context 'when the grandparent share_with_group_lock is enabled' do + let(:grandparent) { create(:group, share_with_group_lock: true) } + + context 'when the current_user owns the parent' do + before do + parent.add_owner(current_user) + end + + context 'when the current_user owns the grandparent' do + before do + grandparent.add_owner(current_user) + end + + it { expect_allowed(:change_share_with_group_lock) } + end + + context 'when the current_user does not own the grandparent' do + it { expect_disallowed(:change_share_with_group_lock) } + end + end + + context 'when the current_user does not own the parent' do + it { expect_disallowed(:change_share_with_group_lock) } + end + end + + context 'when the grandparent share_with_group_lock is disabled' do + let(:grandparent) { create(:group) } + + context 'when the current_user owns the parent' do + before do + parent.add_owner(current_user) + end + + it { expect_allowed(:change_share_with_group_lock) } + end + + context 'when the current_user does not own the parent' do + it { expect_disallowed(:change_share_with_group_lock) } + end + end + end + + context 'when the group does not have a grandparent' do + let(:parent) { create(:group, share_with_group_lock: true) } + + context 'when the current_user owns the parent' do + before do + parent.add_owner(current_user) + end + + it { expect_allowed(:change_share_with_group_lock) } + end + + context 'when the current_user does not own the parent' do + it { expect_disallowed(:change_share_with_group_lock) } + end + end + end + + context 'when the parent group share_with_group_lock is disabled' do + let(:parent) { create(:group) } + + it { expect_allowed(:change_share_with_group_lock) } + end + end + + context 'when the group share_with_group_lock is disabled' do + it { expect_allowed(:change_share_with_group_lock) } + end + end + + context 'when the current_user does not own the group' do + let(:current_user) { create(:user) } + + it { expect_disallowed(:change_share_with_group_lock) } + end + end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 77c43f92456..42f0079e173 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -431,6 +431,30 @@ describe API::Groups do expect(response).to have_http_status(403) end + + context 'as owner', :nested_groups do + before do + group2.add_owner(user1) + end + + it 'can create subgroups' do + post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo' + + expect(response).to have_http_status(201) + end + end + + context 'as master', :nested_groups do + before do + group2.add_master(user1) + end + + it 'cannot create subgroups' do + post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo' + + expect(response).to have_http_status(403) + end + end end context "when authenticated as user with group permissions" do diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index d1b22179888..d9da94d4713 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -14,7 +14,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do it 'returns 200 for a valid merge request' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user) - merge_request_diff = merge_request.merge_request_diffs.first + merge_request_diff = merge_request.merge_request_diffs.last expect(response.status).to eq 200 expect(response).to include_pagination_headers diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f771e4fa4ff..9602584f546 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -595,7 +595,7 @@ describe API::Projects do expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1) expect(response).to have_http_status(201) - project = Project.first + project = Project.last expect(project.name).to eq('Foo Project') expect(project.path).to eq('foo-project') @@ -606,7 +606,7 @@ describe API::Projects do .to change { Project.count }.by(1) expect(response).to have_http_status(201) - project = Project.first + project = Project.last expect(project.name).to eq('Foo Project') expect(project.path).to eq('path-project-Foo') diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb index 8020ddab4c8..3f21ff40726 100644 --- a/spec/requests/api/v3/merge_request_diffs_spec.rb +++ b/spec/requests/api/v3/merge_request_diffs_spec.rb @@ -14,7 +14,7 @@ describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do it 'returns 200 for a valid merge request' do get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) - merge_request_diff = merge_request.merge_request_diffs.first + merge_request_diff = merge_request.merge_request_diffs.last expect(response.status).to eq 200 expect(json_response.size).to eq(merge_request.merge_request_diffs.size) diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index e5d9d3df5a8..6667ce771bd 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -93,7 +93,7 @@ describe 'cycle analytics events' do context 'with private project and builds' do before do - project.members.first.update(access_level: Gitlab::Access::GUEST) + project.members.last.update(access_level: Gitlab::Access::GUEST) end it 'does not list the test events' do diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index 979d9921941..8f32c5639a1 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -16,6 +16,10 @@ describe EnvironmentEntity do expect(subject).to include(:id, :name, :state, :environment_path) end + it 'exposes folder path' do + expect(subject).to include(:folder_path) + end + context 'metrics disabled' do before do allow(environment).to receive(:has_metrics?).and_return(false) diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 881f2b6bfd8..f8df461bc81 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -36,7 +36,7 @@ describe PipelineEntity do it 'contains flags' do expect(subject).to include :flags expect(subject[:flags]) - .to include :latest, :stuck, + .to include :latest, :stuck, :auto_devops, :yaml_errors, :retryable, :cancelable end end diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb index f2ddaa903da..1a56164dba4 100644 --- a/spec/services/boards/issues/create_service_spec.rb +++ b/spec/services/boards/issues/create_service_spec.rb @@ -8,7 +8,7 @@ describe Boards::Issues::CreateService do let(:label) { create(:label, project: project, name: 'in-progress') } let!(:list) { create(:list, board: board, label: label, position: 0) } - subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') } + subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') } before do project.team << [user, :developer] diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 63dfe80d672..464ff9f94b3 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -98,7 +98,7 @@ describe Boards::Issues::MoveService do issue.move_to_end && issue.save! end - params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid) + params.merge!(move_after_id: issue1.id, move_before_id: issue2.id) described_class.new(project, user, params).execute(issue) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 49d7c663128..4c2ff08039c 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreatePipelineService do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } let(:user) { create(:admin) } let(:ref_name) { 'refs/heads/master' } @@ -489,7 +489,7 @@ describe Ci::CreatePipelineService do subject do described_class.new(project, user, ref: ref) - .send(:allowed_to_create?, user) + .send(:allowed_to_create?) end context 'when user is a developer' do diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 10dda45d2a1..224e933bebc 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -22,6 +22,26 @@ describe Groups::CreateService, '#execute' do end end + describe 'creating a top level group' do + let(:service) { described_class.new(user, group_params) } + + context 'when user can create a group' do + before do + user.update_attribute(:can_create_group, true) + end + + it { is_expected.to be_persisted } + end + + context 'when user can not create a group' do + before do + user.update_attribute(:can_create_group, false) + end + + it { is_expected.not_to be_persisted } + end + end + describe 'creating subgroup', :nested_groups do let!(:group) { create(:group) } let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } @@ -44,13 +64,26 @@ describe Groups::CreateService, '#execute' do end end - context 'as guest' do - it 'does not save group and returns an error' do + context 'when nested groups feature is enabled' do + before do allow(Group).to receive(:supports_nested_groups?).and_return(true) + end + + context 'as guest' do + it 'does not save group and returns an error' do + is_expected.not_to be_persisted + + expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.') + expect(subject.parent_id).to be_nil + end + end + + context 'as owner' do + before do + group.add_owner(user) + end - is_expected.not_to be_persisted - expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.') - expect(subject.parent_id).to be_nil + it { is_expected.to be_persisted } end end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 44f22a3b37b..1737fd0a9fc 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -100,4 +100,38 @@ describe Groups::UpdateService do end end end + + context 'for a subgroup', :nested_groups do + let(:subgroup) { create(:group, :private, parent: private_group) } + + context 'when the parent group share_with_group_lock is enabled' do + before do + private_group.update_column(:share_with_group_lock, true) + end + + context 'for the parent group owner' do + it 'allows disabling share_with_group_lock' do + private_group.add_owner(user) + + result = described_class.new(subgroup, user, share_with_group_lock: false).execute + + expect(result).to be_truthy + expect(subgroup.reload.share_with_group_lock).to be_falsey + end + end + + context 'for a subgroup owner (who does not own the parent)' do + it 'does not allow disabling share_with_group_lock' do + subgroup_owner = create(:user) + subgroup.add_owner(subgroup_owner) + + result = described_class.new(subgroup, subgroup_owner, share_with_group_lock: false).execute + + expect(result).to be_falsey + expect(subgroup.errors.full_messages.first).to match(/cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group/) + expect(subgroup.reload.share_with_group_lock).to be_truthy + end + end + end + end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 85f46838351..15a50b85f19 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -80,7 +80,7 @@ describe Issues::UpdateService, :mailer do issue.save end - opts[:move_between_iids] = [issue1.iid, issue2.iid] + opts[:move_between_ids] = [issue1.id, issue2.id] update_issue(opts) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 5b349017c8b..3e493148b32 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1237,7 +1237,7 @@ describe NotificationService, :mailer do end it do - group_member = group.members.first + group_member = group.members.last expect do notification.decline_group_invite(group_member) @@ -1285,7 +1285,7 @@ describe NotificationService, :mailer do end it do - project_member = project.members.first + project_member = project.members.last expect do notification.decline_project_invite(project_member) diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 9386c110385..437c009e7fa 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -20,6 +20,7 @@ describe Projects::HousekeepingService do expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid) subject.execute + expect(project.reload.pushes_since_gc).to eq(0) end diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb index 4218c15a3ce..28dfa9cf59c 100644 --- a/spec/services/test_hooks/project_service_spec.rb +++ b/spec/services/test_hooks/project_service_spec.rb @@ -21,6 +21,7 @@ describe TestHooks::ProjectService do context 'push_events' do let(:trigger) { 'push_events' } + let(:trigger_key) { :push_hooks } it 'returns error message if not enough data' do allow(project).to receive(:empty_repo?).and_return(true) @@ -33,13 +34,14 @@ describe TestHooks::ProjectService do allow(project).to receive(:empty_repo?).and_return(false) allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'tag_push_events' do let(:trigger) { 'tag_push_events' } + let(:trigger_key) { :tag_push_hooks } it 'returns error message if not enough data' do allow(project).to receive(:empty_repo?).and_return(true) @@ -52,13 +54,14 @@ describe TestHooks::ProjectService do allow(project).to receive(:empty_repo?).and_return(false) allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'note_events' do let(:trigger) { 'note_events' } + let(:trigger_key) { :note_hooks } it 'returns error message if not enough data' do expect(hook).not_to receive(:execute) @@ -69,13 +72,14 @@ describe TestHooks::ProjectService do allow(project).to receive(:notes).and_return([Note.new]) allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'issues_events' do let(:trigger) { 'issues_events' } + let(:trigger_key) { :issue_hooks } let(:issue) { build(:issue) } it 'returns error message if not enough data' do @@ -87,13 +91,14 @@ describe TestHooks::ProjectService do allow(project).to receive(:issues).and_return([issue]) allow(issue).to receive(:to_hook_data).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'confidential_issues_events' do let(:trigger) { 'confidential_issues_events' } + let(:trigger_key) { :confidential_issue_hooks } let(:issue) { build(:issue) } it 'returns error message if not enough data' do @@ -105,13 +110,14 @@ describe TestHooks::ProjectService do allow(project).to receive(:issues).and_return([issue]) allow(issue).to receive(:to_hook_data).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'merge_requests_events' do let(:trigger) { 'merge_requests_events' } + let(:trigger_key) { :merge_request_hooks } it 'returns error message if not enough data' do expect(hook).not_to receive(:execute) @@ -122,13 +128,14 @@ describe TestHooks::ProjectService do create(:merge_request, source_project: project) allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'job_events' do let(:trigger) { 'job_events' } + let(:trigger_key) { :job_hooks } it 'returns error message if not enough data' do expect(hook).not_to receive(:execute) @@ -139,13 +146,14 @@ describe TestHooks::ProjectService do create(:ci_build, project: project) allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'pipeline_events' do let(:trigger) { 'pipeline_events' } + let(:trigger_key) { :pipeline_hooks } it 'returns error message if not enough data' do expect(hook).not_to receive(:execute) @@ -156,13 +164,14 @@ describe TestHooks::ProjectService do create(:ci_empty_pipeline, project: project) allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'wiki_page_events' do let(:trigger) { 'wiki_page_events' } + let(:trigger_key) { :wiki_page_hooks } it 'returns error message if wiki disabled' do allow(project).to receive(:wiki_enabled?).and_return(false) @@ -180,7 +189,7 @@ describe TestHooks::ProjectService do create(:wiki_page, wiki: project.wiki) allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb index a15708bf82f..ff8b9595538 100644 --- a/spec/services/test_hooks/system_service_spec.rb +++ b/spec/services/test_hooks/system_service_spec.rb @@ -24,36 +24,39 @@ describe TestHooks::SystemService do context 'push_events' do let(:trigger) { 'push_events' } + let(:trigger_key) { :push_hooks } it 'executes hook' do allow(project).to receive(:empty_repo?).and_return(false) expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original - expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'tag_push_events' do let(:trigger) { 'tag_push_events' } + let(:trigger_key) { :tag_push_hooks } it 'executes hook' do allow(project.repository).to receive(:tags).and_return(['tag']) expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original - expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end context 'repository_update_events' do let(:trigger) { 'repository_update_events' } + let(:trigger_key) { :repository_update_hooks } it 'executes hook' do allow(project).to receive(:empty_repo?).and_return(false) expect(Gitlab::DataBuilder::Repository).to receive(:sample_data).and_call_original - expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger).and_return(success_result) + expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key).and_return(success_result) expect(service.execute).to include(success_result) end end diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 0726e135b20..a669429ce3e 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -12,7 +12,7 @@ describe WebHookService do let(:data) do { before: 'oldrev', after: 'newrev', ref: 'ref' } end - let(:service_instance) { described_class.new(project_hook, data, 'push_hooks') } + let(:service_instance) { described_class.new(project_hook, data, :push_hooks) } describe '#execute' do before do diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index 4eec3127464..b23d81a226a 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -140,9 +140,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do end context "when a namespace with the provider user's username already exists" do - let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) } + let!(:existing_namespace) { user.namespace } context "when the namespace is owned by the GitLab user" do + before do + user.username = other_username + user.save + end + it "takes the existing namespace" do expect(Gitlab::GithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider) @@ -153,12 +158,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do end context "when the namespace is not owned by the GitLab user" do - before do - existing_namespace.owner = create(:user) - existing_namespace.save - end - it "creates a project using user's namespace" do + create(:user, username: other_username) + expect(Gitlab::GithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) .and_return(double(execute: true)) diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index cd55d63125e..688175369ae 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index cd55d63125e..ce8dfe5ae75 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} +}
\ No newline at end of file diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb new file mode 100644 index 00000000000..38cfb84f0d5 --- /dev/null +++ b/spec/views/groups/edit.html.haml_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe 'groups/edit.html.haml' do + include Devise::Test::ControllerHelpers + + describe '"Share with group lock" setting' do + let(:root_owner) { create(:user) } + let(:root_group) { create(:group) } + + before do + root_group.add_owner(root_owner) + end + + shared_examples_for '"Share with group lock" setting' do |checkbox_options| + it 'should have the correct label, help text, and checkbox options' do + assign(:group, test_group) + allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true) + allow(view).to receive(:can_change_group_visibility_level?).and_return(false) + allow(view).to receive(:current_user).and_return(test_user) + expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled]) + expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here') + + render + + expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups") + expect(rendered).to have_css('.descr', text: 'help text here') + expect(rendered).to have_field('group_share_with_group_lock', checkbox_options) + end + end + + context 'for a root group' do + let(:test_group) { root_group } + let(:test_user) { root_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false } + end + + context 'for a subgroup', :nested_groups do + let!(:subgroup) { create(:group, parent: root_group) } + let(:sub_owner) { create(:user) } + let(:test_group) { subgroup } + + context 'when the root_group has "Share with group lock" disabled' do + context 'when the subgroup has "Share with group lock" disabled' do + context 'as the root_owner' do + let(:test_user) { root_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false } + end + + context 'as the sub_owner' do + let(:test_user) { sub_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false } + end + end + + context 'when the subgroup has "Share with group lock" enabled' do + before do + subgroup.update_column(:share_with_group_lock, true) + end + + context 'as the root_owner' do + let(:test_user) { root_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true } + end + + context 'as the sub_owner' do + let(:test_user) { sub_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true } + end + end + end + + context 'when the root_group has "Share with group lock" enabled' do + before do + root_group.update_column(:share_with_group_lock, true) + end + + context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do + context 'as the root_owner' do + let(:test_user) { root_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false } + end + + context 'as the sub_owner' do + let(:test_user) { sub_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false } + end + end + + context 'when the subgroup has "Share with group lock" enabled (same as parent)' do + before do + subgroup.update_column(:share_with_group_lock, true) + end + + context 'as the root_owner' do + let(:test_user) { root_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true } + end + + context 'as the sub_owner' do + let(:test_user) { sub_owner } + + it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true } + end + end + end + end + end +end diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb index c1398629749..5c6b2e4b042 100644 --- a/spec/views/projects/edit.html.haml_spec.rb +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -15,17 +15,6 @@ describe 'projects/edit' do current_application_settings: Gitlab::CurrentSettings.current_application_settings) end - context 'LFS enabled setting' do - it 'displays the correct elements' do - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) - - render - - expect(rendered).to have_select('project_lfs_enabled') - expect(rendered).to have_content('Git Large File Storage') - end - end - context 'project export disabled' do it 'does not display the project export option' do stub_application_setting(project_export_enabled?: false) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index c4979792194..6f9ddb6c63c 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -5,28 +5,100 @@ require 'spec_helper' describe GitGarbageCollectWorker do let(:project) { create(:project, :repository) } let(:shell) { Gitlab::Shell.new } + let!(:lease_uuid) { SecureRandom.uuid } + let!(:lease_key) { "project_housekeeping:#{project.id}" } subject { described_class.new } describe "#perform" do shared_examples 'flushing ref caches' do |gitaly| - it "flushes ref caches when the task if 'gc'" do - expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - - if gitaly - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) - .and_return(nil) - else - expect(Gitlab::Popen).to receive(:popen) - .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + context 'with active lease_uuid' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) end - expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original - expect_any_instance_of(Repository).to receive(:branch_names).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + it "flushes ref caches when the task if 'gc'" do + expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original + expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - subject.perform(project.id) + if gitaly + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) + .and_return(nil) + else + expect(Gitlab::Popen).to receive(:popen) + .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + end + + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + + subject.perform(project.id, :gc, lease_key, lease_uuid) + end + end + + context 'with different lease than the active one' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid) + end + + it 'returns silently' do + expect(subject).not_to receive(:command) + expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original + expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original + + subject.perform(project.id, :gc, lease_key, lease_uuid) + end + end + + context 'with no active lease' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(false) + end + + context 'when is able to get the lease' do + before do + allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid) + end + + it "flushes ref caches when the task if 'gc'" do + expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) + + if gitaly + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) + .and_return(nil) + else + expect(Gitlab::Popen).to receive(:popen) + .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + end + + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + + subject.perform(project.id) + end + end + + context 'when no lease can be obtained' do + before do + expect(subject).to receive(:try_obtain_lease).and_return(false) + end + + it 'returns silently' do + expect(subject).not_to receive(:command) + expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original + expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original + + subject.perform(project.id) + end + end end end @@ -39,25 +111,34 @@ describe GitGarbageCollectWorker do end context "repack_full" do + before do + expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + it "calls Gitaly" do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full) .and_return(nil) - subject.perform(project.id, :full_repack) + subject.perform(project.id, :full_repack, lease_key, lease_uuid) end end context "repack_incremental" do + before do + expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + it "calls Gitaly" do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental) .and_return(nil) - subject.perform(project.id, :incremental_repack) + subject.perform(project.id, :incremental_repack, lease_key, lease_uuid) end end shared_examples 'gc tasks' do before do + allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) end @@ -67,7 +148,7 @@ describe GitGarbageCollectWorker do expect(before_packs.count).to be >= 1 - subject.perform(project.id, 'incremental_repack') + subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid) after_packs = packs(project) # Exactly one new pack should have been created @@ -79,12 +160,12 @@ describe GitGarbageCollectWorker do it 'full repack consolidates into 1 packfile' do create_objects(project) - subject.perform(project.id, 'incremental_repack') + subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid) before_packs = packs(project) expect(before_packs.count).to be >= 2 - subject.perform(project.id, 'full_repack') + subject.perform(project.id, 'full_repack', lease_key, lease_uuid) after_packs = packs(project) expect(after_packs.count).to eq(1) @@ -102,7 +183,7 @@ describe GitGarbageCollectWorker do expect(before_packs.count).to be >= 1 - subject.perform(project.id, 'gc') + subject.perform(project.id, 'gc', lease_key, lease_uuid) after_packed_refs = packed_refs(project) after_packs = packs(project) diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index af6a3c9f6c7..d3707a3cc11 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -127,6 +127,7 @@ describe PostReceive do it "asks the project to trigger all hooks" do allow(Project).to receive(:find_by).and_return(project) + expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice @@ -135,6 +136,7 @@ describe PostReceive do it "enqueues a UpdateMergeRequestsWorker job" do allow(Project).to receive(:find_by).and_return(project) + expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) described_class.new.perform(gl_repository, key_id, base64_changes) diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz Binary files differindex 302a74637b2..69e35e6aa40 100644 --- a/vendor/project_templates/express.tar.gz +++ b/vendor/project_templates/express.tar.gz diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz Binary files differindex 0f406705563..561b1e5902c 100644 --- a/vendor/project_templates/rails.tar.gz +++ b/vendor/project_templates/rails.tar.gz diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz Binary files differindex 02006b14406..0ba6ec7c60c 100644 --- a/vendor/project_templates/spring.tar.gz +++ b/vendor/project_templates/spring.tar.gz |