diff options
233 files changed, 3485 insertions, 1172 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b76c8f00d77..dadce073309 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 @@ -247,7 +283,6 @@ setup-test-env: script: - node --version - yarn install --frozen-lockfile --cache-folder .yarn-cache - - bundle exec rake gettext:po_to_json - bundle exec rake gitlab:assets:compile - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - scripts/gitaly-test-build # Do not use 'bundle exec' here @@ -412,7 +447,6 @@ db:migrate:reset-mysql: .migration-paths: &migration-paths <<: *dedicated-runner - <<: *only-canonical-masters <<: *pull-cache stage: test variables: @@ -496,7 +530,6 @@ gitlab:assets:compile: NO_COMPRESSION: "true" script: - yarn install --frozen-lockfile --production --cache-folder .yarn-cache - - bundle exec rake gettext:po_to_json - bundle exec rake gitlab:assets:compile artifacts: name: webpack-report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cc34f1de08..6fb2c6bd1dc 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 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0f1a7dfc7c4..ca75280b09b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.37.0 +0.38.0 @@ -31,7 +31,7 @@ gem 'omniauth-cas3', '~> 1.1.4' gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.2' -gem 'omniauth-google-oauth2', '~> 0.4.1' +gem 'omniauth-google-oauth2', '~> 0.5.2' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-saml', '~> 1.7.0' @@ -324,7 +324,7 @@ group :development, :test do # Generate Fake data gem 'ffaker', '~> 2.4' - gem 'capybara', '~> 2.6.2' + gem 'capybara', '~> 2.15.0' gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' @@ -397,7 +397,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.32.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.33.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 320d42b8974..9fbf08d80e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,9 +100,9 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (9.0.6) - capybara (2.6.2) + capybara (2.15.1) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) @@ -275,7 +275,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.32.0) + gitaly-proto (0.33.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -479,6 +479,7 @@ GEM method_source (0.8.2) mime-types (2.99.3) mimemagic (0.3.0) + mini_mime (0.1.4) mini_portile2 (2.2.0) minitest (5.7.0) mmap2 (2.2.7) @@ -529,8 +530,8 @@ GEM omniauth-gitlab (1.0.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.4.1) - jwt (~> 1.5.2) + omniauth-google-oauth2 (0.5.2) + jwt (~> 1.5) multi_json (~> 1.3) omniauth (>= 1.1.1) omniauth-oauth2 (>= 1.3.1) @@ -948,7 +949,7 @@ GEM expression_parser rinku xml-simple (1.1.5) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS @@ -979,7 +980,7 @@ DEPENDENCIES browser (~> 2.2) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) - capybara (~> 2.6.2) + capybara (~> 2.15.0) capybara-screenshot (~> 1.0.0) carrierwave (~> 1.1) charlock_holmes (~> 0.7.5) @@ -1021,7 +1022,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.32.0) + gitaly-proto (~> 0.33.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) @@ -1075,7 +1076,7 @@ DEPENDENCIES omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.2) - omniauth-google-oauth2 (~> 0.4.1) + omniauth-google-oauth2 (~> 0.5.2) omniauth-kerberos (~> 0.3.0) omniauth-oauth2-generic (~> 0.2.2) omniauth-saml (~> 1.7.0) 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/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/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 6f7671aa6fe..50d822eba5a 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -175,7 +175,7 @@ GitLabDropdownFilter = (function() { elements.show().removeClass('option-hidden'); } - elements.parent().find('.dropdown-menu-empty-link').toggleClass('hidden', elements.is(':visible')); + elements.parent().find('.dropdown-menu-empty-item').toggleClass('hidden', elements.is(':visible')); } }; @@ -247,7 +247,7 @@ GitLabDropdown = (function() { currentIndex = -1; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; @@ -702,7 +702,7 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.noResults = function() { var html; - return html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>'; + return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>'; }; GitLabDropdown.prototype.rowClicked = function(el) { 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/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 6fd5345a0a6..003a15592f3 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -363,7 +363,7 @@ restoreMenu() { var html; - html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; + html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; return this.dropdownContent.html(html); } 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 5f397f08936..cb501030356 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -163,12 +163,6 @@ } } - &.dropdown-menu-empty-link { - &.is-focused { - background-color: $dropdown-empty-row-bg; - } - } - &.dropdown-menu-user-link { line-height: 16px; } @@ -189,7 +183,7 @@ width: auto; top: 100%; left: 0; - z-index: 200; + z-index: 300; min-width: 240px; max-width: 500px; margin-top: 2px; @@ -256,6 +250,13 @@ @include dropdown-link; } + .dropdown-menu-empty-item a { + &:hover, + &:focus { + background-color: transparent; + } + } + .dropdown-header { color: $gl-text-color-secondary; font-size: 13px; @@ -800,6 +801,13 @@ } } } + + &.dropdown-menu-empty-item a { + &:hover, + &:focus { + background-color: transparent; + } + } } &.dropdown-menu-selectable { @@ -829,17 +837,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/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 081da3e2d1a..8e095cbdd7e 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -458,16 +458,6 @@ 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; font-weight: $gl-font-weight-bold; diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index e7bd6787c89..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 { @@ -172,6 +173,10 @@ $new-sidebar-collapsed-width: 50px; width: 16px; } } + + .fly-out-top-item { + display: none; + } } .nav-sidebar-inner-scroll { @@ -242,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; @@ -263,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; @@ -302,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; } } @@ -400,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/diff.scss b/app/assets/stylesheets/pages/diff.scss index 8cbf0ec6180..a7acaf6c728 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -578,12 +578,12 @@ @media (min-width: $screen-sm-min) { position: -webkit-sticky; position: sticky; - top: 34px; + top: 24px; background-color: $white-light; z-index: 190; &.diff-files-changed-merge-request { - top: 84px; + top: 76px; } + .files, @@ -614,6 +614,14 @@ } } +@media (min-width: $screen-sm-min) { + .with-performance-bar { + .diff-files-changed.diff-files-changed-merge-request { + top: 76px + $performance-bar-height; + } + } +} + .diff-file-changes { width: 450px; z-index: 150; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9f2cb979518..d8a15faf7e9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -220,7 +220,7 @@ .right-sidebar { position: absolute; - top: $header-height; + top: $new-navbar-height; bottom: 0; right: 0; transition: width .3s; @@ -230,7 +230,7 @@ .issuable-sidebar { width: calc(100% + 100px); - height: calc(100% - #{$header-height}); + height: calc(100% - #{$new-navbar-height}); overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; @@ -479,10 +479,10 @@ } .with-performance-bar .right-sidebar { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; .issuable-sidebar { - height: calc(100% - #{$header-height} - #{$performance-bar-height}); + height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height}); } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 8609f72bdab..439636fe026 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -645,7 +645,7 @@ } .merge-request-tabs-holder { - top: $header-height; + top: $new-navbar-height; z-index: 200; background-color: $white-light; border-bottom: 1px solid $border-color; @@ -675,7 +675,7 @@ } .with-performance-bar .merge-request-tabs-holder { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; } .merge-request-tabs { 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/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8367c22d1ca..4dfb397e82c 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -20,8 +20,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def usage_data respond_to do |format| format.html do - usage_data = Gitlab::UsageData.data - usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json + usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data) render html: Gitlab::Highlight.highlight('payload.json', usage_data_json) end 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/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/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/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/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/search_helper.rb b/app/helpers/search_helper.rb index 98e824a8c65..af6683a548b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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 e0d3e9b88f3..95dbdc8ec46 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -99,7 +99,9 @@ module TreeHelper end # returns the relative path of the first subdir that doesn't have only one directory descendant - def flatten_tree(tree) + def flatten_tree(root_path, tree) + return tree.flat_path.sub(/\A#{root_path}\//, '') if tree.flat_path.present? + 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)) 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/ci/build.rb b/app/models/ci/build.rb index 28c16d4037f..64c93966dff 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -27,7 +27,6 @@ module Ci validates :coverage, numericality: true, allow_blank: true validates :ref, presence: true - validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 35d14b6e297..46e5c344fdc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -36,7 +36,6 @@ module Ci validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :status, presence: { unless: :importing? } - validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create validate :valid_commit_sha, unless: :importing? after_create :keep_around_commits, unless: :importing? 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/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/project.rb b/app/models/project.rb index fdd516ec2ae..18800921c6c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1486,6 +1486,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/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/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/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/views/admin/cohorts/_usage_ping.html.haml b/app/views/admin/cohorts/_usage_ping.html.haml index 73aa95d84f1..3dda386fcf7 100644 --- a/app/views/admin/cohorts/_usage_ping.html.haml +++ b/app/views/admin/cohorts/_usage_ping.html.haml @@ -7,4 +7,4 @@ = succeed '.' do = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics') -%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html, pretty: true) } } +%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } 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/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/_search.html.haml b/app/views/layouts/_search.html.haml index 59f16b47bf7..cd7a47da4a1 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -17,8 +17,8 @@ .dropdown-menu.dropdown-select = dropdown_content do %ul - %li - %a.is-focused.dropdown-menu-empty-link + %li.dropdown-menu-empty-item + %a Loading... = dropdown_loading %i.search-icon 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..27fadc1d952 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,6 +100,14 @@ = number_with_delimiter(@project.open_issues_count) %ul.sidebar-sub-level-items + = 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) do = link_to project_issues_path(@project), title: 'Issues' do %span @@ -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/show.html.haml b/app/views/profiles/show.html.haml index 35ad280b037..79f334176a5 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -2,7 +2,7 @@ - @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' -= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f| += bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f| = form_errors(@user) .row 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/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 02fd54c97fb..ad2d355ab4a 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -29,6 +29,6 @@ +#{diff_file.added_lines} %span.cred< \-#{diff_file.removed_lines} - %li.dropdown-menu-empty-link.hidden - %a{ href: "#" } + %li.dropdown-menu-empty-item.hidden + %a No files found. 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..fc7190505da 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -12,16 +12,13 @@ = 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 } .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/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 7e854186973..88085c7185b 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -7,7 +7,7 @@ = custom_icon('ellipsis_v') %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %li - = clipboard_button(text: noteable_note_url(note), title: "Copy reference to clipboard", button_text: 'Copy link', hide_tooltip: true, hide_button_icon: true) + = clipboard_button(text: noteable_note_url(note), title: 'Copy reference to clipboard', button_text: 'Copy link', class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true) - unless is_current_user %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do 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/show.html.haml b/app/views/projects/show.html.haml index 3f0a24cfe83..6ee55bba82a 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 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/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 0c9c8750f2c..56197382a70 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,7 +1,7 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name = tree_icon(type, tree_item.mode, tree_item.name) - - path = flatten_tree(tree_item) + - path = flatten_tree(@path, tree_item) = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path do %span.str-truncated= path %td.hidden-xs.tree-commit 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/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/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/37023-remove-focus-styles-from-dropdown-empty-link.yml b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml new file mode 100644 index 00000000000..fcaa6ec13f8 --- /dev/null +++ b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml @@ -0,0 +1,5 @@ +--- +title: Remove focus styles from dropdown empty links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml b/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml new file mode 100644 index 00000000000..2a8e8a33225 --- /dev/null +++ b/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml @@ -0,0 +1,5 @@ +--- +title: Add description template examples to documentation +merge_request: +author: +type: other diff --git a/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml new file mode 100644 index 00000000000..3e4105a4232 --- /dev/null +++ b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml @@ -0,0 +1,5 @@ +--- +title: Add quick submission on user settings page +merge_request: 14007 +author: Vitaliy @blackst0ne Klachkov +type: added 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-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/fix_wiki_toc_indent.yml b/changelogs/unreleased/fix_wiki_toc_indent.yml new file mode 100644 index 00000000000..60da2e455f2 --- /dev/null +++ b/changelogs/unreleased/fix_wiki_toc_indent.yml @@ -0,0 +1,5 @@ +--- +title: Wiki table of contents are now properly nested to reflect header level +merge_request: 13650 +author: Akihiro Nakashima +type: fixed diff --git a/changelogs/unreleased/wiki_api.yml b/changelogs/unreleased/wiki_api.yml new file mode 100644 index 00000000000..9d60356aedc --- /dev/null +++ b/changelogs/unreleased/wiki_api.yml @@ -0,0 +1,5 @@ +--- +title: Add API support for wiki pages +merge_request: 13372 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/changelogs/unreleased/winh-dropdown-changelog-docs.yml b/changelogs/unreleased/winh-dropdown-changelog-docs.yml new file mode 100644 index 00000000000..2f42b4dd9f9 --- /dev/null +++ b/changelogs/unreleased/winh-dropdown-changelog-docs.yml @@ -0,0 +1,5 @@ +--- +title: Restyle dropdown menus to make them look consistent +merge_request: +author: +type: other 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/20170825104051_migrate_issues_to_ghost_user.rb b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb index 294141e4fdb..c5fb5762d61 100644 --- a/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb +++ b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb @@ -18,6 +18,7 @@ class MigrateIssuesToGhostUser < ActiveRecord::Migration ActiveRecord::Base.clear_cache! ::User.reset_column_information + ::Namespace.reset_column_information end def up 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/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 937075c8a3c..86c1dda537e 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,10 @@ 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 end create_table "audit_events", force: :cascade do |t| @@ -255,6 +255,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 +274,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 +287,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 @@ -531,38 +532,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 +574,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 +587,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 +786,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 @@ -1209,6 +1191,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 +1199,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 @@ -1708,9 +1690,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 @@ -1754,7 +1735,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..a59f71e83a5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -84,7 +84,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/README.md b/doc/api/README.md index db61497db53..6cbea29bda6 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -58,6 +58,7 @@ following locations: - [Validate CI configuration](lint.md) - [V3 to V4](v3_to_v4.md) - [Version](version.md) +- [Wikis](wikis.md) ## Road to GraphQL 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/wikis.md b/doc/api/wikis.md new file mode 100644 index 00000000000..10eebc4a3c1 --- /dev/null +++ b/doc/api/wikis.md @@ -0,0 +1,157 @@ +# Wikis API + +> [Introduced][ce-13372] in GitLab 10.0. + +Available only in APIv4. + +## List wiki pages + +Get all wiki pages for a given project. + +``` +GET /projects/:id/wikis +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `with_content` | boolean | no | Include pages' content | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/wikis?with_content=1 +``` + +Example response: + +```json +[ + { + "content" : "Here is an instruction how to deploy this project.", + "format" : "markdown", + "slug" : "deploy", + "title" : "deploy" + }, + { + "content" : "Our development process is described here.", + "format" : "markdown", + "slug" : "development", + "title" : "development" + },{ + "content" : "* [Deploy](deploy)\n* [Development](development)", + "format" : "markdown", + "slug" : "home", + "title" : "home" + } +] +``` + +## Get a wiki page + +Get a wiki page for a given project. + +``` +GET /projects/:id/wikis/:slug +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `slug` | string | yes | The slug (a unique string) of the wiki page | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/wikis/home +``` + +Example response: + +```json +[ + { + "content" : "home page", + "format" : "markdown", + "slug" : "home", + "title" : "home" + } +] +``` + +## Create a new wiki page + +Creates a new wiki page for the given repository with the given title, slug, and content. + +``` +POST /projects/:id/wikis +``` + +| Attribute | Type | Required | Description | +| ------------- | ------- | -------- | ---------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `content` | string | yes | The content of the wiki page | +| `title` | string | yes | The title of the wiki page | +| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` | + +```bash +curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis" +``` + +Example response: + +```json +{ + "content" : "Hello world", + "format" : "markdown", + "slug" : "Hello", + "title" : "Hello" +} +``` + +## Edit an existing wiki page + +Updates an existing wiki page. At least one parameter is required to update the wiki page. + +``` +PUT /projects/:id/wikis/:slug +``` + +| Attribute | Type | Required | Description | +| --------------- | ------- | --------------------------------- | ------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `content` | string | yes if `title` is not provided | The content of the wiki page | +| `title` | string | yes if `content` is not provided | The title of the wiki page | +| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` | +| `slug` | string | yes | The slug (a unique string) of the wiki page | + + +```bash +curl --request PUT --data "format=rdoc&content=documentation&title=Docs" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" +``` + +Example response: + +```json +{ + "content" : "documentation", + "format" : "markdown", + "slug" : "Docs", + "title" : "Docs" +} +``` + +## Delete a wiki page + +Deletes a wiki page with a given slug. + +``` +DELETE /projects/:id/wikis/:slug +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `slug` | string | yes | The slug (a unique string) of the wiki page | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" +``` + +On success the HTTP status code is `204` and no JSON response is expected. diff --git a/doc/ci/autodeploy/img/auto_deploy_btn.png b/doc/ci/autodeploy/img/auto_deploy_btn.png Binary files differnew file mode 100644 index 00000000000..25915ed1c9d --- /dev/null +++ b/doc/ci/autodeploy/img/auto_deploy_btn.png diff --git a/doc/ci/autodeploy/img/auto_deploy_dropdown.png b/doc/ci/autodeploy/img/auto_deploy_dropdown.png Binary files differindex b93b0a08fea..5815937a4af 100644 --- a/doc/ci/autodeploy/img/auto_deploy_dropdown.png +++ b/doc/ci/autodeploy/img/auto_deploy_dropdown.png diff --git a/doc/ci/autodeploy/img/guide_connect_cluster.png b/doc/ci/autodeploy/img/guide_connect_cluster.png Binary files differnew file mode 100644 index 00000000000..b856b81a1d0 --- /dev/null +++ b/doc/ci/autodeploy/img/guide_connect_cluster.png diff --git a/doc/ci/autodeploy/img/guide_integration.png b/doc/ci/autodeploy/img/guide_integration.png Binary files differnew file mode 100644 index 00000000000..723b2619ea2 --- /dev/null +++ b/doc/ci/autodeploy/img/guide_integration.png diff --git a/doc/ci/autodeploy/img/guide_secret.png b/doc/ci/autodeploy/img/guide_secret.png Binary files differnew file mode 100644 index 00000000000..01f5aa49908 --- /dev/null +++ b/doc/ci/autodeploy/img/guide_secret.png diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index a714689ebd5..a128cf69c20 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -1,8 +1,13 @@ -# Auto deploy +# Auto Deploy -> [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. +>**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. 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` @@ -11,9 +16,23 @@ powering them. These scripts are responsible for packaging your application, setting up the infrastructure and spinning up necessary services (for example a database). -You can use [project services][project-services] to store credentials to -your infrastructure provider and they will be available during the -deployment. +## How it works + +The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy] +project which is used to simplify the deployment process to Kubernetes by +providing intelligent `build`, `deploy`, and `destroy` commands which you can +use in your `.gitlab-ci.yml` as is. It uses [Herokuish](https://github.com/gliderlabs/herokuish), +which uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) +to do some of the work, plus some of GitLab's own tools to package it all up. For +your convenience, a [Docker image][kube-image] is also provided. + +You can use the [Kubernetes project service](../../user/project/integrations/kubernetes.md) +to store credentials to your infrastructure provider and they will be available +during the deployment. + +## Quick start + +We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com. ## Supported templates @@ -22,20 +41,27 @@ The list of supported auto deploy templates is available in the ## Configuration +>**Note:** +In order to understand why the following steps are required, read the +[how it works](#how-it-works) section. + +To configure Autodeploy, you will need to: + 1. Enable a deployment [project service][project-services] to store your -credentials. For example, if you want to deploy to OpenShift you have to -enable [Kubernetes service][kubernetes-service]. -1. Configure GitLab Runner to use Docker or Kubernetes executor with -[privileged mode enabled][docker-in-docker]. + credentials. For example, if you want to deploy to OpenShift you have to + enable [Kubernetes service][kubernetes-service]. +1. Configure GitLab Runner to use the + [Docker or Kubernetes executor](https://docs.gitlab.com/runner/executors/) with + [privileged mode enabled][docker-in-docker]. 1. Navigate to the "Project" tab and click "Set up auto deploy" button.  1. Select a template.  1. Commit your changes and create a merge request. 1. Test your deployment configuration using a [Review App][review-app] that was -created automatically for you. + created automatically for you. -## Private Project Support +## Private project support > Experimental support [introduced][mr-2] in GitLab 9.1. @@ -43,7 +69,7 @@ When a project has been marked as private, GitLab's [Container Registry][contain 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. -## PostgreSQL Database Support +## PostgreSQL database support > Experimental support [introduced][mr-8] in GitLab 9.1. @@ -51,25 +77,13 @@ In order to support applications that require a database, [PostgreSQL][postgresq PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`. -### PostgreSQL Variables +The following PostgreSQL variables are supported: 1. `DISABLE_POSTGRES: "yes"`: 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 -[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 -[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2 -[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8 -[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html -[project-services]: ../../user/project/integrations/project_services.md -[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy -[kubernetes-service]: ../../user/project/integrations/kubernetes.md -[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor -[review-app]: ../review_apps/index.md -[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html -[postgresql]: https://www.postgresql.org/ - ## Auto Monitoring > Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438). @@ -94,3 +108,18 @@ 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"`. + +[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 +[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2 +[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8 +[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html +[project-services]: ../../user/project/integrations/project_services.md +[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy +[kubernetes-service]: ../../user/project/integrations/kubernetes.md +[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../review_apps/index.md +[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry" +[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/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md new file mode 100644 index 00000000000..f76c2a2cf31 --- /dev/null +++ b/doc/ci/autodeploy/quick_start_guide.md @@ -0,0 +1,95 @@ +# Auto Deploy: quick start guide + +This is a step-by-step guide to deploying a project hosted on GitLab.com to Google Cloud, using Auto Deploy. + +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`. + + + +## 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. + + + +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. + + + +For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform. + +## Expose the application to the internet + +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>` pointing to the external IP address you found above. + +Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP. + +## Setup Auto Deploy + +Visit the home page of your GitLab.com project and press "Set up Auto Deploy" button. + + + +You will be redirected to the "New file" page where you can apply one of the Auto Deploy templates. Select "Kubernetes" to apply the template, then in the file, replace `domain.example.com` with your domain name and make any other adjustments you need. + + + +Change the target branch to `master`, and submit your changes. This should create +a new pipeline with several jobs. If you made only the domain name change, the +pipeline will have three jobs: `build`, `staging`, and `production`. + +The `build` job will create a Docker image with your new change and push it to +the GitLab Container Registry. The `staging` job will deploy this image on your +cluster. 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 `ruby-autodeploy-23`, but with a unique ID for your project, and +your app will be listed as "staging" under the "Deployment" tab. + +Once its ready - just visit http://minimal-ruby-app-staging.yourdomain.com to see “Hello, world!” 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/pipelines.md b/doc/ci/pipelines.md index 5a2b61fb0cb..ac4a9b0ed27 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -222,6 +222,30 @@ total running time should be: Pipeline status and test coverage report badges are available. You can find their respective link in the [Pipelines settings] page. +## Security on protected branches + +A strict security model is enforced when pipelines are executed on +[protected branches](../user/project/protected_branches.md). + +The following actions are allowed on protected branches only if the user is +[allowed to merge or push](../user/project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings) +on that specific branch: +- run **manual pipelines** (using Web UI or Pipelines API) +- run **scheduled pipelines** +- run pipelines using **triggers** +- trigger **manual actions** on existing pipelines +- **retry/cancel** existing jobs (using Web UI or Pipelines API) + +**Secret variables** marked as **protected** are accessible only to jobs that +run on protected branches, avoiding untrusted users to get unintended access to +sensitive information like deployment credentials and tokens. + +**Runners** marked as **protected** can run jobs only on protected +branches, avoiding untrusted code to be executed on the protected runner and +preserving deployment keys and other credentials from being unintentionally +accessed. In order to ensure that jobs intended to be executed on protected +runners will not use regular runners, they must be tagged accordingly. + [jobs]: #jobs [jobs-yaml]: yaml/README.md#jobs [manual]: yaml/README.md#manual 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. + + + +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/user/permissions.md b/doc/user/permissions.md index bd0a58c4cca..0c17905aa8c 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -230,6 +230,14 @@ users: GitLab 8.12 has a completely redesigned job permissions system. To learn more, read through the documentation on the [new CI/CD permissions model](project/new_ci_build_permissions_model.md#new-ci-job-permissions-model). +## Running pipelines on protected branches + +The permission to merge or push to protected branches is used to define if a user can +run CI/CD pipelines and execute actions on jobs that are related to those branches. + +See [Security on protected branches](../ci/pipelines.md#security-on-protected-branches) +for details about the pipelines security model. + ## LDAP users permissions Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user. 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/description_templates.md b/doc/user/project/description_templates.md index ea7496af089..7c94f4ef4d8 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -39,4 +39,58 @@ changes you made after picking the template and return it to its initial status.  +## Description template example + +We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples. + +> **Tip:** +It is possible to use [quick actions](./quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions. + +Here is an example for a Bug report template: + +``` +Summary + +(Summarize the bug encountered concisely) + + +Steps to reproduce + +(How one can reproduce the issue - this is very important) + + +Example Project + +(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report) + +(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version) + + +What is the current bug behavior? + +(What actually happens) + + +What is the expected correct behavior? + +(What you should see instead) + + +Relevant logs and/or screenshots + +(Paste any relevant logs - please use code blocks (```) to format console output, +logs, and code as it's very hard to read otherwise.) + + +Possible fixes + +(If you can, link to the line of code that might be responsible for the problem) + +/label ~bug ~reproduced ~needs-investigation +/cc @project-manager +/assign @qa-tester +``` + [ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981 +[gitlab-ce-templates]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/.gitlab + diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index eb8cd821ddc..9f0308d8111 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -23,4 +23,4 @@ Prometheus server up and running. You have two options here: In order to isolate and only display relevant metrics for a given environment however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments). -If you are using [GitLab Auto-Deploy][autodeploy] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. +If you are using [GitLab Auto-Deploy][../../../ci/autodeploy/index.md] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 0570d9f471f..0cbb0c878c2 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -115,6 +115,14 @@ Deleting a protected branch is only allowed via the web interface, not via Git. This means that you can't accidentally delete a protected branch from your command line or a Git client application. +## Running pipelines on protected branches + +The permission to merge or push to protected branches is used to define if a user can +run CI/CD pipelines and execute actions on jobs that are related to those branches. + +See [Security on protected branches](../../ci/pipelines.md#security-on-protected-branches) +for details about the pipelines security model. + ## Changelog **9.2** 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..513ccce2f8f 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 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/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/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 1405a5d0f0e..d9a62bffb6d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -144,6 +144,7 @@ module API mount ::API::Variables mount ::API::GroupVariables mount ::API::Version + mount ::API::Wikis route :any, '*path' do error!('404 Not Found', 404) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d5f2c422c58..52c49e5caa9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1,5 +1,15 @@ module API module Entities + class WikiPageBasic < Grape::Entity + expose :format + expose :slug + expose :title + end + + class WikiPage < WikiPageBasic + expose :content + end + class UserSafe < Grape::Entity expose :id, :name, :username end @@ -547,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/helpers.rb b/lib/api/helpers.rb index e646c63467a..8b03df65ae4 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -56,6 +56,12 @@ module API @project ||= find_project!(params[:id]) end + def wiki_page + page = user_project.wiki.find_page(params[:slug]) + + page || not_found!('Wiki Page') + end + def available_labels @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute end 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/wikis.rb b/lib/api/wikis.rb new file mode 100644 index 00000000000..b3fc4e876ad --- /dev/null +++ b/lib/api/wikis.rb @@ -0,0 +1,89 @@ +module API + class Wikis < Grape::API + helpers do + params :wiki_page_params do + requires :content, type: String, desc: 'Content of a wiki page' + requires :title, type: String, desc: 'Title of a wiki page' + optional :format, + type: String, + values: ProjectWiki::MARKUPS.values.map(&:to_s), + default: 'markdown', + desc: 'Format of a wiki page. Available formats are markdown, rdoc, and asciidoc' + end + end + + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + desc 'Get a list of wiki pages' do + success Entities::WikiPageBasic + end + params do + optional :with_content, type: Boolean, default: false, desc: "Include pages' content" + end + get ':id/wikis' do + authorize! :read_wiki, user_project + + entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic + present user_project.wiki.pages, with: entity + end + + desc 'Get a wiki page' do + success Entities::WikiPage + end + params do + requires :slug, type: String, desc: 'The slug of a wiki page' + end + get ':id/wikis/:slug' do + authorize! :read_wiki, user_project + + present wiki_page, with: Entities::WikiPage + end + + desc 'Create a wiki page' do + success Entities::WikiPage + end + params do + use :wiki_page_params + end + post ':id/wikis' do + authorize! :create_wiki, user_project + + page = WikiPages::CreateService.new(user_project, current_user, params).execute + + if page.valid? + present page, with: Entities::WikiPage + else + render_validation_error!(page) + end + end + + desc 'Update a wiki page' do + success Entities::WikiPage + end + params do + use :wiki_page_params + end + put ':id/wikis/:slug' do + authorize! :create_wiki, user_project + + page = WikiPages::UpdateService.new(user_project, current_user, params).execute(wiki_page) + + if page.valid? + present page, with: Entities::WikiPage + else + render_validation_error!(page) + end + end + + desc 'Delete a wiki page' + params do + requires :slug, type: String, desc: 'The slug of a wiki page' + end + delete ':id/wikis/:slug' do + authorize! :admin_wiki, user_project + + status 204 + WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page) + end + end + end +end diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 8e7084f2543..47151626208 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -22,40 +22,94 @@ module Banzai result[:toc] = "" headers = Hash.new(0) + header_root = current_header = HeaderNode.new doc.css('h1, h2, h3, h4, h5, h6').each do |node| - text = node.text + if header_content = node.children.first + id = node + .text + .downcase + .gsub(PUNCTUATION_REGEXP, '') # remove punctuation + .tr(' ', '-') # replace spaces with dash + .squeeze('-') # replace multiple dashes with one - id = text.downcase - id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation - id.tr!(' ', '-') # replace spaces with dash - id.squeeze!('-') # replace multiple dashes with one + uniq = headers[id] > 0 ? "-#{headers[id]}" : '' + headers[id] += 1 + href = "#{id}#{uniq}" - uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' - headers[id] += 1 + current_header = HeaderNode.new(node: node, href: href, previous_header: current_header) - if header_content = node.children.first - # namespace detection will be automatically handled via javascript (see issue #22781) - namespace = "user-content-" - href = "#{id}#{uniq}" - push_toc(href, text) - header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) + header_content.add_previous_sibling(anchor_tag(href)) end end - result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty? + push_toc(header_root.children, root: true) doc end private - def anchor_tag(id, href) - %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>} + def anchor_tag(href) + %Q{<a id="user-content-#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>} end - def push_toc(href, text) - result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n} + def push_toc(children, root: false) + return if children.empty? + + klass = ' class="section-nav"' if root + + result[:toc] << "<ul#{klass}>" + children.each { |child| push_anchor(child) } + result[:toc] << '</ul>' + end + + def push_anchor(header_node) + result[:toc] << %Q{<li><a href="##{header_node.href}">#{header_node.text}</a>} + push_toc(header_node.children) + result[:toc] << '</li>' + end + + class HeaderNode + attr_reader :node, :href, :parent, :children + + def initialize(node: nil, href: nil, previous_header: nil) + @node = node + @href = href + @children = [] + + @parent = find_parent(previous_header) + @parent.children.push(self) if @parent + end + + def level + return 0 unless node + + @level ||= node.name[1].to_i + end + + def text + return '' unless node + + @text ||= node.text + end + + private + + def find_parent(previous_header) + return unless previous_header + + if level == previous_header.level + parent = previous_header.parent + elsif level > previous_header.level + parent = previous_header + else + parent = previous_header + parent = parent.parent while parent.level >= level + end + + parent + end end end end diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index b54962a4456..5cf336af3c6 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -5,7 +5,7 @@ module Gitlab class Tree include Gitlab::EncodingHelper - attr_accessor :id, :root_id, :name, :path, :type, + attr_accessor :id, :root_id, :name, :path, :flat_path, :type, :mode, :commit_id, :submodule_url class << self @@ -19,8 +19,7 @@ module Gitlab Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled| if is_enabled - client = Gitlab::GitalyClient::CommitService.new(repository) - client.tree_entries(repository, sha, path) + repository.gitaly_commit_client.tree_entries(repository, sha, path) else tree_entries_from_rugged(repository, sha, path) end @@ -88,7 +87,7 @@ module Gitlab end def initialize(options) - %w(id root_id name path type mode commit_id).each do |key| + %w(id root_id name path flat_path type mode commit_id).each do |key| self.send("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end end @@ -101,6 +100,10 @@ module Gitlab encode! @path end + def flat_path + encode! @flat_path + end + def dir? type == :tree end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 21a32a7e0db..0825a3a7694 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -88,14 +88,14 @@ module Gitlab response.flat_map do |message| message.entries.map do |gitaly_tree_entry| - entry_path = gitaly_tree_entry.path.dup Gitlab::Git::Tree.new( id: gitaly_tree_entry.oid, root_id: gitaly_tree_entry.root_oid, type: gitaly_tree_entry.type.downcase, mode: gitaly_tree_entry.mode.to_s(8), - name: File.basename(entry_path), - path: entry_path, + name: File.basename(gitaly_tree_entry.path), + path: GitalyClient.encode(gitaly_tree_entry.path), + flat_path: GitalyClient.encode(gitaly_tree_entry.flat_path), commit_id: gitaly_tree_entry.commit_oid ) 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/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 20580459046..d563a87dcfd 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -69,7 +69,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/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/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/scripts/trigger-build-docs b/scripts/trigger-build-docs new file mode 100755 index 00000000000..e58edf189cf --- /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' + +# +# 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 is_ee? + File.exists?('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 => e + 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 = is_ee? ? 'BRANCH_EE' : 'BRANCH_CE' + + # The review app URL + app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{is_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/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/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/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 06a43909053..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 @@ -115,9 +101,9 @@ feature 'Dashboard Projects' do expect(page).to have_selector('.merge-request-form') expect(current_path).to eq project_new_merge_request_path(project) - expect(find('#merge_request_target_project_id').value).to eq project.id.to_s - expect(find('input#merge_request_source_branch').value).to eq 'feature' - expect(find('input#merge_request_target_branch').value).to eq 'master' + expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s + expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature' + expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master' end end 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/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/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index ac7f75bd308..fd110e68e84 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -196,10 +196,11 @@ feature 'Diff notes resolve', js: true do end it 'does not mark discussion as resolved when resolving single note' do - page.first '.diff-content .note' do + page.within("#note_#{note.id}") do first('.line-resolve-btn').click - expect(page).to have_selector('.note-action-button .loading') + wait_for_requests + expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") end diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb index e13bf4b6089..e1852a6e544 100644 --- a/spec/features/projects/files/creating_a_file_spec.rb +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -14,7 +14,7 @@ feature 'User wants to create a file' do file_name = find('#file_name') file_name.set options[:file_name] || 'README.md' - file_content = find('#file-content') + file_content = find('#file-content', visible: false) file_content.set options[:file_content] || 'Some content' click_button 'Commit changes' 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/search_spec.rb b/spec/features/search_spec.rb index 05a089641f1..8f6d0bb9d1b 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -295,7 +295,7 @@ describe "Search" do fill_in 'search', with: 'foo' click_button 'Search' - expect(find('#group_id').value).to eq(project.namespace.id.to_s) + expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) end it 'preserves the project being searched in' do @@ -304,7 +304,7 @@ describe "Search" do fill_in 'search', with: 'foo' click_button 'Search' - expect(find('#project_id').value).to eq(project.id.to_s) + expect(find('#project_id', visible: false).value).to eq(project.id.to_s) end 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/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index 9523d0f4aa6..d7b66e6f078 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -3,25 +3,35 @@ require 'spec_helper' describe TreeHelper do describe 'flatten_tree' do let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' } + let(:tree) { repository.tree(sha, 'files') } + let(:root_path) { 'files' } + let(:tree_item) { tree.entries.find { |entry| entry.path == path } } - before do - @repository = project.repository - @commit = project.commit("e56497bb") - end + subject { flatten_tree(root_path, tree_item) } context "on a directory containing more than one file/directory" do - let(:tree_item) { double(name: "files", path: "files") } + let(:path) { 'files/html' } it "returns the directory name" do - expect(flatten_tree(tree_item)).to match('files') + expect(subject).to match('html') end end context "on a directory containing only one directory" do - let(:tree_item) { double(name: "foo", path: "foo") } + let(:path) { 'files/flat' } it "returns the flattened path" do - expect(flatten_tree(tree_item)).to match('foo/bar') + expect(subject).to match('flat/path/correct') + end + + context "with a nested root path" do + let(:root_path) { 'files/flat' } + + it "returns the flattened path with the root path suffix removed" do + expect(subject).to match('path/correct') + end end end end 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/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/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index dcb8dbce178..ca048123bf7 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -8,7 +8,7 @@ describe('glDropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); loadJSONFixtures('projects.json'); - const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; const SEARCH_INPUT_SELECTOR = '.dropdown-input-field'; const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`; 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/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index ff6b19459bb..85eddde732e 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -96,5 +96,41 @@ describe Banzai::Filter::TableOfContentsFilter do expect(links.last.attr('href')).to eq '#header-2' expect(links.last.text).to eq 'Header 2' end + + context 'table of contents nesting' do + let(:results) do + result( + header(1, 'Header 1') << + header(2, 'Header 1-1') << + header(3, 'Header 1-1-1') << + header(2, 'Header 1-2') << + header(1, 'Header 2') << + header(2, 'Header 2-1') + ) + end + + it 'keeps list levels regarding header levels' do + items = doc.css('li') + + # Header 1 + expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' } + + # Header 1-1 + expect(items[1].ancestors).to include(items[0]) + + # Header 1-1-1 + expect(items[2].ancestors).to include(items[0], items[1]) + + # Header 1-2 + expect(items[3].ancestors).to include(items[0]) + expect(items[3].ancestors).not_to include(items[1]) + + # Header 2 + expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' } + + # Header 2-1 + expect(items[5].ancestors).to include(items[4]) + end + end end end 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 71141e38b5d..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 diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index c07a2d91768..86f7bcb8e38 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -20,6 +20,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(dir.name).to eq('encoding') } it { expect(dir.path).to eq('encoding') } + it { expect(dir.flat_path).to eq('encoding') } it { expect(dir.mode).to eq('40000') } context :subdir do @@ -30,6 +31,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(subdir.name).to eq('html') } it { expect(subdir.path).to eq('files/html') } + it { expect(subdir.flat_path).to eq('files/html') } end context :subdir_file do @@ -40,6 +42,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(subdir_file.name).to eq('popen.rb') } it { expect(subdir_file.path).to eq('files/ruby/popen.rb') } + it { expect(subdir_file.flat_path).to eq('files/ruby/popen.rb') } end end 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..1613b968bb6 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 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/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb new file mode 100644 index 00000000000..9e889d1eecf --- /dev/null +++ b/spec/requests/api/wikis_spec.rb @@ -0,0 +1,679 @@ +require 'spec_helper' + +# For every API endpoint we test 3 states of wikis: +# - disabled +# - enabled only for team members +# - enabled for everyone who has access +# Every state is tested for 3 user roles: +# - guest +# - developer +# - master +# because they are 3 edge cases of using wiki pages. + +describe API::Wikis do + let(:user) { create(:user) } + let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } + let(:expected_keys_with_content) { %w(content format slug title) } + let(:expected_keys_without_content) { %w(format slug title) } + + shared_examples_for 'returns list of wiki pages' do + context 'when wiki has pages' do + let!(:pages) do + [create(:wiki_page, wiki: project.wiki, attrs: { title: 'page1', content: 'content of page1' }), + create(:wiki_page, wiki: project.wiki, attrs: { title: 'page2', content: 'content of page2' })] + end + + it 'returns the list of wiki pages without content' do + get api(url, user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_without_content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + + it 'returns the list of wiki pages with content' do + get api(url, user), with_content: 1 + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_with_content) + expect(page['content']).to eq(pages[index].content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + end + + it 'return the empty list of wiki pages' do + get api(url, user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + shared_examples_for 'returns wiki page' do + it 'returns the wiki page' do + expect(response).to have_http_status(200) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(page.content) + expect(json_response['slug']).to eq(page.slug) + expect(json_response['title']).to eq(page.title) + end + end + + shared_examples_for 'creates wiki page' do + it 'creates the wiki page' do + post(api(url, user), payload) + + expect(response).to have_http_status(201) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + expect(json_response['rdoc']).to eq(payload[:rdoc]) + end + + [:title, :content].each do |part| + it "responds with validation error on empty #{part}" do + payload.delete(part) + + post(api(url, user), payload) + + expect(response).to have_http_status(400) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq("#{part} is missing") + end + end + end + + shared_examples_for 'updates wiki page' do + it 'updates the wiki page' do + expect(response).to have_http_status(200) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + end + end + + shared_examples_for '403 Forbidden' do + it 'returns 403 Forbidden' do + expect(response).to have_http_status(403) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('403 Forbidden') + end + end + + shared_examples_for '404 Wiki Page Not Found' do + it 'returns 404 Wiki Page Not Found' do + expect(response).to have_http_status(404) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('404 Wiki Page Not Found') + end + end + + shared_examples_for '404 Project Not Found' do + it 'returns 404 Project Not Found' do + expect(response).to have_http_status(404) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + shared_examples_for '204 No Content' do + it 'returns 204 No Content' do + expect(response).to have_http_status(204) + end + end + + describe 'GET /projects/:id/wikis' do + let(:url) { "/projects/#{project.id}/wikis" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'returns list of wiki pages' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'returns list of wiki pages' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'returns list of wiki pages' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'returns list of wiki pages' + end + end + end + + describe 'GET /projects/:id/wikis/:slug' do + let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + get api(url) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + get api(url, user) + end + + include_examples 'returns wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + end + + describe 'POST /projects/:id/wikis' do + let(:payload) { { title: 'title', content: 'content' } } + let(:url) { "/projects/#{project.id}/wikis" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + post(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + post(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'creates wiki page' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'creates wiki page' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + post(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + include_examples 'creates wiki page' + end + + context 'when user is master' do + before do + project.add_master(user) + end + + include_examples 'creates wiki page' + end + end + end + + describe 'PUT /projects/:id/wikis/:slug' do + let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:payload) { { title: 'new title', content: 'new content' } } + let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + put(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + put(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + put(api(url, user), payload) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + put(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + put(api(url), payload) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + + context 'when user is master' do + before do + project.add_master(user) + + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + end + + describe 'DELETE /projects/:id/wikis/:slug' do + let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } + + context 'when wiki is disabled' do + let(:project) { create(:project, :wiki_disabled) } + + context 'when user is guest' do + before do + delete(api(url)) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + end + + context 'when wiki is available only for team members' do + let(:project) { create(:project, :wiki_private) } + + context 'when user is guest' do + before do + delete(api(url)) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + delete(api(url, user)) + end + + include_examples '204 No Content' + end + end + + context 'when wiki is available for everyone with access' do + let(:project) { create(:project) } + + context 'when user is guest' do + before do + delete(api(url)) + end + + include_examples '404 Project Not Found' + end + + context 'when user is developer' do + before do + project.add_developer(user) + + delete(api(url, user)) + end + + include_examples '403 Forbidden' + end + + context 'when user is master' do + before do + project.add_master(user) + + delete(api(url, user)) + end + + include_examples '204 No Content' + + context 'when page is not existing' do + let(:url) { "/projects/#{project.id}/wikis/unknown" } + + include_examples '404 Wiki Page Not Found' + end + end + end + 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/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index f5ed9ff608f..bbc3a8c79f5 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -52,6 +52,17 @@ describe Ci::RetryBuildService do expect(new_build.send(attribute)).to eq build.send(attribute) end end + + context 'when job has nullified protected' do + before do + build.update_attribute(:protected, nil) + end + + it "clones protected build attribute" do + expect(new_build.protected).to be_nil + expect(new_build.protected).to eq build.protected + end + end end describe 'reject acessors' do 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/vendor/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md index 91b92eafa1b..0878db6dd9e 100644 --- a/vendor/Dockerfile/CONTRIBUTING.md +++ b/vendor/Dockerfile/CONTRIBUTING.md @@ -3,3 +3,50 @@ https://gitlab.com/gitlab-org/Dockerfile. GitLab only mirrors the templates. Please submit your merge requests to https://gitlab.com/gitlab-org/Dockerfile. + +## Contributing + +Thank you for your interest in contributing to this GitLab project! We welcome +all contributions. By participating in this project, you agree to abide by the +[code of conduct](#code-of-conduct). + +## Contributor license agreement + +By submitting code as an individual you agree to the [individual contributor +license agreement][individual-agreement]. + +By submitting code as an entity you agree to the [corporate contributor license +agreement][corporate-agreement]. + +## Code of conduct + +As contributors and maintainers of this project, we pledge to respect all people +who contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual +language or imagery, derogatory comments or personal attacks, trolling, public +or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct. Project maintainers who do not follow the +Code of Conduct may be removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior can be +reported by emailing contact@gitlab.com. + +This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0, +available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). + +[contributor-covenant]: http://contributor-covenant.org +[individual-agreement]: https://docs.gitlab.com/ee/legal/individual_contributor_license_agreement.html +[corporate-agreement]: https://docs.gitlab.com/ee/legal/corporate_contributor_license_agreement.html diff --git a/vendor/gitignore/Actionscript.gitignore b/vendor/gitignore/Actionscript.gitignore index 11e612e9853..5d947ca8879 100644 --- a/vendor/gitignore/Actionscript.gitignore +++ b/vendor/gitignore/Actionscript.gitignore @@ -1,9 +1,8 @@ # Build and Release Folders -bin/ bin-debug/ bin-release/ -[Oo]bj/ # FlashDevelop obj -[Bb]in/ # FlashDevelop bin +[Oo]bj/ +[Bb]in/ # Other files and folders .settings/ diff --git a/vendor/gitignore/Drupal.gitignore b/vendor/gitignore/Drupal.gitignore index 0d2fe537f46..072b683190f 100644 --- a/vendor/gitignore/Drupal.gitignore +++ b/vendor/gitignore/Drupal.gitignore @@ -1,10 +1,12 @@ # Ignore configuration files that may contain sensitive information. sites/*/*settings*.php +sites/example.sites.php # Ignore paths that contain generated content. files/ sites/*/files sites/*/private +sites/*/translations # Ignore default text files robots.txt @@ -16,6 +18,7 @@ robots.txt /UPGRADE.txt /README.txt sites/README.txt +sites/all/libraries/README.txt sites/all/modules/README.txt sites/all/themes/README.txt diff --git a/vendor/gitignore/Kotlin.gitignore b/vendor/gitignore/Kotlin.gitignore new file mode 120000 index 00000000000..c48376eebcf --- /dev/null +++ b/vendor/gitignore/Kotlin.gitignore @@ -0,0 +1 @@ +Java.gitignore
\ No newline at end of file diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore index 3f36ea2a878..6f35daaf478 100644 --- a/vendor/gitignore/Nanoc.gitignore +++ b/vendor/gitignore/Nanoc.gitignore @@ -4,7 +4,7 @@ output/ # Temporary file directory -tmp/ +tmp/nanoc/ # Crash Log crash.log diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index 00cbbdf53f6..97e28736892 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -29,7 +29,7 @@ bower_components # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) +# Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore index 5fa47c5a1f2..fe67fdf1ee6 100644 --- a/vendor/gitignore/Qt.gitignore +++ b/vendor/gitignore/Qt.gitignore @@ -26,6 +26,8 @@ moc_*.cpp moc_*.h qrc_*.cpp ui_*.h +*.qmlc +*.jsc Makefile* *build-* diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore index d5340449396..161179bff3e 100644 --- a/vendor/gitignore/Swift.gitignore +++ b/vendor/gitignore/Swift.gitignore @@ -37,6 +37,7 @@ playground.xcworkspace # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins +# Package.resolved .build/ # CocoaPods diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore index 41859c81f1c..f20453be963 100644 --- a/vendor/gitignore/Terraform.gitignore +++ b/vendor/gitignore/Terraform.gitignore @@ -1,5 +1,6 @@ # Compiled files *.tfstate +*.tfstate.*.backup *.tfstate.backup # Module directory diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 89c66054885..f652b45c2ee 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -116,6 +116,10 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + # Visual Studio code coverage results *.coverage *.coveragexml diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml new file mode 100644 index 00000000000..a07edd264c3 --- /dev/null +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -0,0 +1,411 @@ +# Auto DevOps +# This CI/CD configuration provides a standard pipeline for +# * building a Docker image (using a buildpack if necessary), +# * storing the image in the container registry, +# * running tests from a buildpack, +# * running code quality analysis, +# * creating a review app for each topic branch, +# * and continuous deployment to production +# +# In order to deploy, you must have a Kubernetes cluster configured either +# via a project integration, or via group/project variables. +# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project +# level, or manually added below. +# +# If you want to deploy to staging first, or enable canary deploys, +# uncomment the relevant jobs in the pipeline below. +# +# If Auto DevOps fails to detect the proper buildpack, or if you want to +# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the +# repository URL of the buildpack. +# e.g. BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git#v142 +# If you need multiple buildpacks, add a file to your project called +# `.buildpacks` that contains the URLs, one on each line, in order. +# Note: Auto CI does not work with multiple buildpacks yet + +image: alpine:latest + +variables: + # AUTO_DEVOPS_DOMAIN is the application deployment domain and should be set as a variable at the group or project level. + # AUTO_DEVOPS_DOMAIN: domain.example.com + + POSTGRES_USER: user + POSTGRES_PASSWORD: testing-password + POSTGRES_ENABLED: "true" + POSTGRES_DB: $CI_ENVIRONMENT_SLUG + +stages: + - build + - test + - review + - staging + - canary + - production + - cleanup + +build: + stage: build + image: docker:git + services: + - docker:dind + variables: + DOCKER_DRIVER: overlay2 + script: + - setup_docker + - build + only: + - branches + +test: + services: + - postgres:latest + variables: + POSTGRES_DB: test + stage: test + image: gliderlabs/herokuish:latest + script: + - setup_test_db + - cp -R . /tmp/app + - /bin/herokuish buildpack test + only: + - branches + +codequality: + image: docker:latest + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:dind + script: + - setup_docker + - codeclimate + artifacts: + paths: [codeclimate.json] + +review: + stage: review + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - install_tiller + - create_secret + - deploy + environment: + name: review/$CI_COMMIT_REF_NAME + url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$AUTO_DEVOPS_DOMAIN + on_stop: stop_review + only: + refs: + - branches + kubernetes: active + except: + - master + +stop_review: + stage: cleanup + variables: + GIT_STRATEGY: none + script: + - install_dependencies + - delete + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + when: manual + allow_failure: true + only: + refs: + - branches + kubernetes: active + except: + - master + +# Keys that start with a dot (.) will not be processed by GitLab CI. +# Staging and canary jobs are disabled by default, to enable them +# remove the dot (.) before the job name. +# https://docs.gitlab.com/ee/ci/yaml/README.html#hidden-keys + +# Staging deploys are disabled by default since +# continuous deployment to production is enabled by default +# If you prefer to automatically deploy to staging and +# only manually promote to production, enable this job by removing the dot (.), +# and uncomment the `when: manual` line in the `production` job. + +.staging: + stage: staging + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - install_tiller + - create_secret + - deploy + environment: + name: staging + url: http://$CI_PROJECT_PATH_SLUG-staging.$AUTO_DEVOPS_DOMAIN + only: + refs: + - master + kubernetes: active + +# Canaries are disabled by default, but if you want them, +# and know what the downsides are, enable this job by removing the dot (.), +# and uncomment the `when: manual` line in the `production` job. + +.canary: + stage: canary + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - install_tiller + - create_secret + - deploy canary + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN + when: manual + only: + refs: + - master + kubernetes: active + +# This job continuously deploys to production on every push to `master`. +# To make this a manual process, either because you're enabling `staging` +# or `canary` deploys, or you simply want more control over when you deploy +# to production, uncomment the `when: manual` line in the `production` job. + +production: + stage: production + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - install_tiller + - create_secret + - deploy + - delete canary + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN +# when: manual + only: + refs: + - master + kubernetes: active + +# --------------------------------------------------------------------------- + +.auto_devops: &auto_devops | + # Auto DevOps variables and functions + [[ "$TRACE" ]] && set -x + auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB} + export DATABASE_URL=${DATABASE_URL-$auto_database_url} + export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG + export CI_APPLICATION_TAG=$CI_COMMIT_SHA + export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID} + export TILLER_NAMESPACE=$KUBE_NAMESPACE + + function codeclimate() { + cc_opts="--env CODECLIMATE_CODE="$PWD" \ + --volume "$PWD":/code \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume /tmp/cc:/tmp/cc" + + docker run ${cc_opts} codeclimate/codeclimate init + docker run ${cc_opts} codeclimate/codeclimate analyze -f json > codeclimate.json + } + + function deploy() { + track="${1-stable}" + name="$CI_ENVIRONMENT_SLUG" + + if [[ "$track" != "stable" ]]; then + name="$name-$track" + fi + + replicas="1" + service_enabled="false" + postgres_enabled="$POSTGRES_ENABLED" + # canary uses stable db + [[ "$track" == "canary" ]] && postgres_enabled="false" + + env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' ) + env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' ) + + if [[ "$track" == "stable" ]]; then + # for stable track get number of replicas from `PRODUCTION_REPLICAS` + eval new_replicas=\$${env_slug}_REPLICAS + service_enabled="true" + else + # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS` + eval new_replicas=\$${env_track}_${env_slug}_REPLICAS + fi + if [[ -n "$new_replicas" ]]; then + replicas="$new_replicas" + fi + + helm upgrade --install \ + --wait \ + --set service.enabled="$service_enabled" \ + --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ + --set image.repository="$CI_APPLICATION_REPOSITORY" \ + --set image.tag="$CI_APPLICATION_TAG" \ + --set image.pullPolicy=IfNotPresent \ + --set application.track="$track" \ + --set application.database_url="$DATABASE_URL" \ + --set service.url="$CI_ENVIRONMENT_URL" \ + --set replicaCount="$replicas" \ + --set postgresql.enabled="$postgres_enabled" \ + --set postgresql.nameOverride="postgres" \ + --set postgresql.postgresUser="$POSTGRES_USER" \ + --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \ + --set postgresql.postgresDatabase="$POSTGRES_DB" \ + --namespace="$KUBE_NAMESPACE" \ + --version="$CI_PIPELINE_ID-$CI_JOB_ID" \ + "$name" \ + chart/ + + if [[ "$track" == "stable" ]]; then + kubectl rollout status -n "$KUBE_NAMESPACE" -w "deployment/${CI_ENVIRONMENT_SLUG}-auto-deploy" + fi + } + + function install_dependencies() { + apk add -U openssl curl tar gzip bash ca-certificates git + wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub + wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.23-r3/glibc-2.23-r3.apk + apk add glibc-2.23-r3.apk + rm glibc-2.23-r3.apk + + curl https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-amd64.tar.gz | tar zx + mv linux-amd64/helm /usr/bin/ + helm version --client + + curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl + chmod +x /usr/bin/kubectl + kubectl version --client + } + + function setup_docker() { + if ! docker info &>/dev/null; then + if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then + export DOCKER_HOST='tcp://localhost:2375' + fi + fi + } + + function setup_test_db() { + if [ -z ${KUBERNETES_PORT+x} ]; then + DB_HOST=postgres + else + DB_HOST=localhost + fi + export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}" + } + + function download_chart() { + if [[ ! -d chart ]]; then + auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app} + auto_chart_name=$(basename $auto_chart) + auto_chart_name=${auto_chart_name%.tgz} + else + auto_chart="chart" + auto_chart_name="chart" + fi + + helm init --client-only + helm repo add gitlab https://charts.gitlab.io + if [[ ! -d "$auto_chart" ]]; then + helm fetch ${auto_chart} --untar + fi + if [ "$auto_chart_name" != "chart" ]; then + mv ${auto_chart_name} chart + fi + + helm dependency update chart/ + helm dependency build chart/ + } + + function ensure_namespace() { + kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE" + } + + function check_kube_domain() { + if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then + echo "In order to deploy, AUTO_DEVOPS_DOMAIN must be set as a variable at the group or project level, or manually added in .gitlab-cy.yml" + false + else + true + fi + } + + function build() { + if [[ -f Dockerfile ]]; then + echo "Building Dockerfile-based application..." + docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . + else + echo "Building Heroku-based application using gliderlabs/herokuish docker image..." + docker run -i --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build + docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" + docker rm "$CI_CONTAINER_NAME" >/dev/null + echo "" + + echo "Configuring $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG docker image..." + docker create --expose 5000 --env PORT=5000 --name="$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" /bin/herokuish procfile start web + docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" + docker rm "$CI_CONTAINER_NAME" >/dev/null + echo "" + fi + + if [[ -n "$CI_REGISTRY_USER" ]]; then + echo "Logging to GitLab Container Registry with CI credentials..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + echo "" + fi + + echo "Pushing to GitLab Container Registry..." + docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" + echo "" + } + + function install_tiller() { + echo "Checking Tiller..." + helm init --upgrade + kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy" + if ! helm version --debug; then + echo "Failed to init Tiller." + return 1 + fi + echo "" + } + + function create_secret() { + kubectl create secret -n "$KUBE_NAMESPACE" \ + docker-registry gitlab-registry \ + --docker-server="$CI_REGISTRY" \ + --docker-username="$CI_REGISTRY_USER" \ + --docker-password="$CI_REGISTRY_PASSWORD" \ + --docker-email="$GITLAB_USER_EMAIL" \ + -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f - + } + + function delete() { + track="${1-stable}" + name="$CI_ENVIRONMENT_SLUG" + + if [[ "$track" != "stable" ]]; then + name="$name-$track" + fi + + helm delete "$name" || true + } + +before_script: + - *auto_devops diff --git a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml index 27537689b80..2d218b2e164 100644 --- a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml @@ -4,32 +4,32 @@ image: busybox:latest before_script: - - echo "Before script section" - - echo "For example you might run an update here or install a build dependency" - - echo "Or perhaps you might print out some debugging details" + - echo "Before script section" + - echo "For example you might run an update here or install a build dependency" + - echo "Or perhaps you might print out some debugging details" after_script: - echo "After script section" - echo "For example you might do some cleanup here" build1: - stage: build - script: - - echo "Do your build here" + stage: build + script: + - echo "Do your build here" test1: - stage: test - script: - - echo "Do a test here" - - echo "For example run a test suite" + stage: test + script: + - echo "Do a test here" + - echo "For example run a test suite" test2: - stage: test - script: - - echo "Do another parallel test here" - - echo "For example run a lint test" + stage: test + script: + - echo "Do another parallel test here" + - echo "For example run a lint test" deploy1: - stage: deploy - script: - - echo "Do your deploy here"
\ No newline at end of file + stage: deploy + script: + - echo "Do your deploy here" diff --git a/vendor/gitlab-ci-yml/Packer.gitlab-ci.yml b/vendor/gitlab-ci-yml/Packer.gitlab-ci.yml new file mode 100644 index 00000000000..fa296057c72 --- /dev/null +++ b/vendor/gitlab-ci-yml/Packer.gitlab-ci.yml @@ -0,0 +1,26 @@ +image: + name: hashicorp/packer:1.0.4 + entrypoint: + - '/usr/bin/env' + - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + +before_script: + - packer --version + +stages: + - validate + - deploy + +validate: + stage: validate + script: + - find . -maxdepth 1 -name '*.json' -print0 | xargs -t0n1 packer validate + +build: + stage: deploy + environment: production + script: + - find . -maxdepth 1 -name '*.json' -print0 | xargs -t0n1 packer build + when: manual + only: + - master diff --git a/vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml b/vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml new file mode 100644 index 00000000000..7160fce26a8 --- /dev/null +++ b/vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml @@ -0,0 +1,55 @@ +# Official image for Hashicorp's Terraform. It uses light image which is Alpine +# based as it is much lighter. +# +# Entrypoint is also needed as image by default set `terraform` binary as an +# entrypoint. +image: + name: hashicorp/terraform:light + entrypoint: + - '/usr/bin/env' + - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + +# Default output file for Terraform plan +variables: + PLAN: plan.tfplan + +cache: + paths: + - .terraform + +before_script: + - terraform --version + - terraform init + +stages: + - validate + - build + - deploy + +validate: + stage: validate + script: + - terraform validate + +plan: + stage: build + script: + - terraform plan -out=$PLAN + artifacts: + name: plan + paths: + - $PLAN + +# Separate apply job for manual launching Terraform as it can be destructive +# action. +apply: + stage: deploy + environment: + name: production + script: + - terraform apply -input=false $PLAN + dependencies: + - plan + when: manual + only: + - master 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..92b9860fbc0 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 |