diff options
482 files changed, 9656 insertions, 1883 deletions
diff --git a/.flayignore b/.flayignore index 44df2ba2371..fc64b0b5892 100644 --- a/.flayignore +++ b/.flayignore @@ -1,3 +1,4 @@ *.erb lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/diff/position_tracer.rb +app/policies/project_policy.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2141716311..65149ad2444 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -450,9 +450,9 @@ pages: script: - mv public/ .public/ - mkdir public/ - - mv coverage public/coverage-ruby - - mv coverage-javascript/default/ public/coverage-javascript/ - - mv eslint-report.html public/ + - mv coverage/ public/coverage-ruby/ || true + - mv coverage-javascript/default/ public/coverage-javascript/ || true + - mv eslint-report.html public/ || true artifacts: paths: - public diff --git a/CHANGELOG.md b/CHANGELOG.md index 25e02b1ae1c..71d38e5453d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.4 (2017-02-02) + +- Support non-ASCII characters in GFM autocomplete. !8729 +- Fix search bar search param encoding. !8753 +- Fix project name label's for reference in project settings. !8795 +- Fix filtering with multiple words. !8830 +- Fixed services form cancel not redirecting back the integrations settings view. !8843 +- Fix filtering usernames with multiple words. !8851 +- Improve performance of slash commands. !8876 +- Remove old project members when retrying an export. +- Fix permalink discussion note being collapsed. +- Add project ID index to `project_authorizations` table to optimize queries. +- Check public snippets for spam. +- 19164 Add settings dropdown to mobile screens. + ## 8.16.3 (2017-01-27) - Add caching of droplab ajax requests. !8725 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d1f3d3f926..315cd1e598c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ - [Issue weight](#issue-weight) - [Regression issues](#regression-issues) - [Technical debt](#technical-debt) + - [Stewardship][#stewardship] - [Merge requests](#merge-requests) - [Merge request guidelines](#merge-request-guidelines) - [Contribution acceptance criteria](#contribution-acceptance-criteria) @@ -230,6 +231,21 @@ for a release by the appropriate person. Make sure to mention the merge request that the `technical debt` issue is associated with in the description of the issue. +### Stewardship + +For issues related to the open source stewardship of GitLab, +there is the ~"stewardship" label. + +This label is to be used for issues in which the stewardship of GitLab +is a topic of discussion. For instance if GitLab Inc. is planning to remove +features from GitLab CE to make exclusive in GitLab EE, related issues +would be labelled with ~"stewardship". + +A recent example of this was the issue for +[bringing the time tracking API to GitLab CE][time-tracking-issue]. + +[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084 + ## Merge requests We welcome merge requests with fixes and improvements to GitLab code, tests, diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION new file mode 100644 index 00000000000..abd410582de --- /dev/null +++ b/GITLAB_PAGES_VERSION @@ -0,0 +1 @@ +0.2.4 @@ -47,6 +47,9 @@ gem 'rqrcode-rails3', '~> 0.1.7' gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' +# GitLab Pages +gem 'validates_hostname', '~> 1.0.6' + # Browser detection gem 'browser', '~> 2.2' diff --git a/Gemfile.lock b/Gemfile.lock index 82d03a86a77..c5dd6ab8d22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -785,6 +785,9 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + validates_hostname (1.0.6) + activerecord (>= 3.0) + activesupport (>= 3.0) version_sorter (2.1.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -998,6 +1001,7 @@ DEPENDENCIES unf (~> 0.1.4) unicorn (~> 5.1.0) unicorn-worker-killer (~> 0.4.4) + validates_hostname (~> 1.0.6) version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.3.0) diff --git a/PROCESS.md b/PROCESS.md index 993d60bbba8..6eabaf05d24 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -33,7 +33,7 @@ core team members will mention this person. ### Merge request coaching Several people from the [GitLab team][team] are helping community members to get -their contributions accepted by meeting our [Definition of done][CONTRIBUTING.md#definition-of-done]. +their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done). What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. @@ -79,47 +79,47 @@ not be merged into any stable branches. ### Improperly formatted issue -Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +Thanks for the issue report. Please reformat your issue to conform to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Issue report for old version -Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Support requests and configuration questions Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please check our -\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available -support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) +[getting help](https://about.gitlab.com/getting-help/) page to see all of the available +support options. Also, have a look at the [contribution guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. ### Code format -Please use ``` to format console output, logs, and code as it's very hard to read otherwise. +Please use \`\`\` to format console output, logs, and code as it's very hard to read otherwise. ### Issue fixed in newer version -Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please [upgrade](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Improperly formatted merge request -Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines). +Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines). ### Inactivity close of an issue -It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Inactivity close of a merge request -This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request. +This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request. ### Accepting merge requests Is there an issue on the -\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is +[issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) that is similar to this? Could you please link it here? Please be aware that new functionality that is not marked -\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) +[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) might not make it into GitLab. ### Only accepting merge requests with green tests @@ -134,7 +134,7 @@ rebase with master to see if that solves the issue. We are currently in the process of closing down the issue tracker on GitHub, to prevent duplication with the GitLab.com issue tracker. Since this is an older issue I'll be closing this for now. If you think this is -still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues). +still an issue I encourage you to open it on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). [team]: https://about.gitlab.com/team/ [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 637fca4d4da..ea3f13bd00f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -247,5 +247,7 @@ window.ES6Promise.polyfill(); new Aside(); // bind sidebar events new gl.Sidebar(); + + gl.utils.initTimeagoTimeout(); }); }).call(this); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index e3241974e59..c345fb6ce14 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -16,7 +16,7 @@ require('./components/board'); require('./components/board_sidebar'); require('./components/new_list_dropdown'); require('./components/modal/index'); -require('./vue_resource_interceptor'); +require('../vue_shared/vue_resource_interceptor'); $(() => { const $boardApp = document.getElementById('board-app'); diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 new file mode 100644 index 00000000000..6de06811d94 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -0,0 +1,49 @@ +/* global Vue */ +const userFilter = require('./filters/user'); +const milestoneFilter = require('./filters/milestone'); +const labelFilter = require('./filters/label'); + +module.exports = Vue.extend({ + name: 'modal-filters', + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, + destroyed() { + gl.issueBoards.ModalStore.setDefaultFilter(); + }, + components: { + userFilter, + milestoneFilter, + labelFilter, + }, + template: ` + <div class="modal-filters"> + <user-filter + dropdown-class-name="dropdown-menu-author" + toggle-class-name="js-user-search js-author-search" + toggle-label="Author" + field-name="author_id" + :project-id="projectId"></user-filter> + <user-filter + dropdown-class-name="dropdown-menu-author" + toggle-class-name="js-assignee-search" + toggle-label="Assignee" + field-name="assignee_id" + :null-user="true" + :project-id="projectId"></user-filter> + <milestone-filter :milestone-path="milestonePath"></milestone-filter> + <label-filter :label-path="labelPath"></label-filter> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 new file mode 100644 index 00000000000..4fc8f72a145 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 @@ -0,0 +1,54 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global LabelsSelect */ +module.exports = Vue.extend({ + name: 'filter-label', + props: { + labelPath: { + type: String, + required: true, + }, + }, + mounted() { + new LabelsSelect(this.$refs.dropdown); + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options" + type="button" + data-toggle="dropdown" + data-show-any="true" + data-show-no="true" + :data-labels="labelPath" + ref="dropdown"> + <span class="dropdown-toggle-text"> + Label + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> + <div class="dropdown-title"> + Filter by label + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 new file mode 100644 index 00000000000..d555599d300 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 @@ -0,0 +1,55 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global MilestoneSelect */ +module.exports = Vue.extend({ + name: 'filter-milestone', + props: { + milestonePath: { + type: String, + required: true, + }, + }, + mounted() { + new MilestoneSelect(null, this.$refs.dropdown); + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-milestone-select" + type="button" + data-toggle="dropdown" + data-show-any="true" + data-show-upcoming="true" + data-field-name="milestone_title" + :data-milestones="milestonePath" + ref="dropdown"> + <span class="dropdown-toggle-text"> + Milestone + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> + <div class="dropdown-title"> + <span>Filter by milestone</span> + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search milestones" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 new file mode 100644 index 00000000000..8523028c29c --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 @@ -0,0 +1,96 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global UsersSelect */ +module.exports = Vue.extend({ + name: 'filter-user', + props: { + toggleClassName: { + type: String, + required: true, + }, + dropdownClassName: { + type: String, + required: false, + default: '', + }, + toggleLabel: { + type: String, + required: true, + }, + fieldName: { + type: String, + required: true, + }, + nullUser: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: true, + }, + }, + mounted() { + new UsersSelect(null, this.$refs.dropdown); + }, + computed: { + currentUsername() { + return gon.current_username; + }, + dropdownTitle() { + return `Filter by ${this.toggleLabel.toLowerCase()}`; + }, + inputPlaceholder() { + return `Search ${this.toggleLabel.toLowerCase()}`; + }, + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-user-search" + :class="toggleClassName" + type="button" + data-toggle="dropdown" + data-current-user="true" + :data-any-user="'Any ' + toggleLabel" + :data-null-user="nullUser" + :data-field-name="fieldName" + :data-project-id="projectId" + :data-first-user="currentUsername" + ref="dropdown"> + <span class="dropdown-toggle-text"> + {{ toggleLabel }} + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div + class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable" + :class="dropdownClassName"> + <div class="dropdown-title"> + {{ dropdownTitle }} + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + autocomplete="off" + :placeholder="inputPlaceholder" /> + <i class="fa fa-search dropdown-input-search"></i> + <i + role="button" + class="fa fa-times dropdown-input-clear js-dropdown-input-clear"> + </i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, +}); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index ab903722ba4..70c088f9054 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -1,12 +1,26 @@ /* global Vue */ - require('./tabs'); +const modalFilters = require('./filters'); (() => { const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalHeader = Vue.extend({ mixins: [gl.issueBoards.ModalMixins], + props: { + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, + }, data() { return ModalStore.store; }, @@ -31,6 +45,7 @@ require('./tabs'); }, components: { 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, }, template: ` <div> @@ -51,6 +66,11 @@ require('./tabs'); <div class="add-issues-search append-bottom-10" v-if="showSearch"> + <modal-filters + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> + </modal-filters> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index d367b7e4246..f290cd13763 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -27,6 +27,18 @@ require('./empty_state'); type: String, required: true, }, + projectId: { + type: Number, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -52,17 +64,27 @@ require('./empty_state'); this.issuesCount = false; } }, + filter: { + handler() { + this.loadIssues(true); + }, + deep: true, + }, }, methods: { searchOperation: _.debounce(function searchOperationDebounce() { this.loadIssues(true); }, 500), loadIssues(clearIssues = false) { - return gl.boardService.getBacklog({ + if (!this.showAddIssuesModal) return false; + + const queryData = Object.assign({}, this.filter, { search: this.searchTerm, page: this.page, per: this.perPage, - }).then((res) => { + }); + + return gl.boardService.getBacklog(queryData).then((res) => { const data = res.json(); if (clearIssues) { @@ -112,8 +134,13 @@ require('./empty_state'); class="add-issues-modal" v-if="showAddIssuesModal"> <div class="add-issues-container"> - <modal-header></modal-header> + <modal-header + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> + </modal-header> <modal-list + :image="blankStateImage" :issue-link-base="issueLinkBase" :root-path="rootPath" v-if="!loading && showList"></modal-list> diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index d0901219216..3730c1ecaeb 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -14,6 +14,10 @@ type: String, required: true, }, + image: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -111,6 +115,19 @@ class="add-issues-list add-issues-list-columns" ref="list"> <div + class="empty-state add-issues-empty-state-filter text-center" + v-if="issuesCount > 0 && issues.length === 0"> + <div + class="svg-content" + v-html="image"> + </div> + <div class="text-content"> + <h4> + There are no issues to show. + </h4> + </div> + </div> + <div v-for="group in groupedIssues" class="add-issues-list-column"> <div diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 73518b42b84..15fc6c79e8d 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -18,6 +18,17 @@ page: 1, perPage: 50, }; + + this.setDefaultFilter(); + } + + setDefaultFilter() { + this.store.filter = { + author_id: '', + assignee_id: '', + milestone_title: '', + label_name: [], + }; } selectedCount() { diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 deleted file mode 100644 index 54c2b4ad369..00000000000 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ -/* global Vue */ - -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next(function (response) { - Vue.activeResources -= 1; - }); -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 new file mode 100644 index 00000000000..fbfec7743c7 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -0,0 +1,26 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +window.Vue = require('vue'); +require('./pipelines_table'); +/** + * Commits View > Pipelines Tab > Pipelines Table. + * Merge Request View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * Renders Pipelines table in pipelines tab in the merge request show view. + */ + +$(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); + } + + gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); +}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 new file mode 100644 index 00000000000..483b414126a --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -0,0 +1,29 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +/** + * Pipelines service. + * + * Used to fetch the data used to render the pipelines table. + * Uses Vue.Resource + */ +class PipelinesService { + constructor(endpoint) { + this.pipelines = Vue.resource(endpoint); + } + + /** + * Given the root param provided when the class is initialized, will + * make a GET request. + * + * @return {Promise} + */ + all() { + return this.pipelines.get(); + } +} + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 new file mode 100644 index 00000000000..f1b41911b73 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -0,0 +1,50 @@ +/* eslint-disable no-underscore-dangle*/ +/** + * Pipelines' Store for commits view. + * + * Used to store the Pipelines rendered in the commit view in the pipelines table. + */ + +class PipelinesStore { + constructor() { + this.state = {}; + this.state.pipelines = []; + } + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + + return pipelines; + } + + /** + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } +} + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesStore = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 new file mode 100644 index 00000000000..ce0dbd4d56b --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -0,0 +1,107 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); +require('../../vue_shared/components/pipelines_table'); +require('../../vue_realtime_listener/index'); +require('./pipelines_service'); +require('./pipelines_store'); + +/** + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as `endpoint`. + * We need a store to store the received environemnts. + * We need a service to communicate with the server. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ + +(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = new gl.commits.pipelines.PipelinesStore(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = gl.utils.DOMStringMapToObject(svgsData); + + return { + endpoint: pipelinesTableData.endpoint, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, + }; + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); + + this.isLoading = true; + return pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.storePipelines(json); + this.store.startTimeAgoLoops.call(this, Vue); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); + }); + }, + + template: ` + <div> + <div class="pipelines realtime-loading" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> + + <div class="table-holder pipelines" + v-if="!isLoading && state.pipelines.length > 0"> + <pipelines-table-component + :pipelines="state.pipelines" + :svgs="svgs"> + </pipelines-table-component> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index edec21e3b63..f8efca76b13 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -19,7 +19,6 @@ /* global UsersSelect */ /* global GroupAvatar */ /* global LineHighlighter */ -/* global ShortcutsBlob */ /* global ProjectFork */ /* global BuildArtifacts */ /* global GroupsSelect */ @@ -36,6 +35,8 @@ /* global Labels */ /* global Shortcuts */ +const ShortcutsBlob = require('./shortcuts_blob'); + (function() { var Dispatcher; @@ -162,7 +163,7 @@ case 'projects:commit:pipelines': new gl.MiniPipelineGraph({ container: '.js-pipeline-table', - }); + }).bindEvents(); break; case 'projects:commits:show': case 'projects:activity': @@ -225,7 +226,12 @@ case 'projects:blame:show': new LineHighlighter(); shortcut_handler = new ShortcutsNavigation(); - new ShortcutsBlob(true); + const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + new ShortcutsBlob({ + skipResetBindings: true, + fileBlobPermalinkUrl, + }); break; case 'groups:labels:new': case 'groups:labels:edit': @@ -259,7 +265,7 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; - case 'projects:variables:index': + case 'projects:ci_cd:show': new gl.ProjectVariables(); break; case 'ci:lints:create': diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 521873b14b4..33a99231315 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -4,7 +4,7 @@ window.Vue = require('vue'); window.timeago = require('vendor/timeago'); require('../../lib/utils/text_utility'); -require('../../vue_common_component/commit'); +require('../../vue_shared/components/commit'); require('./environment_actions'); require('./environment_external_url'); require('./environment_stop'); @@ -147,12 +147,12 @@ require('./environment_terminal_button'); }, /** - * Returns the value of the `stoppable?` key provided in the response. + * Returns the value of the `stop_action?` key provided in the response. * * @returns {Boolean} */ - isStoppable() { - return this.model['stoppable?']; + hasStopAction() { + return this.model['stop_action?']; }, /** @@ -508,7 +508,7 @@ require('./environment_terminal_button'); </external-url-component> </div> - <div v-if="isStoppable && canCreateDeployment" + <div v-if="hasStopAction && canCreateDeployment" class="inline js-stop-component-container"> <stop-component :stop-url="model.stop_path"> diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 58f4c6eadb2..05c59d92fd4 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,8 +1,7 @@ window.Vue = require('vue'); - require('./stores/environments_store'); require('./components/environment'); -require('./vue_resource_interceptor'); +require('../vue_shared/vue_resource_interceptor'); $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 deleted file mode 100644 index 406bdbc1c7d..00000000000 --- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/* global Vue */ -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); // eslint-disable-line - } - - Vue.activeResources--; // eslint-disable-line - }); -}); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index f93605a5a21..7e9c6f74aa5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -8,7 +8,7 @@ require('./filtered_search_dropdown'); super(droplab, dropdown, input, filter); this.config = { droplabAjaxFilter: { - endpoint: '/autocomplete/users.json', + endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search', params: { per_page: 20, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 70dc0d06b7b..e4cf9057e6d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,10 +4,17 @@ (function() { this.LabelsSelect = (function() { - function LabelsSelect() { - var _this; + function LabelsSelect(els) { + var _this, $els; _this = this; - $('.js-label-select').each(function(i, dropdown) { + + $els = $(els); + + if (!els) { + $els = $('.js-label-select'); + } + + $els.each(function(i, dropdown) { var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); @@ -324,7 +331,7 @@ multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(label, $el, e, isMarking) { - var isIssueIndex, isMRIndex, page; + var isIssueIndex, isMRIndex, page, boardsModel; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; @@ -346,22 +353,31 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && + !$dropdown.closest('.add-issues-modal').length) { + boardsModel = gl.issueBoards.BoardsStore.state.filters; + } else if ($dropdown.closest('.add-issues-modal').length) { + boardsModel = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsModel) { if (label.isAny) { - gl.issueBoards.BoardsStore.state.filters['label_name'] = []; + boardsModel['label_name'] = []; } else if ($el.hasClass('is-active')) { - gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title); + boardsModel['label_name'].push(label.title); } else { - var filters = gl.issueBoards.BoardsStore.state.filters['label_name']; + var filters = boardsModel['label_name']; filters = filters.filter(function (filteredLabel) { return filteredLabel !== label.title; }); - gl.issueBoards.BoardsStore.state.filters['label_name'] = filters; + boardsModel['label_name'] = filters; } - gl.issueBoards.BoardsStore.updateFiltersUrl(); + if (!$dropdown.closest('.add-issues-modal').length) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } e.preventDefault(); return; } diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index e3bff2559fd..5becf688652 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -69,6 +69,9 @@ var hash = w.gl.utils.getLocationHash(); if (!hash) return; + // This is required to handle non-unicode characters in hash + hash = decodeURIComponent(hash); + var navbar = document.querySelector('.navbar-gitlab'); var subnav = document.querySelector('.layout-nav'); var fixedTabs = document.querySelector('.js-tabs-affix'); @@ -134,6 +137,14 @@ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; + gl.utils.isMetaClick = function(e) { + // Identify following special clicks + // 1) Cmd + Click on Mac (e.metaKey) + // 2) Ctrl + Click on PC (e.ctrlKey) + // 3) Middle-click or Mouse Wheel Click (e.which is 2) + return e.metaKey || e.ctrlKey || e.which === 2; + }; + gl.utils.scrollToElement = function($el) { var top = $el.offset().top; gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); @@ -230,5 +241,16 @@ return upperCaseHeaders; }; + + /** + * Transforms a DOMStringMap into a plain object. + * + * @param {DOMStringMap} DOMStringMapObject + * @returns {Object} + */ + w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => { + acc[element] = DOMStringMapObject[element]; + return acc; + }, {}); })(window); }).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js deleted file mode 100644 index 5128ffd8c6f..00000000000 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ /dev/null @@ -1,101 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */ -/* global timeago */ -/* global dateFormat */ - -window.timeago = require('vendor/timeago'); -window.dateFormat = require('vendor/date.format'); - -(function() { - (function(w) { - var base; - if (w.gl == null) { - w.gl = {}; - } - if ((base = w.gl).utils == null) { - base.utils = {}; - } - w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - - w.gl.utils.formatDate = function(datetime) { - return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); - }; - - w.gl.utils.getDayName = function(date) { - return this.days[date.getDay()]; - }; - - w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { - if (setTimeago == null) { - setTimeago = true; - } - - $timeagoEls.filter(':not([data-timeago-rendered])').each(function() { - var $el = $(this); - $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); - - if (setTimeago) { - // Recreate with custom template - $el.tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - }); - } - - $el.attr('data-timeago-rendered', true); - gl.utils.renderTimeago($el); - }); - }; - - w.gl.utils.getTimeago = function() { - var locale = function(number, index) { - return [ - ['less than a minute ago', 'a while'], - ['less than a minute ago', 'in %s seconds'], - ['about a minute ago', 'in 1 minute'], - ['%s minutes ago', 'in %s minutes'], - ['about an hour ago', 'in 1 hour'], - ['about %s hours ago', 'in %s hours'], - ['a day ago', 'in 1 day'], - ['%s days ago', 'in %s days'], - ['a week ago', 'in 1 week'], - ['%s weeks ago', 'in %s weeks'], - ['a month ago', 'in 1 month'], - ['%s months ago', 'in %s months'], - ['a year ago', 'in 1 year'], - ['%s years ago', 'in %s years'] - ][index]; - }; - - timeago.register('gl_en', locale); - return timeago(); - }; - - w.gl.utils.timeFor = function(time, suffix, expiredLabel) { - var timefor; - if (!time) { - return ''; - } - suffix || (suffix = 'remaining'); - expiredLabel || (expiredLabel = 'Past due'); - timefor = gl.utils.getTimeago().format(time).replace('in', ''); - if (timefor.indexOf('ago') > -1) { - timefor = expiredLabel; - } else { - timefor = timefor.trim() + ' ' + suffix; - } - return timefor; - }; - - w.gl.utils.renderTimeago = function($element) { - var timeagoInstance = gl.utils.getTimeago(); - timeagoInstance.render($element, 'gl_en'); - }; - - w.gl.utils.getDayDifference = function(a, b) { - var millisecondsPerDay = 1000 * 60 * 60 * 24; - var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); - var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); - - return Math.floor((date2 - date1) / millisecondsPerDay); - }; - })(window); -}).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6 new file mode 100644 index 00000000000..56300926188 --- /dev/null +++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6 @@ -0,0 +1,126 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */ +/* global timeago */ +/* global dateFormat */ + +window.timeago = require('vendor/timeago'); +window.dateFormat = require('vendor/date.format'); + +(function() { + (function(w) { + var base; + var timeagoInstance; + + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + + w.gl.utils.formatDate = function(datetime) { + return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); + }; + + w.gl.utils.getDayName = function(date) { + return this.days[date.getDay()]; + }; + + w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { + $timeagoEls.each((i, el) => { + el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime'))); + + if (setTimeago) { + // Recreate with custom template + $(el).tooltip({ + template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + }); + } + + el.classList.add('js-timeago-render'); + }); + + gl.utils.renderTimeago($timeagoEls); + }; + + w.gl.utils.getTimeago = function() { + var locale; + + if (!timeagoInstance) { + locale = function(number, index) { + return [ + ['less than a minute ago', 'a while'], + ['less than a minute ago', 'in %s seconds'], + ['about a minute ago', 'in 1 minute'], + ['%s minutes ago', 'in %s minutes'], + ['about an hour ago', 'in 1 hour'], + ['about %s hours ago', 'in %s hours'], + ['a day ago', 'in 1 day'], + ['%s days ago', 'in %s days'], + ['a week ago', 'in 1 week'], + ['%s weeks ago', 'in %s weeks'], + ['a month ago', 'in 1 month'], + ['%s months ago', 'in %s months'], + ['a year ago', 'in 1 year'], + ['%s years ago', 'in %s years'] + ][index]; + }; + + timeago.register('gl_en', locale); + timeagoInstance = timeago(); + } + + return timeagoInstance; + }; + + w.gl.utils.timeFor = function(time, suffix, expiredLabel) { + var timefor; + if (!time) { + return ''; + } + suffix || (suffix = 'remaining'); + expiredLabel || (expiredLabel = 'Past due'); + timefor = gl.utils.getTimeago().format(time).replace('in', ''); + if (timefor.indexOf('ago') > -1) { + timefor = expiredLabel; + } else { + timefor = timefor.trim() + ' ' + suffix; + } + return timefor; + }; + + w.gl.utils.cachedTimeagoElements = []; + w.gl.utils.renderTimeago = function($els) { + if (!$els && !w.gl.utils.cachedTimeagoElements.length) { + w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render')); + } else if ($els) { + w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray()); + } + + w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText); + }; + + w.gl.utils.updateTimeagoText = function(el) { + const timeago = gl.utils.getTimeago(); + const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en'); + + if (el.textContent !== formattedDate) { + el.textContent = formattedDate; + } + }; + + w.gl.utils.initTimeagoTimeout = function() { + gl.utils.renderTimeago(); + + gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); + }; + + w.gl.utils.getDayDifference = function(a, b) { + var millisecondsPerDay = 1000 * 60 * 60 * 24; + var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); + var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); + + return Math.floor((date2 - date1) / millisecondsPerDay); + }; + })(window); +}).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 7e74bebb81e..af1ba9ecaf3 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -61,7 +61,6 @@ require('./flash'); constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; - this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; @@ -83,12 +82,18 @@ require('./flash'); $(document) .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('click', '.js-show-tab', this.showTab); + + $('.merge-request-tabs a[data-toggle="tab"]') + .on('click', this.clickTab); } unbindEvents() { $(document) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('click', '.js-show-tab', this.showTab); + + $('.merge-request-tabs a[data-toggle="tab"]') + .off('click', this.clickTab); } showTab(e) { @@ -96,6 +101,14 @@ require('./flash'); this.activateTab($(e.target).data('action')); } + clickTab(e) { + if (e.target && gl.utils.isMetaClick(e)) { + const targetLink = e.target.getAttribute('href'); + e.stopImmediatePropagation(); + window.open(targetLink, '_blank'); + } + } + tabShown(e) { const $target = $(e.target); const action = $target.data('action'); @@ -116,10 +129,6 @@ require('./flash'); $.scrollTo('.merge-request-details .merge-request-tabs', { offset: -navBarHeight, }); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); } else { this.expandView(); this.resetViewContainer(); @@ -244,25 +253,6 @@ require('./flash'); }); } - loadPipelines(source) { - if (this.pipelinesLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - this.scrollToElement('#pipelines'); - - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - }, - }); - } - // Show or hide the loading spinner // // status - Boolean, true to show, false to hide diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 05b9a63765f..e5d2d706fc7 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -51,6 +51,8 @@ require('./smart_interval'); this.getCIStatus(false); this.retrieveSuccessIcon(); + this.initMiniPipelineGraph(); + this.ciStatusInterval = new global.SmartInterval({ callback: this.getCIStatus.bind(this, true), startingInterval: 10000, @@ -66,6 +68,7 @@ require('./smart_interval'); incrementByFactorOf: 15000, immediateExecution: true, }); + notifyPermissions(); } @@ -236,17 +239,20 @@ require('./smart_interval'); case "failed": case "canceled": case "not_found": - return this.setMergeButtonClass('btn-danger'); + this.setMergeButtonClass('btn-danger'); + break; case "running": - return this.setMergeButtonClass('btn-info'); + this.setMergeButtonClass('btn-info'); + break; case "success": case "success_with_warnings": - return this.setMergeButtonClass('btn-create'); + this.setMergeButtonClass('btn-create'); } } else { $('.ci_widget.ci-error').show(); - return this.setMergeButtonClass('btn-danger'); + this.setMergeButtonClass('btn-danger'); } + this.initMiniPipelineGraph(); }; MergeRequestWidget.prototype.showCICoverage = function(coverage) { @@ -269,6 +275,12 @@ require('./smart_interval'); $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); }; + MergeRequestWidget.prototype.initMiniPipelineGraph = function() { + new gl.MiniPipelineGraph({ + container: '.js-pipeline-inline-mr-widget-graph:visible', + }).bindEvents(); + }; + return MergeRequestWidget; })(); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 7ab39ffbd05..2f08aa7fe8b 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,13 +5,20 @@ (function() { this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject) { - var _this; + function MilestoneSelect(currentProject, els) { + var _this, $els; if (currentProject != null) { _this = this; this.currentProject = JSON.parse(currentProject); } - $('.js-milestone-select').each(function(i, dropdown) { + + $els = $(els); + + if (!els) { + $els = $('.js-milestone-select'); + } + + $els.each(function(i, dropdown) { var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); @@ -108,7 +115,7 @@ }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(selected, $el, e) { - var data, isIssueIndex, isMRIndex, page; + var data, isIssueIndex, isMRIndex, page, boardsStore; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); @@ -116,9 +123,19 @@ e.preventDefault(); return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { - gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; - gl.issueBoards.BoardsStore.updateFiltersUrl(); + + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && + !$dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.BoardsStore.state.filters; + } else if ($dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsStore) { + boardsStore[$dropdown.data('field-name')] = selected.name; + if (!$dropdown.closest('.add-issues-modal').length) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (selected.name != null) { diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 80549532ea9..4becbc32681 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -21,8 +21,6 @@ this.container = opts.container || ''; this.dropdownListSelector = '.js-builds-dropdown-container'; this.getBuildsList = this.getBuildsList.bind(this); - - this.bindEvents(); } /** diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js deleted file mode 100644 index a3e549a2735..00000000000 --- a/app/assets/javascripts/shortcuts_blob.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */ -/* global Shortcuts */ -/* global Mousetrap */ - -require('./shortcuts'); - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.ShortcutsBlob = (function(superClass) { - extend(ShortcutsBlob, superClass); - - function ShortcutsBlob(skipResetBindings) { - ShortcutsBlob.__super__.constructor.call(this, skipResetBindings); - Mousetrap.bind('y', ShortcutsBlob.copyToClipboard); - } - - ShortcutsBlob.copyToClipboard = function() { - var clipboardButton; - clipboardButton = $('.btn-clipboard'); - if (clipboardButton) { - return clipboardButton.click(); - } - }; - - return ShortcutsBlob; - })(Shortcuts); -}).call(this); diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js.es6 new file mode 100644 index 00000000000..bfe90aef71e --- /dev/null +++ b/app/assets/javascripts/shortcuts_blob.js.es6 @@ -0,0 +1,29 @@ +/* global Mousetrap */ +/* global Shortcuts */ + +require('./shortcuts'); + +const defaults = { + skipResetBindings: false, + fileBlobPermalinkUrl: null, +}; + +class ShortcutsBlob extends Shortcuts { + constructor(opts) { + const options = Object.assign({}, defaults, opts); + super(options.skipResetBindings); + this.options = options; + + Mousetrap.bind('y', this.moveToFilePermalink.bind(this)); + } + + moveToFilePermalink() { + if (this.options.fileBlobPermalinkUrl) { + const hash = gl.utils.getLocationHash(); + const hashUrlString = hash ? `#${hash}` : ''; + gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`); + } + } +} + +module.exports = ShortcutsBlob; diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 96c7d927509..b07e62a8c30 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -146,14 +146,26 @@ } goToTodoUrl(e) { - const todoLink = $(this).data('url'); + const todoLink = this.dataset.url; + let targetLink = e.target.getAttribute('href'); + + if (e.target.tagName === 'IMG') { // See if clicked target was Avatar + targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link + } + if (!todoLink) { return; } - // Allow Meta-Click or Mouse3-click to open in a new tab - if (e.metaKey || e.which === 2) { + + if (gl.utils.isMetaClick(e)) { e.preventDefault(); - return window.open(todoLink, '_blank'); + // Meta-Click on username leads to different URL than todoLink. + // Turbolinks can resolve that URL, but window.open requires URL manually. + if (targetLink !== todoLink) { + return window.open(targetLink, '_blank'); + } else { + return window.open(todoLink, '_blank'); + } } else { return gl.utils.visitUrl(todoLink); } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 77d2764cdf0..d4b24d13299 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -8,7 +8,8 @@ slice = [].slice; this.UsersSelect = (function() { - function UsersSelect(currentUser) { + function UsersSelect(currentUser, els) { + var $els; this.users = bind(this.users, this); this.user = bind(this.user, this); this.usersPath = "/autocomplete/users.json"; @@ -20,7 +21,14 @@ this.currentUser = JSON.parse(currentUser); } } - $('.js-user-search').each((function(_this) { + + $els = $(els); + + if (!els) { + $els = $('.js-user-search'); + } + + $els.each((function(_this) { return function(i, dropdown) { var options = {}; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; @@ -193,7 +201,9 @@ selectedId = user.id; return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($el.closest('.add-issues-modal').length) { + gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { selectedId = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.updateFiltersUrl(); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index e1bebe0fe5b..e7432afb56e 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,41 +1,36 @@ +/* eslint-disable no-param-reassign */ /* global Vue, VueResource, gl */ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -require('../vue_common_component/commit'); -require('../vue_pagination/index'); -require('../boards/vue_resource_interceptor'); -require('./status'); -require('./store'); -require('./pipeline_url'); -require('./stage'); -require('./stages'); -require('./pipeline_actions'); -require('./time_ago'); +require('../lib/utils/common_utils'); +require('../vue_shared/vue_resource_interceptor'); require('./pipelines'); -(() => { - const project = document.querySelector('.pipelines'); - const entry = document.querySelector('.vue-pipelines-index'); - const svgs = document.querySelector('.pipeline-svgs'); +$(() => new Vue({ + el: document.querySelector('.vue-pipelines-index'), - if (!entry) return null; - return new Vue({ - el: entry, - data: { + data() { + const project = document.querySelector('.pipelines'); + const svgs = document.querySelector('.pipeline-svgs').dataset; + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = gl.utils.DOMStringMapToObject(svgs); + + return { scope: project.dataset.url, store: new gl.PipelineStore(), - svgs: svgs.dataset, - }, - components: { - 'vue-pipelines': gl.VuePipelines, - }, - template: ` - <vue-pipelines - :scope='scope' - :store='store' - :svgs='svgs' - > - </vue-pipelines> - `, - }); -})(); + svgs: svgsObject, + }; + }, + components: { + 'vue-pipelines': gl.VuePipelines, + }, + template: ` + <vue-pipelines + :scope='scope' + :store='store' + :svgs='svgs' + > + </vue-pipelines> + `, +})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 01f8b6519a4..8106934e864 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -50,9 +50,9 @@ <button v-if='artifacts' class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" - data-toggle="dropdown" title="Artifacts" data-placement="top" + data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> @@ -81,8 +81,7 @@ data-placement="top" data-toggle="dropdown" :href='pipeline.retry_path' - aria-label="Retry" - > + aria-label="Retry"> <i class="fa fa-repeat" aria-hidden="true"></i> </a> <a @@ -94,8 +93,7 @@ data-placement="top" data-toggle="dropdown" :href='pipeline.cancel_path' - aria-label="Cancel" - > + aria-label="Cancel"> <i class="fa fa-remove" aria-hidden="true"></i> </a> </div> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 194bbae07d9..e47dc6935d6 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,19 +1,19 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +window.Vue = require('vue'); +require('../vue_shared/components/table_pagination'); +require('./store'); +require('../vue_shared/components/pipelines_table'); + ((gl) => { gl.VuePipelines = Vue.extend({ + components: { - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - stages: gl.VueStages, - commit: gl.CommitComponent, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - glPagination: gl.VueGlPagination, - statusScope: gl.VueStatusScope, - timeAgo: gl.VueTimeAgo, + 'gl-pagination': gl.VueGlPagination, + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, + data() { return { pipelines: [], @@ -38,87 +38,29 @@ change(pagenum, apiScope) { gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, - ref(pipeline) { - const { ref } = pipeline; - return { name: ref.name, tag: ref.tag, ref_url: ref.path }; - }, - commitTitle(pipeline) { - return pipeline.commit ? pipeline.commit.title : ''; - }, - commitSha(pipeline) { - return pipeline.commit ? pipeline.commit.short_id : ''; - }, - commitUrl(pipeline) { - return pipeline.commit ? pipeline.commit.commit_path : ''; - }, - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, }, template: ` <div> - <div class="pipelines realtime-loading" v-if='pipelines.length < 1'> + <div class="pipelines realtime-loading" v-if='pageRequest'> <i class="fa fa-spinner fa-spin"></i> </div> - <div class="table-holder" v-if='pipelines.length'> - <table class="table ci-table"> - <thead> - <tr> - <th class="pipeline-status">Status</th> - <th class="pipeline-info">Pipeline</th> - <th class="pipeline-commit">Commit</th> - <th class="pipeline-stages">Stages</th> - <th class="pipeline-date"></th> - <th class="pipeline-actions hidden-xs"></th> - </tr> - </thead> - <tbody> - <tr class="commit" v-for='pipeline in pipelines'> - <status-scope - :pipeline='pipeline' - :match='match' - :svgs='svgs' - > - </status-scope> - <pipeline-url :pipeline='pipeline'></pipeline-url> - <td> - <commit - :commit-icon-svg='svgs.commitIconSvg' - :author='author(pipeline)' - :tag="pipeline.ref.tag" - :title='commitTitle(pipeline)' - :commit-ref='ref(pipeline)' - :short-sha='commitSha(pipeline)' - :commit-url='commitUrl(pipeline)' - > - </commit> - </td> - <stages - :pipeline='pipeline' - :svgs='svgs' - :match='match' - > - </stages> - <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> - <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> - </tr> - </tbody> - </table> + + <div class="blank-state blank-state-no-icon" + v-if="!pageRequest && pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> </div> - <div class="pipelines realtime-loading" v-if='pageRequest'> - <i class="fa fa-spinner fa-spin"></i> + + <div class="table-holder" v-if='!pageRequest && pipelines.length'> + <pipelines-table-component + :pipelines='pipelines' + :svgs='svgs'> + </pipelines-table-component> </div> + <gl-pagination - v-if='pageInfo.total > pageInfo.perPage' + v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage' :pagenum='pagenum' :change='change' :count='count.all' diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 496df9aaced..8cc417a9966 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -15,7 +15,7 @@ required: true, }, svgs: { - type: DOMStringMap, + type: Object, required: true, }, match: { diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 deleted file mode 100644 index cb176b3f0c6..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueStages = Vue.extend({ - components: { - 'vue-stage': gl.VueStage, - }, - props: ['pipeline', 'svgs', 'match'], - template: ` - <td class="stage-cell"> - <div - class="stage-container dropdown js-mini-pipeline-graph" - v-for='stage in pipeline.details.stages' - > - <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> - </div> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 0f5ce2a9274..0ee21f00fdc 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -20,6 +20,7 @@ require('../vue_realtime_listener'); gl.PipelineStore = class { fetchDataLoop(Vue, pageNum, url, apiScope) { + this.pageRequest = true; const updatePipelineNums = (count) => { const { all } = count; const running = count.running_or_pending; @@ -41,16 +42,18 @@ require('../vue_realtime_listener'); this.pageRequest = false; }, () => { this.pageRequest = false; - return new Flash('Something went wrong on our end.'); + return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); }); goFetch(); const startTimeLoops = () => { this.timeLoopInterval = setInterval(() => { - this.$children - .filter(e => e.$options._componentTag === 'time-ago') - .forEach(e => e.changeTime()); + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); }, 10000); }; diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 655110feba1..3598da11573 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -1,6 +1,9 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +window.Vue = require('vue'); +require('../lib/utils/datetime_utility'); + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 index 4adad7bea31..7f7c18ddeb1 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -1,7 +1,5 @@ /* global Vue */ -window.Vue = require('vue'); - (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 new file mode 100644 index 00000000000..4bdaef31ee9 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -0,0 +1,61 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +require('./pipelines_table_row'); +/** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ + +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { + + props: { + pipelines: { + type: Array, + required: true, + default: () => ([]), + }, + + /** + * TODO: Remove this when we have webpack. + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, + }, + + template: ` + <table class="table ci-table"> + <thead> + <tr> + <th class="js-pipeline-status pipeline-status">Status</th> + <th class="js-pipeline-info pipeline-info">Pipeline</th> + <th class="js-pipeline-commit pipeline-commit">Commit</th> + <th class="js-pipeline-stages pipeline-stages">Stages</th> + <th class="js-pipeline-date pipeline-date"></th> + <th class="js-pipeline-actions pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <template v-for="model in pipelines" + v-bind:model="model"> + <tr is="pipelines-table-row-component" + :pipeline="model" + :svgs="svgs"></tr> + </template> + </tbody> + </table> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 new file mode 100644 index 00000000000..c819f0dd7cd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -0,0 +1,234 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +require('../../vue_pipelines_index/status'); +require('../../vue_pipelines_index/pipeline_url'); +require('../../vue_pipelines_index/stage'); +require('../../vue_pipelines_index/pipeline_actions'); +require('../../vue_pipelines_index/time_ago'); +require('./commit'); +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', { + + props: { + pipeline: { + type: Object, + required: true, + default: () => ({}), + }, + + /** + * TODO: Remove this when we have webpack; + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'commit-component': gl.CommitComponent, + 'pipeline-actions': gl.VuePipelineActions, + 'dropdown-stage': gl.VueStage, + 'pipeline-url': gl.VuePipelineUrl, + 'status-scope': gl.VueStatusScope, + 'time-ago': gl.VueTimeAgo, + }, + + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code + * + * @returns {Object|Undefined} + */ + commitAuthor() { + let commitAuthorInformation; + + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } + } + + // 4. If committer is not a GitLab User he/she can have a Gravatar + if (this.pipeline && + this.pipeline.commit) { + commitAuthorInformation = { + avatar_url: this.pipeline.commit.author_gravatar_url, + web_url: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + + return commitAuthorInformation; + }, + + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * Matched `url` prop sent in the API to `path` prop needed + * in the commit component. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'url') { + accumulator.path = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + }, + + methods: { + /** + * FIXME: This should not be in this component but in the components that + * need this function. + * + * Used to render SVGs in the following components: + * - status-scope + * - dropdown-stage + * + * @param {String} string + * @return {String} + */ + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + + template: ` + <tr class="commit"> + <status-scope + :pipeline="pipeline" + :svgs="svgs" + :match="match"> + </status-scope> + + <pipeline-url :pipeline="pipeline"></pipeline-url> + + <td> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :commit-icon-svg="svgs.commitIconSvg"> + </commit-component> + </td> + + <td class="stage-cell"> + <div class="stage-container dropdown js-mini-pipeline-graph" + v-if="pipeline.details.stages.length > 0" + v-for="stage in pipeline.details.stages"> + <dropdown-stage + :stage="stage" + :svgs="svgs" + :match="match"> + </dropdown-stage> + </div> + </td> + + <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago> + + <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions> + </tr> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 index 67c6cb73761..67c6cb73761 100644 --- a/app/assets/javascripts/vue_pagination/index.js.es6 +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..d3229f9f730 --- /dev/null +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -0,0 +1,23 @@ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, +no-param-reassign, no-plusplus */ +/* global Vue */ + +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next((response) => { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); + } + + Vue.activeResources--; + }); +}); + +Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); +}); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 0ce94a26a7f..a4b38723bbd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -253,6 +253,8 @@ li.note { .progress { margin-bottom: 0; margin-top: 4px; + box-shadow: none; + background-color: $border-gray-light; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 426596027de..2bfdb9f9601 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -307,3 +307,7 @@ ul.controls { } } } + +ul.indent-list { + padding: 10px 0 0 30px; +} diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index b37c1d0d670..c3ec9db0f07 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -6,8 +6,22 @@ .pagination { padding: 0; + + a { + cursor: pointer; + } + + .separator, + .separator:hover { + a { + cursor: default; + background-color: $gray-light; + padding: $gl-vert-padding; + } + } } + .gap, .gap:hover { background-color: $gray-light; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9b413f3e61c..b362cc758cc 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -389,6 +389,13 @@ flex: 1; margin-top: 0; + &.add-issues-empty-state-filter { + -webkit-flex-direction: column; + flex-direction: column; + -webkit-justify-content: center; + justify-content: center; + } + > .row { width: 100%; margin: auto 0; @@ -416,6 +423,14 @@ .add-issues-search { display: -webkit-flex; display: flex; + + .form-control { + margin-left: auto; + + @media (min-width: $screen-sm-min) { + max-width: 200px; + } + } } .add-issues-list-column { @@ -486,3 +501,24 @@ line-height: 15px; border-radius: 50%; } + +.modal-filters { + display: flex; + + > .dropdown { + display: none; + margin-right: 10px; + + @media (min-width: $screen-sm-min) { + display: block; + } + } + + .dropdown-menu-toggle { + width: 100px; + + @media (min-width: $screen-md-min) { + width: 140px; + } + } +} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index fef8e8eec27..c3d45d708c1 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -159,7 +159,6 @@ .commit-row-description { font-size: 14px; - border-left: 1px solid $white-normal; padding: 10px 15px; margin: 10px 0; background: $gray-light; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4ef95d27f4f..9174976c4c6 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -193,7 +193,7 @@ top: $header-height; bottom: 0; right: 0; - z-index: 10; + z-index: 8; transition: width .3s; background: $gray-light; padding: 10px 20px; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 8734a3b1598..1e605337f09 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -148,3 +148,7 @@ ul.related-merge-requests > li { border: 1px solid $border-gray-normal; } } + +.recaptcha { + margin-bottom: 30px; +} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 21d9b4c54ea..762b95a657c 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -259,3 +259,8 @@ } } } + +.label-link { + display: inline-block; + vertical-align: text-top; +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 0c013915a63..b01d8d695d6 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -80,6 +80,10 @@ .ci_widget { border-bottom: 1px solid $well-inner-border; color: $gl-text-color; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; svg { margin-right: 4px; @@ -88,12 +92,20 @@ overflow: visible; } + &> span { + padding-right: 4px; + } + &.ci-success_with_warnings { i { color: $gl-warning; } } + + @media (max-width: $screen-xs-max) { + flex-wrap: wrap; + } } .mr-widget-body, @@ -102,6 +114,37 @@ padding: $gl-padding; } + .mr-widget-pipeline-graph { + flex-shrink: 0; + + .dropdown-menu { + margin-top: 11px; + } + + .ci-action-icon-wrapper { + line-height: 16px; + } + + @media (max-width: $screen-xs-max) { + order: 1; + margin-top: $gl-padding-top; + border-radius: 3px; + background-color: $white-light; + border: 1px solid $gray-darker; + width: 100%; + text-align: center; + + .dropdown-menu { + margin-left: -97.5px; + } + + .arrow-up::before, + .arrow-up::after, { + margin-left: 97.5px; + } + } + } + .normal { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 367a468e1ba..974100bdff0 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -183,52 +183,11 @@ } } - .stage-cell { - font-size: 0; - padding: 10px 4px; - - > .stage-container > div > button > span > svg, - > .stage-container > button > svg { - height: 22px; - width: 22px; - position: absolute; - top: -1px; - left: -1px; - z-index: 2; - overflow: visible; - } - - .stage-container { - display: inline-block; - position: relative; - height: 22px; - margin: 3px 6px 3px 0; - - .tooltip { - white-space: nowrap; - } - - .tooltip-inner { - padding: 3px 4px; - } - - &:not(:last-child) { - &::after { - content: ''; - width: 7px; - position: absolute; - right: -7px; - top: 10px; - border-bottom: 2px solid $border-color; - } - } - } - } - .duration, .finished-at { color: $gl-text-color-secondary; margin: 4px 0; + white-space: nowrap; .fa { font-size: 12px; @@ -311,6 +270,48 @@ } } +.stage-cell { + font-size: 0; + padding: 10px 4px; + + > .stage-container > div > button > span > svg, + > .stage-container > button > svg { + height: 22px; + width: 22px; + position: absolute; + top: -1px; + left: -1px; + z-index: 2; + overflow: visible; + } + + .stage-container { + display: inline-block; + position: relative; + height: 22px; + margin: 3px 6px 3px 0; + + .tooltip { + white-space: nowrap; + } + + .tooltip-inner { + padding: 3px 4px; + } + + &:not(:last-child) { + &::after { + content: ''; + width: 7px; + position: absolute; + right: -7px; + top: 10px; + border-bottom: 2px solid $border-color; + } + } + } +} + .admin-builds-table { .ci-table td:last-child { min-width: 120px; @@ -666,7 +667,7 @@ vertical-align: bottom; display: inline-block; position: relative; - font-weight: 200; + font-weight: normal; } // Dropdown button in mini pipeline graph diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 3f10ae81767..b0f5d4a9933 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -109,6 +109,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :plantuml_url, :max_artifacts_size, :max_attachment_size, + :max_pages_size, :metrics_enabled, :metrics_host, :metrics_method_call_threshold, diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index aa0f8d434dc..1cd50852e89 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -175,7 +175,7 @@ class Admin::UsersController < Admin::ApplicationController def user_params_ce [ - :admin, + :access_level, :avatar, :bio, :can_create_group, diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 562f92bd83c..a6891149bfa 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -1,6 +1,8 @@ module SpammableActions extend ActiveSupport::Concern + include Recaptcha::Verify + included do before_action :authorize_submit_spammable!, only: :mark_as_spam end @@ -15,6 +17,15 @@ module SpammableActions private + def recaptcha_params + return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha + + { + recaptcha_verified: true, + spam_log_id: params[:spam_log_id] + } + end + def spammable raise NotImplementedError, "#{self.class} does not implement #{__method__}" end @@ -22,4 +33,11 @@ module SpammableActions def authorize_submit_spammable! access_denied! unless current_user.admin? end + + def render_recaptcha? + return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors + return false unless Gitlab::Recaptcha.enabled? + + spammable.spam + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 9940263ae24..4c39fe98028 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController end def show + environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } + @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last end def edit diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index b5a7078a3a1..e10d7992db7 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController format.json do render json: PipelineSerializer .new(project: @project, user: @current_user) - .with_pagination(request, response) .represent(@pipelines) end end @@ -95,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs(opts) @notes_count = commit.notes.count + + @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last end def define_note_vars diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 321cde255c3..c6651254d70 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -57,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController @diffs = @compare.diffs(diff_options) + environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit } + @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last + @diff_notes_disabled = true @grouped_diff_discussions = {} end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 87cc36253f1..0ec8f5bd64a 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController def index @scope = params[:scope] - @environments = project.environments + @environments = project.environments.includes(:last_deployment) respond_to do |format| format.html @@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController end def stop - return render_404 unless @environment.stoppable? + return render_404 unless @environment.available? - new_action = @environment.stop!(current_user) - redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) + stop_action = @environment.stop_with_action!(current_user) + + if stop_action + redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action]) + else + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + end end def terminal diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 8472ceca329..c75b8987a4b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -93,15 +93,13 @@ class Projects::IssuesController < Projects::ApplicationController def create extra_params = { request: request, merge_request_for_resolving_discussions: merge_request_for_resolving_discussions } + extra_params.merge!(recaptcha_params) + @issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute respond_to do |format| format.html do - if @issue.valid? - redirect_to issue_path(@issue) - else - render :new - end + html_response_create end format.js do @link = @issue.attachment.url.to_js @@ -178,6 +176,20 @@ class Projects::IssuesController < Projects::ApplicationController protected + def html_response_create + if @issue.valid? + redirect_to issue_path(@issue) + elsif render_recaptcha? + if params[:recaptcha_verification] + flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + end + + render :verify + else + render :new + end + end + def issue # The Sortable default scope causes performance issues when used with find_by @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6eb542e4bd8..3be6e8e1772 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -103,6 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + @environment = @merge_request.environments_for(current_user).last + respond_to do |format| format.html { define_discussion_vars } format.json do @@ -216,19 +218,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController end format.json do - render json: { - html: view_to_html_string('projects/merge_requests/show/_pipelines'), - pipelines: PipelineSerializer - .new(project: @project, user: @current_user) - .with_pagination(request, response) - .represent(@pipelines) - } + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent(@pipelines) end end end def new - define_new_vars + respond_to do |format| + format.html { define_new_vars } + format.json do + render json: { pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .represent(@pipelines) } + end + end end def new_diffs @@ -245,7 +250,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end @diff_notes_disabled = true - render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) } + @environment = @merge_request.environments_for(current_user).last + + render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) } end end end @@ -444,14 +451,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_environments_status environments = begin - @merge_request.environments.map do |environment| - next unless can?(current_user, :read_environment, environment) - + @merge_request.environments_for(current_user).map do |environment| project = environment.project deployment = environment.first_deployment_for(@merge_request.diff_head_commit) stop_url = - if environment.stoppable? && can?(current_user, :create_deployment, environment) + if environment.stop_action? && can?(current_user, :create_deployment, environment) stop_namespace_project_environment_path(project.namespace, project, environment) end diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb new file mode 100644 index 00000000000..fbd18b68141 --- /dev/null +++ b/app/controllers/projects/pages_controller.rb @@ -0,0 +1,22 @@ +class Projects::PagesController < Projects::ApplicationController + layout 'project_settings' + + before_action :authorize_read_pages!, only: [:show] + before_action :authorize_update_pages!, except: [:show] + + def show + @domains = @project.pages_domains.order(:domain) + end + + def destroy + project.remove_pages + project.pages_domains.destroy_all + + respond_to do |format| + format.html do + redirect_to(namespace_project_pages_path(@project.namespace, @project), + notice: 'Pages were removed') + end + end + end +end diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb new file mode 100644 index 00000000000..b8c253f6ae3 --- /dev/null +++ b/app/controllers/projects/pages_domains_controller.rb @@ -0,0 +1,49 @@ +class Projects::PagesDomainsController < Projects::ApplicationController + layout 'project_settings' + + before_action :authorize_update_pages!, except: [:show] + before_action :domain, only: [:show, :destroy] + + def show + end + + def new + @domain = @project.pages_domains.new + end + + def create + @domain = @project.pages_domains.create(pages_domain_params) + + if @domain.valid? + redirect_to namespace_project_pages_path(@project.namespace, @project) + else + render 'new' + end + end + + def destroy + @domain.destroy + + respond_to do |format| + format.html do + redirect_to(namespace_project_pages_path(@project.namespace, @project), + notice: 'Domain was removed') + end + format.js + end + end + + private + + def pages_domain_params + params.require(:pages_domain).permit( + :certificate, + :key, + :domain + ) + end + + def domain + @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) + end +end diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 53ce23221ed..c8c80551ac9 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController before_action :authorize_admin_pipeline! def show - @ref = params[:ref] || @project.default_branch || 'master' - - @badges = [Gitlab::Badge::Build::Status, - Gitlab::Badge::Coverage::Report] - - @badges.map! do |badge| - badge.new(@project, @ref).metadata - end + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project, params: params) end def update if @project.update_attributes(update_params) flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated." - redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project) + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) else render 'show' end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 53c36635efe..74c54037ba9 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -5,11 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController layout 'project_settings' def index - @project_runners = project.runners.ordered - @assignable_runners = current_user.ci_authorized_runners. - assignable_for(project).ordered.page(params[:page]).per(20) - @shared_runners = Ci::Runner.shared.active - @shared_runners_count = @shared_runners.count(:all) + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end def edit @@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController def toggle_shared_runners project.toggle!(:shared_runners_enabled) - redirect_to namespace_project_runners_path(project.namespace, project) + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end protected diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb new file mode 100644 index 00000000000..6f009d61950 --- /dev/null +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -0,0 +1,44 @@ +module Projects + module Settings + class CiCdController < Projects::ApplicationController + before_action :authorize_admin_pipeline! + + def show + define_runners_variables + define_secret_variables + define_triggers_variables + define_badges_variables + end + + private + + def define_runners_variables + @project_runners = @project.runners.ordered + @assignable_runners = current_user.ci_authorized_runners. + assignable_for(project).ordered.page(params[:page]).per(20) + @shared_runners = Ci::Runner.shared.active + @shared_runners_count = @shared_runners.count(:all) + end + + def define_secret_variables + @variable = Ci::Variable.new + end + + def define_triggers_variables + @triggers = @project.triggers + @trigger = Ci::Trigger.new + end + + def define_badges_variables + @ref = params[:ref] || @project.default_branch || 'master' + + @badges = [Gitlab::Badge::Build::Status, + Gitlab::Badge::Coverage::Report] + + @badges.map! do |badge| + badge.new(@project, @ref).metadata + end + end + end + end +end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index 92359745cec..b2c11ea4156 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -4,8 +4,7 @@ class Projects::TriggersController < Projects::ApplicationController layout 'project_settings' def index - @triggers = project.triggers - @trigger = Ci::Trigger.new + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end def create @@ -13,17 +12,18 @@ class Projects::TriggersController < Projects::ApplicationController @trigger.save if @trigger.valid? - redirect_to namespace_project_triggers_path(@project.namespace, @project) + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.' else @triggers = project.triggers.select(&:persisted?) - render :index + render action: "show" end end def destroy trigger.destroy + flash[:alert] = "Trigger removed" - redirect_to namespace_project_triggers_path(@project.namespace, @project) + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end private diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 6f068729390..a4d1b1ee69b 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController layout 'project_settings' def index - @variable = Ci::Variable.new + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end def show @@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController @variable = Ci::Variable.new(project_params) if @variable.valid? && @project.variables << @variable - redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' + flash[:notice] = 'Variables were successfully updated.' + redirect_to namespace_project_settings_ci_cd_path(project.namespace, project) else - render action: "index" + render "show" end end @@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController @key = @project.variables.find(params[:id]) @key.destroy - redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.' + redirect_to namespace_project_settings_ci_cd_path(project.namespace, project), notice: 'Variable was successfully removed.' end private diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index bf27f3d4d51..68bf01ba08d 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -17,7 +17,7 @@ class RegistrationsController < Devise::RegistrationsController if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha super else - flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' + flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash.delete :recaptcha_error render action: 'new' end @@ -30,7 +30,7 @@ class RegistrationsController < Devise::RegistrationsController format.html do session.try(:destroy) redirect_to new_user_session_path, notice: "Account successfully removed." - end + end end end diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb new file mode 100644 index 00000000000..a59f8c1efa3 --- /dev/null +++ b/app/finders/environments_finder.rb @@ -0,0 +1,55 @@ +class EnvironmentsFinder + attr_reader :project, :current_user, :params + + def initialize(project, current_user, params = {}) + @project, @current_user, @params = project, current_user, params + end + + def execute + deployments = project.deployments + deployments = + if ref + deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref' + deployments.where(deployments_query, ref: ref.to_s) + elsif commit + deployments.where(sha: commit.sha) + else + deployments.none + end + + environment_ids = deployments + .group(:environment_id) + .select(:environment_id) + + environments = project.environments.available + .where(id: environment_ids).order_by_last_deployed_at.to_a + + environments.select! do |environment| + Ability.allowed?(current_user, :read_environment, environment) + end + + if ref && commit + environments.select! do |environment| + environment.includes_commit?(commit) + end + end + + if ref && params[:recently_updated] + environments.select! do |environment| + environment.recently_updated_on_branch?(ref) + end + end + + environments + end + + private + + def ref + params[:ref].try(:to_s) + end + + def commit + params[:commit] + end +end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index aa8f4c1d0e4..3b9a421b118 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder projects = [] if current_user - if @group.users.include?(current_user) || current_user.admin? + if @group.users.include?(current_user) projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 6dcb624c4da..8aad39e148b 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -194,7 +194,7 @@ module CommitsHelper end end - def view_file_btn(commit_sha, diff_new_path, project) + def view_file_button(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff_new_path)), @@ -205,6 +205,17 @@ module CommitsHelper end end + def view_on_environment_button(commit_sha, diff_new_path, environment) + return unless environment && commit_sha + + external_url = environment.external_url_for(diff_new_path, commit_sha) + return unless external_url + + link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do + icon('external-link') + end + end + def truncate_sha(sha) Commit.truncate_sha(sha) end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 2159e4ce21a..f16a63e2178 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -211,8 +211,12 @@ module GitlabRoutingHelper def project_settings_integrations_path(project, *args) namespace_project_settings_integrations_path(project.namespace, project, *args) end - + def project_settings_members_path(project, *args) namespace_project_settings_members_path(project.namespace, project, *args) end + + def project_settings_ci_cd_path(project, *args) + namespace_project_settings_ci_cd_path(project.namespace, project, *args) + end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 83ff898e68a..91b24b8bc29 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -20,8 +20,8 @@ module MergeRequestsHelper end def mr_widget_refresh_url(mr) - if mr && mr.source_project - merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr) + if mr && mr.target_project + merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr) else '' end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b1f77bf242c..5213ea9d02b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,6 +9,7 @@ module Ci belongs_to :erased_by, class_name: 'User' has_many :deployments, as: :deployable + has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' # The "environment" field for builds is a String, and is the unexpanded name def persisted_environment @@ -41,7 +42,7 @@ module Ci before_save :update_artifacts_size, if: :artifacts_file_changed? before_save :ensure_token - before_destroy { project } + before_destroy { unscoped_project } after_create :execute_hooks after_save :update_project_statistics, if: :artifacts_size_changed? @@ -183,10 +184,6 @@ module Ci success? && !last_deployment.try(:last?) end - def last_deployment - deployments.last - end - def depends_on_builds # Get builds of the same type latest_builds = self.pipeline.builds.latest @@ -256,7 +253,7 @@ module Ci end def project_id - pipeline.project_id + gl_project_id end def project_name @@ -416,16 +413,23 @@ module Ci # This method returns old path to artifacts only if it already exists. # def artifacts_path + # We need the project even if it's soft deleted, because whenever + # we're really deleting the project, we'll also delete the builds, + # and in order to delete the builds, we need to know where to find + # the artifacts, which is depending on the data of the project. + # We need to retain the project in this case. + the_project = project || unscoped_project + old = File.join(created_at.utc.strftime('%Y_%m'), - project.ci_id.to_s, + the_project.ci_id.to_s, id.to_s) old_store = File.join(ArtifactUploader.artifacts_path, old) - return old if project.ci_id && File.directory?(old_store) + return old if the_project.ci_id && File.directory?(old_store) File.join( created_at.utc.strftime('%Y_%m'), - project.id.to_s, + the_project.id.to_s, id.to_s ) end @@ -451,6 +455,7 @@ module Ci build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + PagesService.new(build_data).execute project.running_or_pending_build_count(force: true) end @@ -559,6 +564,10 @@ module Ci self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) end + def unscoped_project + @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id) + end + def predefined_variables variables = [ { key: 'CI', value: 'true', public: true }, @@ -597,6 +606,8 @@ module Ci end def update_project_statistics + return unless project + ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size]) end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fab8497ec7d..bbc358adb83 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -283,13 +283,7 @@ module Ci def ci_yaml_file return @ci_yaml_file if defined?(@ci_yaml_file) - @ci_yaml_file ||= begin - blob = project.repository.blob_at(sha, '.gitlab-ci.yml') - blob.load_all_data!(project.repository) - blob.data - rescue - nil - end + @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil end def has_yaml_errors? diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 1acff093aa1..423ae98a60e 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -11,6 +11,7 @@ module Spammable has_one :user_agent_detail, as: :subject, dependent: :destroy attr_accessor :spam + attr_accessor :spam_log after_validation :check_for_spam, on: :create @@ -34,9 +35,14 @@ module Spammable end def check_for_spam - if spam? - self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") - end + error_msg = if Gitlab::Recaptcha.enabled? + "Your #{spammable_entity_type} has been recognized as spam. "\ + "You can still submit it by solving Captcha." + else + "Your #{spammable_entity_type} has been recognized as spam and has been discarded." + end + + self.errors.add(:base, error_msg) if spam? end def spammable_entity_type diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 040e3a2884e..9cf83440784 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -18,7 +18,7 @@ module TimeTrackable validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false validate :check_negative_time_spent - has_many :timelogs, as: :trackable, dependent: :destroy + has_many :timelogs, dependent: :destroy end def spend_time(options) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 91d85c2279b..afad001d50f 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base @stop_action ||= manual_actions.find_by(name: on_stop) end - def stoppable? + def stop_action? stop_action.present? end diff --git a/app/models/environment.rb b/app/models/environment.rb index 577367f1eed..803060b3979 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true, validate: true - has_many :deployments + has_many :deployments, dependent: :destroy + has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url before_validation :generate_slug, if: ->(env) { env.slug.blank? } @@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base scope :available, -> { with_state(:available) } scope :stopped, -> { with_state(:stopped) } + scope :order_by_last_deployed_at, -> do + max_deployment_id_sql = + Deployment.select(Deployment.arel_table[:id].maximum). + where(Deployment.arel_table[:environment_id].eq(arel_table[:id])). + to_sql + order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC')) + end state_machine :state, initial: :available do event :start do @@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base ref.to_s == last_deployment.try(:ref) end - def last_deployment - deployments.last - end - def nullify_external_url self.external_url = nil if self.external_url.blank? end @@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base last_deployment.includes_commit?(commit) end + def last_deployed_at + last_deployment.try(:created_at) + end + def update_merge_request_metrics? (environment_type || name) == "production" end @@ -110,15 +118,15 @@ class Environment < ActiveRecord::Base external_url.gsub(/\A.*?:\/\//, '') end - def stoppable? + def stop_action? available? && stop_action.present? end - def stop!(current_user) - return unless stoppable? + def stop_with_action!(current_user) + return unless available? - stop - stop_action.play(current_user) + stop! + stop_action.play(current_user) if stop_action end def actions_for(environment) @@ -171,6 +179,15 @@ class Environment < ActiveRecord::Base self.slug = slugified end + def external_url_for(path, commit_sha) + return unless self.external_url + + public_path = project.public_path_for_source_path(path, commit_sha) + return unless public_path + + [external_url, public_path].join('/') + end + private # Slugifying a name may remove the uniqueness guarantee afforded by it being diff --git a/app/models/group.rb b/app/models/group.rb index 4cdfd022094..a5b92283daa 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -197,7 +197,12 @@ class Group < Namespace end def refresh_members_authorized_projects - UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute + UserProjectAccessChangedService.new(user_ids_for_project_authorizations). + execute + end + + def user_ids_for_project_authorizations + users_with_parents.pluck(:id) end def members_with_parents diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 082adcafcc8..43085f69105 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -715,18 +715,22 @@ class MergeRequest < ActiveRecord::Base !head_pipeline || head_pipeline.success? || head_pipeline.skipped? end - def environments + def environments_for(current_user) return [] unless diff_head_commit - @environments ||= begin - target_envs = target_project.environments_for( - target_branch, commit: diff_head_commit, with_tags: true) + @environments ||= Hash.new do |h, current_user| + envs = EnvironmentsFinder.new(target_project, current_user, + ref: target_branch, commit: diff_head_commit, with_tags: true).execute - source_envs = source_project.environments_for( - source_branch, commit: diff_head_commit) if source_project + if source_project + envs.concat EnvironmentsFinder.new(source_project, current_user, + ref: source_branch, commit: diff_head_commit).execute + end - (target_envs.to_a + source_envs.to_a).uniq + h[current_user] = envs.uniq end + + @environments[current_user] end def state_human_name diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 67d8c1c2e4c..c5713fb7818 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -130,6 +130,7 @@ class Namespace < ActiveRecord::Base end Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + Gitlab::PagesTransfer.new.rename_namespace(path_was, path) remove_exports! @@ -212,6 +213,10 @@ class Namespace < ActiveRecord::Base self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC') end + def user_ids_for_project_authorizations + [owner_id] + end + private def repository_storage_paths diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb new file mode 100644 index 00000000000..0b9ebf1ffe2 --- /dev/null +++ b/app/models/pages_domain.rb @@ -0,0 +1,119 @@ +class PagesDomain < ActiveRecord::Base + belongs_to :project + + validates :domain, hostname: true + validates_uniqueness_of :domain, case_sensitive: false + validates :certificate, certificate: true, allow_nil: true, allow_blank: true + validates :key, certificate_key: true, allow_nil: true, allow_blank: true + + validate :validate_pages_domain + validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } + validate :validate_intermediates, if: ->(domain) { domain.certificate.present? } + + attr_encrypted :key, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' + + after_create :update + after_save :update + after_destroy :update + + def to_param + domain + end + + def url + return unless domain + + if certificate + "https://#{domain}" + else + "http://#{domain}" + end + end + + def has_matching_key? + return false unless x509 + return false unless pkey + + # We compare the public key stored in certificate with public key from certificate key + x509.check_private_key(pkey) + end + + def has_intermediates? + return false unless x509 + + # self-signed certificates doesn't have the certificate chain + return true if x509.verify(x509.public_key) + + store = OpenSSL::X509::Store.new + store.set_default_paths + + # This forces to load all intermediate certificates stored in `certificate` + Tempfile.open('certificate_chain') do |f| + f.write(certificate) + f.flush + store.add_file(f.path) + end + + store.verify(x509) + rescue OpenSSL::X509::StoreError + false + end + + def expired? + return false unless x509 + current = Time.new + current < x509.not_before || x509.not_after < current + end + + def subject + return unless x509 + x509.subject.to_s + end + + def certificate_text + @certificate_text ||= x509.try(:to_text) + end + + private + + def update + ::Projects::UpdatePagesConfigurationService.new(project).execute + end + + def validate_matching_key + unless has_matching_key? + self.errors.add(:key, "doesn't match the certificate") + end + end + + def validate_intermediates + unless has_intermediates? + self.errors.add(:certificate, 'misses intermediates') + end + end + + def validate_pages_domain + return unless domain + if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase) + self.errors.add(:domain, "*.#{Settings.pages.host} is restricted") + end + end + + def x509 + return unless certificate + @x509 ||= OpenSSL::X509::Certificate.new(certificate) + rescue OpenSSL::X509::CertificateError + nil + end + + def pkey + return unless key + @pkey ||= OpenSSL::PKey::RSA.new(key) + rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError + nil + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 0d286bfbaa8..b45f22d94d9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -53,6 +53,8 @@ class Project < ActiveRecord::Base update_column(:last_activity_at, self.created_at) end + after_destroy :remove_pages + # update visibility_level of forks after_update :update_forks_visibility_level def update_forks_visibility_level @@ -148,6 +150,7 @@ class Project < ActiveRecord::Base has_many :lfs_objects, through: :lfs_objects_projects has_many :project_group_links, dependent: :destroy has_many :invited_groups, through: :project_group_links, source: :group + has_many :pages_domains, dependent: :destroy has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy, as: :source @@ -955,6 +958,7 @@ class Project < ActiveRecord::Base Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) + Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path) end # Expires various caches before a project is renamed. @@ -1156,6 +1160,45 @@ class Project < ActiveRecord::Base ensure_runners_token! end + def pages_deployed? + Dir.exist?(public_pages_path) + end + + def pages_url + # The hostname always needs to be in downcased + # All web servers convert hostname to lowercase + host = "#{namespace.path}.#{Settings.pages.host}".downcase + + # The host in URL always needs to be downcased + url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| + "#{prefix}#{namespace.path}." + end.downcase + + # If the project path is the same as host, we serve it as group page + return url if host == path + + "#{url}/#{path}" + end + + def pages_path + File.join(Settings.pages.path, path_with_namespace) + end + + def public_pages_path + File.join(pages_path, 'public') + end + + def remove_pages + # 1. We rename pages to temporary directory + # 2. We wait 5 minutes, due to NFS caching + # 3. We asynchronously remove pages with force + temp_path = "#{path}.#{SecureRandom.hex}.deleted" + + if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path) + PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path) + end + end + def wiki @wiki ||= ProjectWiki.new(self, self.owner) end @@ -1263,28 +1306,26 @@ class Project < ActiveRecord::Base Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end - def environments_for(ref, commit: nil, with_tags: false) - deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?' - - environment_ids = deployments - .where(deployments_query, ref.to_s) - .group(:environment_id) - .select(:environment_id) - - environments_found = environments.available - .where(id: environment_ids).to_a + def route_map_for(commit_sha) + @route_maps_by_commit ||= Hash.new do |h, sha| + h[sha] = begin + data = repository.route_map_for(sha) + next unless data - return environments_found unless commit - - environments_found.select do |environment| - environment.includes_commit?(commit) + Gitlab::RouteMap.new(data) + rescue Gitlab::RouteMap::FormatError + nil + end end + + @route_maps_by_commit[commit_sha] end - def environments_recently_updated_on_branch(branch) - environments_for(branch).select do |environment| - environment.recently_updated_on_branch?(branch) - end + def public_path_for_source_path(path, commit_sha) + map = route_map_for(commit_sha) + return unless map + + map.public_path_for_source_path(path) end private diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 5eb1bd86e9d..8b5bc24fd3c 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' } + { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' } ] end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index b0f7a42f9a3..56f42d63b2d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService end def title - 'Mattermost Command' + 'Mattermost slash commands' end def description - "Perform common operations on GitLab in Mattermost" + "Perform common operations in Mattermost" end def self.to_param diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index c34991e4262..2182c1c7e4b 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService include TriggersHelper def title - 'Slack Command' + 'Slack slash commands' end def description - "Perform common operations on GitLab in Slack" + "Perform common operations in Slack" end def self.to_param diff --git a/app/models/repository.rb b/app/models/repository.rb index 7cf09c52bf4..d2d92a064a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -464,6 +464,8 @@ class Repository unless Gitlab::Git.blank_ref?(sha) Blob.decorate(Gitlab::Git::Blob.find(self, sha, path)) end + rescue Gitlab::Git::Repository::NoRepository + nil end def blob_by_oid(oid) @@ -1160,6 +1162,14 @@ class Repository end end + def route_map_for(sha) + blob_data_at(sha, '.gitlab/route-map.yml') + end + + def gitlab_ci_yml_for(sha) + blob_data_at(sha, '.gitlab-ci.yml') + end + protected def tree_entry_at(branch_name, path) @@ -1186,6 +1196,14 @@ class Repository private + def blob_data_at(sha, path) + blob = blob_at(sha, path) + return unless blob + + blob.load_all_data!(self) + blob.data + end + def git_action(index, action) path = normalize_path(action[:file_path]) diff --git a/app/models/timelog.rb b/app/models/timelog.rb index f768c4e3da5..e166cf69703 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -1,6 +1,22 @@ class Timelog < ActiveRecord::Base validates :time_spent, :user, presence: true + validate :issuable_id_is_present - belongs_to :trackable, polymorphic: true + belongs_to :issue + belongs_to :merge_request belongs_to :user + + def issuable + issue || merge_request + end + + private + + def issuable_id_is_present + if issue_id && merge_request_id + errors.add(:base, 'Only Issue ID or Merge Request ID is required') + elsif issuable.nil? + errors.add(:base, 'Issue or Merge Request ID is required') + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 54f5388eb2c..f64d0c17a45 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -83,8 +83,6 @@ class User < ActiveRecord::Base has_many :events, dependent: :destroy, foreign_key: :author_id has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" - has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" - has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy has_many :spam_logs, dependent: :destroy @@ -94,6 +92,9 @@ class User < ActiveRecord::Base has_many :notification_settings, dependent: :destroy has_many :award_emoji, dependent: :destroy + has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" + has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" + # # Validations # @@ -903,6 +904,21 @@ class User < ActiveRecord::Base end end + def access_level + if admin? + :admin + else + :regular + end + end + + def access_level=(new_level) + new_level = new_level.to_s + return unless %w(admin regular).include?(new_level) + + self.admin = (new_level == 'admin') + end + private def ci_projects_union diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 71ef8901932..f8594e29547 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -110,6 +110,9 @@ class ProjectPolicy < BasePolicy can! :admin_pipeline can! :admin_environment can! :admin_deployment + can! :admin_pages + can! :read_pages + can! :update_pages end def public_access! @@ -136,6 +139,7 @@ class ProjectPolicy < BasePolicy can! :remove_fork_project can! :destroy_merge_request can! :destroy_issue + can! :remove_pages end def team_member_owner_access! @@ -214,25 +218,7 @@ class ProjectPolicy < BasePolicy def anonymous_rules return unless project.public? - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_merge_request - can! :read_note - can! :read_pipeline - can! :read_commit_status - can! :read_container_image - can! :download_code - can! :download_wiki_code - can! :read_cycle_analytics - - # NOTE: may be overridden by IssuePolicy - can! :read_issue + base_readonly_access! # Allow to read builds by anonymous user if guests are allowed can! :read_build if project.public_builds? @@ -265,4 +251,31 @@ class ProjectPolicy < BasePolicy :"admin_#{name}" ] end + + private + + # A base set of abilities for read-only users, which + # is then augmented as necessary for anonymous and other + # read-only users. + def base_readonly_access! + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :read_pipeline + can! :read_commit_status + can! :read_container_image + can! :download_code + can! :download_wiki_code + can! :read_cycle_analytics + + # NOTE: may be overridden by IssuePolicy + can! :read_issue + end end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index 57acccfafd9..3a96836917e 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -3,7 +3,7 @@ class ProjectSnippetPolicy < BasePolicy can! :read_project_snippet if @subject.public? return unless @user - if @user && @subject.author == @user || @user.admin? + if @user && (@subject.author == @user || @user.admin?) can! :read_project_snippet can! :update_project_snippet can! :admin_project_snippet diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 5d15eb8d3d3..4c017960628 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity expose :external_url expose :environment_type expose :last_deployment, using: DeploymentEntity - expose :stoppable? + expose :stop_action? expose :environment_path do |environment| namespace_project_environment_path( diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb index cf590459cb2..42c72aba7dd 100644 --- a/app/services/ci/stop_environments_service.rb +++ b/app/services/ci/stop_environments_service.rb @@ -8,10 +8,9 @@ module Ci return unless has_ref? environments.each do |environment| - next unless environment.stoppable? next unless can?(current_user, :create_deployment, project) - environment.stop!(current_user) + environment.stop_with_action!(current_user) end end @@ -22,8 +21,8 @@ module Ci end def environments - @environments ||= project - .environments_recently_updated_on_branch(@ref) + @environments ||= + EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute end end end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index d2eb46ac41b..c9168f74249 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -3,6 +3,8 @@ module Issues def execute @request = params.delete(:request) @api = params.delete(:api) + @recaptcha_verified = params.delete(:recaptcha_verified) + @spam_log_id = params.delete(:spam_log_id) issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions) @issue = BuildService.new(project, current_user, issue_attributes).execute @@ -11,7 +13,13 @@ module Issues end def before_create(issuable) - issuable.spam = spam_service.check(@api) + if @recaptcha_verified + spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title) + spam_log.update!(recaptcha_verified: true) if spam_log + else + issuable.spam = spam_service.check(@api) + issuable.spam_log = spam_service.spam_log + end end def after_create(issuable) @@ -35,7 +43,7 @@ module Issues private def spam_service - SpamService.new(@issue, @request) + @spam_service ||= SpamService.new(@issue, @request) end def user_agent_detail_service diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb new file mode 100644 index 00000000000..446eeb34d3b --- /dev/null +++ b/app/services/pages_service.rb @@ -0,0 +1,15 @@ +class PagesService + attr_reader :data + + def initialize(data) + @data = data + end + + def execute + return unless Settings.pages.enabled + return unless data[:build_name] == 'pages' + return unless data[:build_status] == 'success' + + PagesWorker.perform_async(:deploy, data[:build_id]) + end +end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 06252c7b625..535da706159 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -26,7 +26,7 @@ module Projects end def project_tree_saver - Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared) + Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared) end def uploads_saver diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 34ec575e808..484700c8c29 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -25,9 +25,10 @@ module Projects end def transfer(project, new_namespace) + old_namespace = project.namespace + Project.transaction do old_path = project.path_with_namespace - old_namespace = project.namespace old_group = project.group new_path = File.join(new_namespace.try(:path) || '', project.path) @@ -64,11 +65,17 @@ module Projects # Move uploads Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + # Move pages + Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + project.old_path_with_namespace = old_path SystemHooksService.new.execute_hooks_for(project, :transfer) - true end + + refresh_permissions(old_namespace, new_namespace) + + true end def allowed_transfer?(current_user, project, namespace) @@ -77,5 +84,14 @@ module Projects namespace.id != project.namespace_id && current_user.can?(:create_projects, namespace) end + + def refresh_permissions(old_namespace, new_namespace) + # This ensures we only schedule 1 job for every user that has access to + # the namespaces. + user_ids = old_namespace.user_ids_for_project_authorizations | + new_namespace.user_ids_for_project_authorizations + + UserProjectAccessChangedService.new(user_ids).execute + end end end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb new file mode 100644 index 00000000000..eb4809afa85 --- /dev/null +++ b/app/services/projects/update_pages_configuration_service.rb @@ -0,0 +1,69 @@ +module Projects + class UpdatePagesConfigurationService < BaseService + attr_reader :project + + def initialize(project) + @project = project + end + + def execute + update_file(pages_config_file, pages_config.to_json) + reload_daemon + success + rescue => e + error(e.message) + end + + private + + def pages_config + { + domains: pages_domains_config + } + end + + def pages_domains_config + project.pages_domains.map do |domain| + { + domain: domain.domain, + certificate: domain.certificate, + key: domain.key, + } + end + end + + def reload_daemon + # GitLab Pages daemon constantly watches for modification time of `pages.path` + # It reloads configuration when `pages.path` is modified + update_file(pages_update_file, SecureRandom.hex(64)) + end + + def pages_path + @pages_path ||= project.pages_path + end + + def pages_config_file + File.join(pages_path, 'config.json') + end + + def pages_update_file + File.join(::Settings.pages.path, '.update') + end + + def update_file(file, data) + unless data + FileUtils.remove(file, force: true) + return + end + + temp_file = "#{file}.#{SecureRandom.hex(16)}" + File.open(temp_file, 'w') do |f| + f.write(data) + end + FileUtils.move(temp_file, file, force: true) + ensure + # In case if the updating fails + FileUtils.remove(temp_file, force: true) + end + end +end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb new file mode 100644 index 00000000000..f5f9ee88912 --- /dev/null +++ b/app/services/projects/update_pages_service.rb @@ -0,0 +1,164 @@ +module Projects + class UpdatePagesService < BaseService + BLOCK_SIZE = 32.kilobytes + MAX_SIZE = 1.terabyte + SITE_PATH = 'public/' + + attr_reader :build + + def initialize(project, build) + @project, @build = project, build + end + + def execute + # Create status notifying the deployment of pages + @status = create_status + @status.enqueue! + @status.run! + + raise 'missing pages artifacts' unless build.artifacts_file? + raise 'pages are outdated' unless latest? + + # Create temporary directory in which we will extract the artifacts + FileUtils.mkdir_p(tmp_path) + Dir.mktmpdir(nil, tmp_path) do |archive_path| + extract_archive!(archive_path) + + # Check if we did extract public directory + archive_public_path = File.join(archive_path, 'public') + raise 'pages miss the public folder' unless Dir.exist?(archive_public_path) + raise 'pages are outdated' unless latest? + + deploy_page!(archive_public_path) + success + end + rescue => e + error(e.message) + end + + private + + def success + @status.success + super + end + + def error(message, http_status = nil) + @status.allow_failure = !latest? + @status.description = message + @status.drop + super + end + + def create_status + GenericCommitStatus.new( + project: project, + pipeline: build.pipeline, + user: build.user, + ref: build.ref, + stage: 'deploy', + name: 'pages:deploy' + ) + end + + def extract_archive!(temp_path) + if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz') + extract_tar_archive!(temp_path) + elsif artifacts.ends_with?('.zip') + extract_zip_archive!(temp_path) + else + raise 'unsupported artifacts format' + end + end + + def extract_tar_archive!(temp_path) + results = Open3.pipeline(%W(gunzip -c #{artifacts}), + %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), + %W(tar -x -C #{temp_path} #{SITE_PATH}), + err: '/dev/null') + raise 'pages failed to extract' unless results.compact.all?(&:success?) + end + + def extract_zip_archive!(temp_path) + raise 'missing artifacts metadata' unless build.artifacts_metadata? + + # Calculate page size after extract + public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) + + if public_entry.total_size > max_size + raise "artifacts for pages are too large: #{public_entry.total_size}" + end + + # Requires UnZip at least 6.00 Info-ZIP. + # -n never overwrite existing files + # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories + site_path = File.join(SITE_PATH, '*') + unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path})) + raise 'pages failed to extract' + end + end + + def deploy_page!(archive_public_path) + # Do atomic move of pages + # Move and removal may not be atomic, but they are significantly faster then extracting and removal + # 1. We move deployed public to previous public path (file removal is slow) + # 2. We move temporary public to be deployed public + # 3. We remove previous public path + FileUtils.mkdir_p(pages_path) + begin + FileUtils.move(public_path, previous_public_path) + rescue + end + FileUtils.move(archive_public_path, public_path) + ensure + FileUtils.rm_r(previous_public_path, force: true) + end + + def latest? + # check if sha for the ref is still the most recent one + # this helps in case when multiple deployments happens + sha == latest_sha + end + + def blocks + # Calculate dd parameters: we limit the size of pages + 1 + max_size / BLOCK_SIZE + end + + def max_size + current_application_settings.max_pages_size.megabytes || MAX_SIZE + end + + def tmp_path + @tmp_path ||= File.join(::Settings.pages.path, 'tmp') + end + + def pages_path + @pages_path ||= project.pages_path + end + + def public_path + @public_path ||= File.join(pages_path, 'public') + end + + def previous_public_path + @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") + end + + def ref + build.ref + end + + def artifacts + build.artifacts_file.path + end + + def latest_sha + project.commit(build.ref).try(:sha).to_s + end + + def sha + build.sha + end + end +end diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb index 48903291799..024a7c19d33 100644 --- a/app/services/spam_service.rb +++ b/app/services/spam_service.rb @@ -1,5 +1,6 @@ class SpamService attr_accessor :spammable, :request, :options + attr_reader :spam_log def initialize(spammable, request = nil) @spammable = spammable @@ -63,7 +64,7 @@ class SpamService end def create_spam_log(api) - SpamLog.create( + @spam_log = SpamLog.create!( { user_id: spammable_owner_id, title: spammable.spam_title, diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 110072e3a16..87ba72cf991 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -385,6 +385,7 @@ module SystemNoteService # Returns Boolean def cross_reference_disallowed?(noteable, mentioner) return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? + return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?)) return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb new file mode 100644 index 00000000000..098b16017d2 --- /dev/null +++ b/app/validators/certificate_key_validator.rb @@ -0,0 +1,25 @@ +# UrlValidator +# +# Custom validator for private keys. +# +# class Project < ActiveRecord::Base +# validates :certificate_key, certificate_key: true +# end +# +class CertificateKeyValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless valid_private_key_pem?(value) + record.errors.add(attribute, "must be a valid PEM private key") + end + end + + private + + def valid_private_key_pem?(value) + return false unless value + pkey = OpenSSL::PKey::RSA.new(value) + pkey.private? + rescue OpenSSL::PKey::PKeyError + false + end +end diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb new file mode 100644 index 00000000000..e3d18097f71 --- /dev/null +++ b/app/validators/certificate_validator.rb @@ -0,0 +1,24 @@ +# UrlValidator +# +# Custom validator for private keys. +# +# class Project < ActiveRecord::Base +# validates :certificate_key, certificate: true +# end +# +class CertificateValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless valid_certificate_pem?(value) + record.errors.add(attribute, "must be a valid PEM certificate") + end + end + + private + + def valid_certificate_pem?(value) + return false unless value + OpenSSL::X509::Certificate.new(value).present? + rescue OpenSSL::X509::CertificateError + false + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 2c64de1d530..816035ec442 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -187,6 +187,14 @@ .help-block Markdown enabled %fieldset + %legend Pages + .form-group + = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :max_pages_size, class: 'form-control' + .help-block Zero for unlimited + + %fieldset %legend Continuous Integration .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml index 4ce4eab8753..33f6d847782 100644 --- a/app/views/admin/spam_logs/_spam_log.html.haml +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -14,6 +14,8 @@ %td = spam_log.via_api? ? 'Y' : 'N' %td + = spam_log.recaptcha_verified ? 'Y' : 'N' + %td = spam_log.noteable_type %td = spam_log.title diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml index 0fdd5bd9960..8aaa6379730 100644 --- a/app/views/admin/spam_logs/index.html.haml +++ b/app/views/admin/spam_logs/index.html.haml @@ -10,6 +10,7 @@ %th User %th Source IP %th API? + %th Recaptcha verified? %th Type %th Title %th Description diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml new file mode 100644 index 00000000000..7855239dfe5 --- /dev/null +++ b/app/views/admin/users/_access_levels.html.haml @@ -0,0 +1,37 @@ +%fieldset + %legend Access + .form-group + = f.label :projects_limit, class: 'control-label' + .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' + + .form-group + = f.label :can_create_group, class: 'control-label' + .col-sm-10= f.check_box :can_create_group + + .form-group + = f.label :access_level, class: 'control-label' + .col-sm-10 + - editing_current_user = (current_user == @user) + + = f.radio_button :access_level, :regular, disabled: editing_current_user + = label_tag :regular do + Regular + %p.light + Regular users have access to their groups and projects + + = f.radio_button :access_level, :admin, disabled: editing_current_user + = label_tag :admin do + Admin + %p.light + Administrators have access to all groups, projects and users and can manage all features in this installation + - if editing_current_user + %p.light + You cannot remove your own admin rights. + + .form-group + = f.label :external, class: 'control-label' + .col-sm-10 + = f.check_box :external do + External + %p.light + External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 3145212728f..e911af3f6f9 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -40,28 +40,7 @@ = f.label :password_confirmation, class: 'control-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' - %fieldset - %legend Access - .form-group - = f.label :projects_limit, class: 'control-label' - .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' - - .form-group - = f.label :can_create_group, class: 'control-label' - .col-sm-10= f.check_box :can_create_group - - .form-group - = f.label :admin, class: 'control-label' - - if current_user == @user - .col-sm-10= f.check_box :admin, disabled: true - .col-sm-10 You cannot remove your own admin rights. - - else - .col-sm-10= f.check_box :admin - - .form-group - = f.label :external, class: 'control-label' - .col-sm-10= f.check_box :external - .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + = render partial: 'access_levels', locals: { f: f } %fieldset %legend Profile diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 01ecf237925..5a44ec45b7b 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -23,7 +23,7 @@ = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." %p.gl-field-hint Minimum length is #{@minimum_password_length} characters %div - - if current_application_settings.recaptcha_enabled + - if Gitlab::Recaptcha.enabled? = recaptcha_tags %div = f.submit "Register", class: "btn-register btn" diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index da2df0d8080..705e20112fa 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -79,6 +79,14 @@ %td.shortcut .key esc %td Go back + %tbody + %tr + %th + %th Project File + %tr + %td.shortcut + .key y + %td Go to file permalink .col-lg-4 %table.shortcut-mappings diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index c6df66d2c3c..665725f6862 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -18,19 +18,11 @@ Protected Branches - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :runners) do - = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do - %span - Runners - = nav_link(controller: :variables) do - = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do - %span - Variables - = nav_link(controller: :triggers) do - = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do - %span - Triggers - = nav_link(controller: :pipelines_settings) do - = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do + = nav_link(controller: :ci_cd) do + = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do %span CI/CD Pipelines + = nav_link(controller: :pages) do + = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do + %span + Pages diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index ff893ea74e1..7b9cfbbd067 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,3 +1,6 @@ +.btn-group + = view_on_environment_button(@commit.sha, @path, @environment) if @environment + .btn-group.tree-btn-group = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' @@ -12,7 +15,7 @@ = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-sm' = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-sm' + tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url' - if current_user .btn-group{ role: "group" } diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index f0c76af29dc..05fe504d1c9 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -29,5 +29,8 @@ = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), + "milestone-path" => milestones_filter_dropdown_path, + "label-path" => labels_filter_path, ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath" } + ":root-path" => "rootPath", + ":project-id" => @project.try(:id) } diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index cdab1e1b1a6..ac0fd87fd8d 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -40,25 +40,8 @@ - else Cant find HEAD commit for this branch - %td.stage-cell - - pipeline.stages.each do |stage| - - if stage.status - - detailed_status = stage.detailed_status(current_user) - - icon_status = "#{detailed_status.icon}_borderless" - - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" - - .stage-container.dropdown.js-mini-pipeline-graph - %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } - = custom_icon(icon_status) - = icon('caret-down') - - %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container - .arrow-up - .js-builds-dropdown-list.scrollable-menu - - .js-builds-dropdown-loading.builds-dropdown-loading.hidden - %span.fa.fa-spinner.fa-spin - + %td + = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph' %td - if pipeline.duration diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 1164627fa11..aae2cb8a04b 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,15 +1,25 @@ -%div - - if pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .table-holder.pipelines - %table.table.ci-table.js-pipeline-table - %thead - %th.pipeline-status Status - %th.pipeline-info Pipeline - %th.pipeline-commit Commit - %th.pipeline-stages Stages - %th.pipeline-date - %th.pipeline-actions - = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false +#commit-pipeline-table-view{ data: { endpoint: endpoint } } +.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), +} } + +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('commit_pipelines') diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 89968cf4e0d..ac93eac41ac 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -2,4 +2,4 @@ = render 'commit_box' = render 'ci_menu' -= render 'pipelines_list', pipelines: @pipelines += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 7afd3d80ef5..d5fc283aa8d 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -9,7 +9,7 @@ = render "ci_menu" - else .block-connector - = render "projects/diffs/diffs", diffs: @diffs + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 9c8f58d4aea..0dfc9fe20ed 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - else .light-well .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 58c20e225c6..4b49bed835f 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,3 +1,4 @@ +- environment = local_assigns.fetch(:environment, nil) - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files @@ -30,4 +31,4 @@ - file_hash = hexdigest(diff_file.file_path) = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob + diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index fc478ccc995..75885badac9 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,3 +1,4 @@ +- environment = local_assigns.fetch(:environment, nil) .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) } .file-title = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" @@ -13,6 +14,7 @@ = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) - = view_file_btn(diff_commit.id, diff_file.new_path, project) + = view_file_button(diff_commit.id, diff_file.new_path, project) + = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 7a2dacdb1e7..9c5c1a6d707 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -133,6 +133,7 @@ %hr = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" + .row.prepend-top-default %hr .row.prepend-top-default diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml index 69848123c17..14a2d627203 100644 --- a/app/views/projects/environments/_stop.html.haml +++ b/app/views/projects/environments/_stop.html.haml @@ -1,4 +1,4 @@ -- if can?(current_user, :create_deployment, environment) && environment.stoppable? +- if can?(current_user, :create_deployment, environment) && environment.stop_action? .inline = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post, class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7800d6ac382..7036325fff8 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -12,7 +12,7 @@ = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' - - if can?(current_user, :create_deployment, @environment) && @environment.stoppable? + - if can?(current_user, :create_deployment, @environment) && @environment.can_stop? = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post .deployments-container diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index f3be343daae..085b2fc2814 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -50,7 +50,7 @@ - if issue.labels.any? - issue.labels.each do |label| - = link_to_label(label, subject: issue.project) + = link_to_label(label, subject: issue.project, css_class: 'label-link') - if issue.tasks? %span.task-status diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml new file mode 100644 index 00000000000..1934b18c086 --- /dev/null +++ b/app/views/projects/issues/verify.html.haml @@ -0,0 +1,20 @@ +- page_title "Anti-spam verification" + +%h3.page-title + Anti-spam verification +%hr + +%p + We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue. + += form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f| + .recaptcha + - params[:issue].each do |field, value| + = hidden_field(:issue, field, value: value) + = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions]) + = hidden_field_tag(:spam_log_id, @issue.spam_log.id) + = hidden_field_tag(:recaptcha_verification, true) + = recaptcha_tags + + .row-content-block.footer-block + = f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create' diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 513f0818169..4dbb97b3228 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -64,7 +64,7 @@ - if merge_request.labels.any? - merge_request.labels.each do |label| - = link_to_label(label, subject: merge_request.project, type: :merge_request) + = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link') - if merge_request.tasks? diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml index 74367ab9b7b..627fc4e9671 100644 --- a/app/views/projects/merge_requests/_new_diffs.html.haml +++ b/app/views/projects/merge_requests/_new_diffs.html.haml @@ -1 +1 @@ -= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false += render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index d3c013b3f21..38259faf62f 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -46,7 +46,7 @@ -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render "projects/merge_requests/show/pipelines" + = render "projects/merge_requests/show/pipelines", endpoint: link_to(url_for(params)) .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index b46c4a13cc4..83250443bea 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,8 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - -# This tab is always loaded via AJAX + - if @pipelines.any? + = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 5f048d04b27..7f0913ea516 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/show/versions' - = render "projects/diffs/diffs", diffs: @diffs + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml index afe3f3430c6..cbe534abedb 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -1 +1 @@ -= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ae134563ead..bef76f16ca7 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,16 +1,20 @@ - if @pipeline .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do - = ci_icon_for_status(status) + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } + %div{ class: "ci-status-icon-#{status}" } + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do + = ci_icon_for_status(status) %span Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) - for - = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" + .mr-widget-pipeline-graph + = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' + %span + for + = succeed "." do + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" %span.ci-coverage - elsif @merge_request.has_ci? diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index fbd2bff5bbb..08c73d94a09 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -13,7 +13,7 @@ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form = render "projects/notes/form", view: diff_view - - else + - elsif !current_user .disabled-comment.text-center .disabled-comment-text.inline Please diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml new file mode 100644 index 00000000000..82e20eeebb3 --- /dev/null +++ b/app/views/projects/pages/_access.html.haml @@ -0,0 +1,13 @@ +- if @project.pages_deployed? + .panel.panel-default + .panel-heading + Access pages + .panel-body + %p + %strong + Congratulations! Your pages are served under: + + %p= link_to @project.pages_url, @project.pages_url + + - @project.pages_domains.each do |domain| + %p= link_to domain.url, domain.url diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml new file mode 100644 index 00000000000..42d9ef5ccba --- /dev/null +++ b/app/views/projects/pages/_destroy.haml @@ -0,0 +1,12 @@ +- if @project.pages_deployed? + - if can?(current_user, :remove_pages, @project) + .panel.panel-default.panel.panel-danger + .panel-heading Remove pages + .errors-holder + .panel-body + %p + Removing the pages will prevent from exposing them to outside world. + .form-actions + = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" + - else + .nothing-here-block Only the project owner can remove pages diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml new file mode 100644 index 00000000000..ad51fbc6cab --- /dev/null +++ b/app/views/projects/pages/_disabled.html.haml @@ -0,0 +1,4 @@ +.panel.panel-default + .nothing-here-block + GitLab Pages are disabled. + Ask your system's administrator to enable it. diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml new file mode 100644 index 00000000000..4f2dd1a1398 --- /dev/null +++ b/app/views/projects/pages/_list.html.haml @@ -0,0 +1,17 @@ +- if can?(current_user, :update_pages, @project) && @domains.any? + .panel.panel-default + .panel-heading + Domains (#{@domains.count}) + %ul.well-list + - @domains.each do |domain| + %li + .pull-right + = link_to 'Details', namespace_project_pages_domain_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_pages_domain_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + .clearfix + %span= link_to domain.domain, domain.url + %p + - if domain.subject + %span.label.label-gray Certificate: #{domain.subject} + - if domain.expired? + %span.label.label-danger Expired diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml new file mode 100644 index 00000000000..7cea5f3e70b --- /dev/null +++ b/app/views/projects/pages/_no_domains.html.haml @@ -0,0 +1,7 @@ +- if can?(current_user, :update_pages, @project) + .panel.panel-default + .panel-heading + Domains + .nothing-here-block + Support for domains and certificates is disabled. + Ask your system's administrator to enable it. diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml new file mode 100644 index 00000000000..9db46f0b1fc --- /dev/null +++ b/app/views/projects/pages/_use.html.haml @@ -0,0 +1,8 @@ +- unless @project.pages_deployed? + .panel.panel-info + .panel-heading + Configure pages + .panel-body + %p + Learn how to upload your static site and have it served by + GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml new file mode 100644 index 00000000000..b6595269b06 --- /dev/null +++ b/app/views/projects/pages/show.html.haml @@ -0,0 +1,26 @@ +- page_title 'Pages' +%h3.page_title + Pages + + - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https) + = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do + %i.fa.fa-plus + New Domain + +%p.light + With GitLab Pages you can host your static websites on GitLab. + Combined with the power of GitLab CI and the help of GitLab Runner + you can deploy static pages for your individual projects, your user or your group. + +%hr.clearfix + +- if Gitlab.config.pages.enabled + = render 'access' + = render 'use' + - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https + = render 'list' + - else + = render 'no_domains' + = render 'destroy' +- else + = render 'disabled' diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml new file mode 100644 index 00000000000..ca1b41b140a --- /dev/null +++ b/app/views/projects/pages_domains/_form.html.haml @@ -0,0 +1,34 @@ += form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| + - if @domain.errors.any? + #error_explanation + .alert.alert-danger + - @domain.errors.full_messages.each do |msg| + %p= msg + + .form-group + = f.label :domain, class: 'control-label' do + Domain + .col-sm-10 + = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' + + - if Gitlab.config.pages.external_https + .form-group + = f.label :certificate, class: 'control-label' do + Certificate (PEM) + .col-sm-10 + = f.text_area :certificate, rows: 5, class: 'form-control' + %span.help-inline Upload a certificate for your domain with all intermediates + + .form-group + = f.label :key, class: 'control-label' do + Key (PEM) + .col-sm-10 + = f.text_area :key, rows: 5, class: 'form-control' + %span.help-inline Upload a private key for your certificate + - else + .nothing-here-block + Support for custom certificates is disabled. + Ask your system's administrator to enable it. + + .form-actions + = f.submit 'Create New Domain', class: "btn btn-save" diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml new file mode 100644 index 00000000000..e1477c71d06 --- /dev/null +++ b/app/views/projects/pages_domains/new.html.haml @@ -0,0 +1,6 @@ +- page_title 'New Pages Domain' +%h3.page_title + New Pages Domain +%hr.clearfix +%div + = render 'form' diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml new file mode 100644 index 00000000000..52dddb052a7 --- /dev/null +++ b/app/views/projects/pages_domains/show.html.haml @@ -0,0 +1,30 @@ +- page_title "#{@domain.domain}", 'Pages Domains' + +%h3.page-title + Pages Domain + +.table-holder + %table.table + %tr + %td + Domain + %td + = link_to @domain.domain, @domain.url + %tr + %td + DNS + %td + %p + To access the domain create a new DNS record: + %pre + #{@domain.domain} CNAME #{@domain.project.namespace.path}.#{Settings.pages.host}. + %tr + %td + Certificate + %td + - if @domain.certificate_text + %pre + = @domain.certificate_text + - else + .light + missing diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index f776734556a..81e393d7626 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -36,31 +36,27 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - - if @pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), - } } + .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), + } } .vue-pipelines-index diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 18328c67f02..8024fb8979d 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -1,9 +1,7 @@ -- page_title "CI/CD Pipelines" - .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - = page_title + CI/CD Pipelines .col-lg-9 = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f| %fieldset.builds-feature @@ -95,4 +93,4 @@ %hr .row.prepend-top-default - = render partial: 'badge', collection: @badges + = render partial: 'projects/pipelines_settings/badge', collection: @badges diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/_index.html.haml index d6f691d9c24..f9808f7c990 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/_index.html.haml @@ -1,5 +1,3 @@ -- page_title "Runners" - .light.prepend-top-default %p A 'Runner' is a process which runs a job. @@ -22,6 +20,6 @@ %p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners .row .col-sm-6 - = render 'specific_runners' + = render 'projects/runners/specific_runners' .col-sm-6 - = render 'shared_runners' + = render 'projects/runners/shared_runners' diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 5afa193357e..0671dd66e78 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -22,7 +22,7 @@ - else %h4.underlined-title Available shared Runners : #{@shared_runners_count} %ul.bordered-list.available-shared-runners - = render partial: 'runner', collection: @shared_runners, as: :runner + = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner - if @shared_runners_count > 10 .light and #{@shared_runners_count - 10} more... diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index dcff675eafc..6b8e6bd4fee 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -20,10 +20,10 @@ - if @project_runners.any? %h4.underlined-title Runners activated for this project %ul.bordered-list.activated-specific-runners - = render partial: 'runner', collection: @project_runners, as: :runner + = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner - if @assignable_runners.any? %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners - = render partial: 'runner', collection: @assignable_runners, as: :runner + = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner = paginate @assignable_runners, theme: "gitlab" diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 8ca4c51a064..3a323d94cc2 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -1,16 +1,19 @@ -- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" +- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}" -To setup this service: -%ul.list-unstyled +%p To setup this service: +%ul.list-unstyled.indent-list %li 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + Enable custom slash commands + = icon('external-link') on your Mattermost installation %li 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do + Add a slash command + = icon('external-link') + in your Mattermost team with these options: %hr .help-form @@ -83,9 +86,14 @@ To setup this service: %hr -%ul.list-unstyled +%ul.list-unstyled.indent-list %li - 3. After adding the slash command, paste the - - %strong token + 3. Paste the + %strong Token into the field below + %li + 4. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Mattermost! diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index c1e576b42fc..a04fd5035a6 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,13 +1,16 @@ - enabled = Gitlab.config.mattermost.enabled .well - This service allows GitLab users to perform common operations on this - project by entering slash commands in Mattermost. - %br - See list of available commands in Mattermost after setting up this service, - by entering - %code /<command_trigger_word> help - + %p + This service allows users to perform common operations on this + project by entering slash commands in Mattermost. + = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Mattermost after setting up this service, + by entering + %kbd.inline /<trigger> help - unless enabled || @service.template? = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 04b9100acc6..0d973a20d4c 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,21 +1,25 @@ -- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path" -- run_actions_text = "Perform common operations on this project: #{pretty_name}" +- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path' +- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" .well - This service allows GitLab users to perform common operations on this - project by entering slash commands in Slack. - %br - See list of available commands in Slack after setting up this service, - by entering - %code /<command> help - %br - %br + %p + This service allows users to perform common operations on this + project by entering slash commands in Slack. + = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Slack after setting up this service, + by entering + %kbd.inline /<command> help - unless @service.template? - To setup this service: - %ul.list-unstyled + %p To setup this service: + %ul.list-unstyled.indent-list %li 1. - = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' + = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + Add a slash command + = icon('external-link') in your Slack team with these options: %hr @@ -82,7 +86,7 @@ %hr - %ul.list-unstyled + %ul.list-unstyled.indent-list %li 2. Paste the %strong Token diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml new file mode 100644 index 00000000000..52f5f7b81e2 --- /dev/null +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -0,0 +1,6 @@ +- page_title "CI/CD Pipelines" + += render 'projects/runners/index' += render 'projects/variables/index' += render 'projects/triggers/index' += render 'projects/pipelines_settings/show' diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index e2a5107a883..dde2e2b644d 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,3 +1,5 @@ +- return unless current_user + .hidden-xs - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/_index.html.haml index b9c4e323430..5cb1818ae54 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -1,9 +1,7 @@ -- page_title "Triggers" - .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 - = page_title + Triggers %p.prepend-top-20 Triggers can force a specific branch or tag to get rebuilt with an API call. %p.append-bottom-0 @@ -25,12 +23,12 @@ %th %strong Last used %th - = render partial: 'trigger', collection: @triggers, as: :trigger + = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger - else %p.settings-message.text-center.append-bottom-default No triggers have been created yet. Add one using the button below. - = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| + = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f| = f.submit "Add trigger", class: 'btn btn-success' .panel-footer diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/_index.html.haml index cf7ae0b489f..1b852a9c5b3 100644 --- a/app/views/projects/variables/index.html.haml +++ b/app/views/projects/variables/_index.html.haml @@ -1,12 +1,10 @@ -- page_title "Variables" - .row.prepend-top-default.append-bottom-default .col-lg-3 - = render "content" + = render "projects/variables/content" .col-lg-9 %h5.prepend-top-0 Add a variable - = render "form", btn_text: "Add new variable" + = render "projects/variables/form", btn_text: "Add new variable" %hr %h5.prepend-top-0 Your variables (#{@project.variables.size}) @@ -14,5 +12,5 @@ %p.settings-message.text-center.append-bottom-0 No variables found, add one with the form above. - else - = render "table" + = render "projects/variables/table" %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml new file mode 100644 index 00000000000..b0778653d4e --- /dev/null +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -0,0 +1,18 @@ +.stage-cell + - pipeline.stages.each do |stage| + - if stage.status + - detailed_status = stage.detailed_status(current_user) + - icon_status = "#{detailed_status.icon}_borderless" + - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" + + .stage-container.dropdown{ class: klass } + %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } + = custom_icon(icon_status) + = icon('caret-down') + + %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container + .arrow-up + .js-builds-dropdown-list.scrollable-menu + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 2ad06dcf25b..aa459f8ffb1 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -66,7 +66,7 @@ .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } .filter-item.inline = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 55360dadbc4..173fa922f56 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -101,7 +101,7 @@ .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } .filter-item.inline = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 9a9a3ff9220..855a995afa9 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,3 +1,5 @@ +- return unless current_user + .hidden-xs - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do @@ -5,29 +7,27 @@ - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do - New snippet + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do + New snippet - if @snippet.submittable_as_spam? && current_user.admin? = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' -- if current_user - .visible-xs-block.dropdown - %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-full-width - %ul +.visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + = icon('caret-down') + .dropdown-menu.dropdown-menu-full-width + %ul + %li + = link_to new_snippet_path, title: "New snippet" do + New snippet + - if can?(current_user, :admin_personal_snippet, @snippet) %li - = link_to new_snippet_path, title: "New snippet" do - New snippet - - if can?(current_user, :admin_personal_snippet, @snippet) - %li - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do - Delete - - if can?(current_user, :update_personal_snippet, @snippet) - %li - = link_to edit_snippet_path(@snippet) do - Edit - - if @snippet.submittable_as_spam? && current_user.admin? - %li - = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete + - if can?(current_user, :update_personal_snippet, @snippet) + %li + = link_to edit_snippet_path(@snippet) do + Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb new file mode 100644 index 00000000000..4eeb9666bb0 --- /dev/null +++ b/app/workers/pages_worker.rb @@ -0,0 +1,23 @@ +class PagesWorker + include Sidekiq::Worker + + sidekiq_options queue: :pages, retry: false + + def perform(action, *arg) + send(action, *arg) + end + + def deploy(build_id) + build = Ci::Build.find_by(id: build_id) + result = Projects::UpdatePagesService.new(build.project, build).execute + if result[:status] == :success + result = Projects::UpdatePagesConfigurationService.new(build.project).execute + end + result + end + + def remove(namespace_path, project_path) + full_path = File.join(Settings.pages.path, namespace_path, project_path) + FileUtils.rm_r(full_path, force: true) + end +end diff --git a/changelogs/unreleased/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml deleted file mode 100644 index c26a20f87e2..00000000000 --- a/changelogs/unreleased/19164-mobile-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 19164 Add settings dropdown to mobile screens -merge_request: -author: diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml new file mode 100644 index 00000000000..bd6c9d7521e --- /dev/null +++ b/changelogs/unreleased/21518_recaptcha_spam_issues.yml @@ -0,0 +1,4 @@ +--- +title: Use reCaptcha when an issue is identified as a spam +merge_request: 8846 +author: diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml new file mode 100644 index 00000000000..14e80cacbfb --- /dev/null +++ b/changelogs/unreleased/24147-delete-env-button.yml @@ -0,0 +1,4 @@ +--- +title: Adds back ability to stop all environments +merge_request: 7379 +author: diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml new file mode 100644 index 00000000000..13de5db5e41 --- /dev/null +++ b/changelogs/unreleased/24716-fix-ctrl-click-links.yml @@ -0,0 +1,4 @@ +--- +title: Fix Ctrl+Click support for Todos and Merge Request page tabs +merge_request: 8898 +author: diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml new file mode 100644 index 00000000000..fc3f1af5b61 --- /dev/null +++ b/changelogs/unreleased/26059-segoe-ui-vertical.yml @@ -0,0 +1,4 @@ +--- +title: Align Segoe UI label text +merge_request: +author: diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys new file mode 100644 index 00000000000..0e8f7093b34 --- /dev/null +++ b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys @@ -0,0 +1,4 @@ +--- +title: Refactor Timelogs structure to use foreign keys. +merge_request: 8769 +author: diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml new file mode 100644 index 00000000000..ea567437ac2 --- /dev/null +++ b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml @@ -0,0 +1,4 @@ +--- +title: Fixes hover cursor on pipeline pagenation +merge_request: 9003 +author: diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml deleted file mode 100644 index 1758ed9e9ea..00000000000 --- a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support non-ASCII characters in GFM autocomplete -merge_request: 8729 -author: diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml deleted file mode 100644 index ddd454da376..00000000000 --- a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix permalink discussion note being collapsed -merge_request: -author: diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml new file mode 100644 index 00000000000..3f902fb324e --- /dev/null +++ b/changelogs/unreleased/27240-make-progress-bars-consistent.yml @@ -0,0 +1,4 @@ +--- +title: 27240 Make progress bars consistent +merge_request: +author: diff --git a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml b/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml deleted file mode 100644 index 6e036923158..00000000000 --- a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix filtering with multiple words -merge_request: 8830 -author: diff --git a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml b/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml deleted file mode 100644 index 2591f161bc5..00000000000 --- a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix project name label's for reference in project settings -merge_request: 8795 -author: diff --git a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml new file mode 100644 index 00000000000..8f061a34ac0 --- /dev/null +++ b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml @@ -0,0 +1,5 @@ +--- +title: Fix filtered search user autocomplete for gitlab instances that are hosted + on a subdirectory +merge_request: 8891 +author: diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml new file mode 100644 index 00000000000..191b530aee8 --- /dev/null +++ b/changelogs/unreleased/27352-search-label-filter-header.yml @@ -0,0 +1,4 @@ +--- +title: 27352-search-label-filter-header +merge_request: +author: diff --git a/changelogs/unreleased/27632_fix_mr_widget_url.yml b/changelogs/unreleased/27632_fix_mr_widget_url.yml new file mode 100644 index 00000000000..958621a43a1 --- /dev/null +++ b/changelogs/unreleased/27632_fix_mr_widget_url.yml @@ -0,0 +1,4 @@ +--- +title: Fix MR widget url +merge_request: 8989 +author: diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml new file mode 100644 index 00000000000..0531ef2c038 --- /dev/null +++ b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml @@ -0,0 +1,4 @@ +--- +title: Layer award emoji dropdown over the right sidebar +merge_request: 9004 +author: diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml new file mode 100644 index 00000000000..aa89d9f9850 --- /dev/null +++ b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml @@ -0,0 +1,4 @@ +--- +title: Give ci status text on pipeline graph a better font-weight +merge_request: +author: diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml new file mode 100644 index 00000000000..136d2108c63 --- /dev/null +++ b/changelogs/unreleased/8082-permalink-to-file.yml @@ -0,0 +1,4 @@ +--- +title: Add `y` keyboard shortcut to move to file permalink +merge_request: +author: diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml new file mode 100644 index 00000000000..67603bfab3b --- /dev/null +++ b/changelogs/unreleased/api-remove-snippets-expires-at.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove deprecated ''expires_at'' from project snippets' +merge_request: 8723 +author: Robert Schilling diff --git a/changelogs/unreleased/dont-delete-assigned-issuables.yml b/changelogs/unreleased/dont-delete-assigned-issuables.yml new file mode 100644 index 00000000000..fb589a053c0 --- /dev/null +++ b/changelogs/unreleased/dont-delete-assigned-issuables.yml @@ -0,0 +1,4 @@ +--- +title: Don't delete assigned MRs/issues when user is deleted +merge_request: +author: diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml new file mode 100644 index 00000000000..b5cc6bbf8b6 --- /dev/null +++ b/changelogs/unreleased/fe-commit-mr-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Use vue.js Pipelines table in commit and merge request view +merge_request: 8844 +author: diff --git a/changelogs/unreleased/fix-anchor-scrolling.yml b/changelogs/unreleased/fix-anchor-scrolling.yml new file mode 100644 index 00000000000..43b3b9bf96e --- /dev/null +++ b/changelogs/unreleased/fix-anchor-scrolling.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken anchor links when special characters are used +merge_request: 8961 +author: Andrey Krivko diff --git a/changelogs/unreleased/fix-cancel-integration-settings.yml b/changelogs/unreleased/fix-cancel-integration-settings.yml deleted file mode 100644 index 294b0aa5db9..00000000000 --- a/changelogs/unreleased/fix-cancel-integration-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed services form cancel not redirecting back the integrations settings view -merge_request: 8843 -author: diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml new file mode 100644 index 00000000000..e13215f22a7 --- /dev/null +++ b/changelogs/unreleased/fix-deleting-project-again.yml @@ -0,0 +1,4 @@ +--- +title: Fix deleting projects with pipelines and builds +merge_request: 8960 +author: diff --git a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml deleted file mode 100644 index 3513f5afdfb..00000000000 --- a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix filtering usernames with multiple words -merge_request: 8851 -author: diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml new file mode 100644 index 00000000000..df7e3776700 --- /dev/null +++ b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml @@ -0,0 +1,4 @@ +--- +title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context +merge_request: 8981 +author: diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml new file mode 100644 index 00000000000..fe580af31b3 --- /dev/null +++ b/changelogs/unreleased/fix-import-group-members.yml @@ -0,0 +1,4 @@ +--- +title: Add ability to export project inherited group members to Import/Export +merge_request: 8923 +author: diff --git a/changelogs/unreleased/fix-import-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml deleted file mode 100644 index 985a3b0b26f..00000000000 --- a/changelogs/unreleased/fix-import-user-validation-error.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove old project members when retrying an export -merge_request: -author: diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml new file mode 100644 index 00000000000..b927279cdf4 --- /dev/null +++ b/changelogs/unreleased/fix-references-header-parsing.yml @@ -0,0 +1,5 @@ +--- +title: Fix reply by email without sub-addressing for some clients from + Microsoft and Apple +merge_request: 8620 +author: diff --git a/changelogs/unreleased/fix-search-bar-search-param.yml b/changelogs/unreleased/fix-search-bar-search-param.yml deleted file mode 100644 index 4df14d3bf13..00000000000 --- a/changelogs/unreleased/fix-search-bar-search-param.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix search bar search param encoding -merge_request: 8753 -author: diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml new file mode 100644 index 00000000000..5dea1493f23 --- /dev/null +++ b/changelogs/unreleased/issue_19262.yml @@ -0,0 +1,4 @@ +--- +title: Disallow system notes for closed issuables +merge_request: +author: diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml new file mode 100644 index 00000000000..ee4a43a93db --- /dev/null +++ b/changelogs/unreleased/jej-pages-picked-from-ee.yml @@ -0,0 +1,4 @@ +--- +title: Added GitLab Pages to CE +merge_request: 8463 +author: diff --git a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml new file mode 100644 index 00000000000..a94bcdaa9a3 --- /dev/null +++ b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml @@ -0,0 +1,4 @@ +--- +title: Refresh authorizations when transferring projects +merge_request: +author: diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml new file mode 100644 index 00000000000..3ff69adb4d3 --- /dev/null +++ b/changelogs/unreleased/remove-deploy-key-endpoint.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove /projects/:id/keys/.. endpoints' +merge_request: 8716 +author: Robert Schilling diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml new file mode 100644 index 00000000000..f42aa6fae79 --- /dev/null +++ b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml @@ -0,0 +1,4 @@ +--- +title: Don't use backup Active Record connections for Sidekiq +merge_request: +author: diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml new file mode 100644 index 00000000000..9b6df0c54af --- /dev/null +++ b/changelogs/unreleased/route-map.yml @@ -0,0 +1,4 @@ +--- +title: Add 'View on [env]' link to blobs and individual files in diffs +merge_request: 8867 +author: diff --git a/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml b/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml deleted file mode 100644 index e69fcd2aa63..00000000000 --- a/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add project ID index to `project_authorizations` table to optimize queries -merge_request: -author: diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml deleted file mode 100644 index 4867f088953..00000000000 --- a/changelogs/unreleased/snippet-spam.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Check public snippets for spam -merge_request: -author: diff --git a/changelogs/unreleased/zj-slow-service-fetch.yml b/changelogs/unreleased/zj-slow-service-fetch.yml deleted file mode 100644 index 8037361d2fc..00000000000 --- a/changelogs/unreleased/zj-slow-service-fetch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve performance of slash commands -merge_request: 8876 -author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2906633fcbc..cc1af77a1de 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -153,6 +153,21 @@ production: &base # The location where LFS objects are stored (default: shared/lfs-objects). # storage_path: shared/lfs-objects + ## GitLab Pages + pages: + enabled: false + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + # The domain under which the pages are served: + # http://group.example.com/project + # or project path can be a group page: group.example.com + host: example.com + port: 80 # Set to 443 if you serve the pages with HTTPS + https: false # Set to true if you serve the pages with HTTPS + # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages + # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages + ## Mattermost ## For enabling Add to Mattermost button mattermost: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ea61aa9e047..ab59394cb0c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -6,7 +6,7 @@ class Settings < Settingslogic class << self def gitlab_on_standard_port? - gitlab.port.to_i == (gitlab.https ? 443 : 80) + on_standard_port?(gitlab) end def host_without_www(url) @@ -14,7 +14,7 @@ class Settings < Settingslogic end def build_gitlab_ci_url - if gitlab_on_standard_port? + if on_standard_port?(gitlab) custom_port = nil else custom_port = ":#{gitlab.port}" @@ -27,6 +27,10 @@ class Settings < Settingslogic ].join('') end + def build_pages_url + base_url(pages).join('') + end + def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -42,11 +46,11 @@ class Settings < Settingslogic end def build_base_gitlab_url - base_gitlab_url.join('') + base_url(gitlab).join('') end def build_gitlab_url - (base_gitlab_url + [gitlab.relative_url_root]).join('') + (base_url(gitlab) + [gitlab.relative_url_root]).join('') end # check that values in `current` (string or integer) is a contant in `modul`. @@ -74,15 +78,19 @@ class Settings < Settingslogic private - def base_gitlab_url - custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}" - [ gitlab.protocol, + def base_url(config) + custom_port = on_standard_port?(config) ? nil : ":#{config.port}" + [ config.protocol, "://", - gitlab.host, + config.host, custom_port ] end + def on_standard_port?(config) + config.port.to_i == (config.https ? 443 : 80) + end + # Extract the host part of the given +url+. def host(url) url = url.downcase @@ -255,6 +263,20 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root) # +# Pages +# +Settings['pages'] ||= Settingslogic.new({}) +Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? +Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root) +Settings.pages['https'] = false if Settings.pages['https'].nil? +Settings.pages['host'] ||= "example.com" +Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 +Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" +Settings.pages['url'] ||= Settings.send(:build_pages_url) +Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil? +Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil? + +# # Git LFS # Settings['lfs'] ||= Settingslogic.new({}) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index fa318384405..0c4516b70f0 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -36,11 +36,9 @@ Sidekiq.configure_server do |config| Gitlab::SidekiqThrottler.execute! - # Database pool should be at least `sidekiq_concurrency` + 2 - # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md config = ActiveRecord::Base.configurations[Rails.env] || Rails.application.config.database_configuration[Rails.env] - config['pool'] = Sidekiq.options[:concurrency] + 2 + config['pool'] = Sidekiq.options[:concurrency] ActiveRecord::Base.establish_connection(config) Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") diff --git a/config/karma.config.js b/config/karma.config.js index 44229e2ee88..59b95427dbc 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -13,9 +13,17 @@ module.exports = function(config) { { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, ], preprocessors: { - 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'], + 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap', 'coverage'], }, + reporters: ['progress', 'coverage'], webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, + coverageReporter: { + dir: './coverage-javascript', + reporters: [ + { type: 'html', subdir: 'default' }, + { type: 'text-summary' } + ], + } }); }; diff --git a/config/routes/project.rb b/config/routes/project.rb index 7cd4a73b1a0..2ac98cf3842 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -39,6 +39,10 @@ constraints(ProjectUrlConstrainer.new) do end end + resource :pages, only: [:show, :destroy] do + resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ } + end + resources :compare, only: [:index, :create] do collection do get :diff_for_path @@ -311,6 +315,7 @@ constraints(ProjectUrlConstrainer.new) do end namespace :settings do resource :members, only: [:show] + resource :ci_cd, only: [:show], controller: 'ci_cd' resource :integrations, only: [:show] end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 022b0e80917..56bf4e6b1de 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -50,3 +50,4 @@ - [reactive_caching, 1] - [cronjob, 1] - [default, 1] + - [pages, 1] diff --git a/config/webpack.config.js b/config/webpack.config.js index a156756f9ff..953ae463c86 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -19,6 +19,7 @@ var config = { boards: './boards/boards_bundle.js', boards_test: './boards/test_utils/simulate_drag.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', + commit_pipelines: './commit/pipelines/pipelines_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js', environments: './environments/environments_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', diff --git a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb new file mode 100644 index 00000000000..f3a663f805b --- /dev/null +++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb @@ -0,0 +1,14 @@ +class AddPagesSizeToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default :application_settings, :max_pages_size, :integer, default: 100, allow_null: false + end + + def down + remove_column(:application_settings, :max_pages_size) + end +end diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb new file mode 100644 index 00000000000..0e8507c7e9a --- /dev/null +++ b/db/migrate/20160210105555_create_pages_domain.rb @@ -0,0 +1,16 @@ +class CreatePagesDomain < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :pages_domains do |t| + t.integer :project_id + t.text :certificate + t.text :encrypted_key + t.string :encrypted_key_iv + t.string :encrypted_key_salt + t.string :domain + end + + add_index :pages_domains, :domain, unique: true + end +end diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb new file mode 100644 index 00000000000..69bfa2d3fc4 --- /dev/null +++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb @@ -0,0 +1,54 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddForeignKeysToTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + change_table :timelogs do |t| + t.column :issue_id, :integer + t.column :merge_request_id, :integer + end + + add_concurrent_index :timelogs, :issue_id + add_concurrent_index :timelogs, :merge_request_id + + if Gitlab::Database.postgresql? + execute <<-EOF + ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID; + ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID; + EOF + else + execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;" + execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;" + end + + Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id") + Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id") + end + + def down + Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'") + Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'") + + remove_columns :timelogs, :issue_id, :merge_request_id + end +end diff --git a/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb new file mode 100644 index 00000000000..44372334d21 --- /dev/null +++ b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb @@ -0,0 +1,15 @@ +class AddRecaptchaVerifiedToSpamLogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:spam_logs, :recaptcha_verified, :boolean, default: false) + end + + def down + remove_column(:spam_logs, :recaptcha_verified) + end +end diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb new file mode 100644 index 00000000000..89aa753646c --- /dev/null +++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.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 RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + remove_columns :timelogs, :trackable_id, :trackable_type + end +end diff --git a/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb new file mode 100644 index 00000000000..f397ef919cc --- /dev/null +++ b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb @@ -0,0 +1,32 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + if Gitlab::Database.postgresql? + execute <<-EOF + ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id"; + ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id"; + EOF + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index a9f4e865d60..850772ba356 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170204181513) do +ActiveRecord::Schema.define(version: 20170206101030) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,17 +98,18 @@ ActiveRecord::Schema.define(version: 20170204181513) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" 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 t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false end @@ -857,6 +858,17 @@ ActiveRecord::Schema.define(version: 20170204181513) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "pages_domains", force: :cascade do |t| + t.integer "project_id" + t.text "certificate" + t.text "encrypted_key" + t.string "encrypted_key_iv" + t.string "encrypted_key_salt" + t.string "domain" + end + + add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree + create_table "personal_access_tokens", force: :cascade do |t| t.integer "user_id", null: false t.string "token", null: false @@ -1102,6 +1114,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "submitted_as_ham", default: false, null: false + t.boolean "recaptcha_verified", default: false, null: false end create_table "subscriptions", force: :cascade do |t| @@ -1138,14 +1151,15 @@ ActiveRecord::Schema.define(version: 20170204181513) do create_table "timelogs", force: :cascade do |t| t.integer "time_spent", null: false - t.integer "trackable_id" - t.string "trackable_type" t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "issue_id" + t.integer "merge_request_id" end - add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree + add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree + add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree create_table "todos", force: :cascade do |t| @@ -1327,6 +1341,8 @@ ActiveRecord::Schema.define(version: 20170204181513) do add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "subscriptions", "projects", on_delete: :cascade + add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade + add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" end diff --git a/doc/README.md b/doc/README.md index d5f0c37325e..78030ce4e72 100644 --- a/doc/README.md +++ b/doc/README.md @@ -12,6 +12,7 @@ - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry. - [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. +- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages. - [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab. - [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Markdown](user/markdown.md) GitLab's advanced formatting system. @@ -53,6 +54,7 @@ - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. +- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages. - [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics. - [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics. - [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests. diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 1824829903c..dad8e956c0e 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -66,4 +66,4 @@ Read more on high-availability configuration: configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. -[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html +[gitlab-pages]: ../pages/index.md diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 5602d70f1ef..3893d837006 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -47,13 +47,13 @@ When using default Omnibus configuration you will need to share 5 data locations between all GitLab cluster nodes. No other locations should be shared. The following are the 5 locations you need to mount: -| Location | Description | -| -------- | ----------- | -| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data -| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services -| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments -| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data -| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces +| Location | Description | Default configuration | +| -------- | ----------- | --------------------- | +| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})` +| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'` +| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'` +| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'` +| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'` Other GitLab directories should not be shared between nodes. They contain node-specific files and GitLab code that does not need to be shared. To ship @@ -73,10 +73,10 @@ as subdirectories. Mount `/gitlab-data` then use the following Omnibus configuration to move each data location to a subdirectory: ```ruby +git_data_dirs({"default" => "/gitlab-data/git-data"}) user['home'] = '/gitlab-data/home' -git_data_dir '/gitlab-data/git-data' -gitlab_rails['shared_path'] = '/gitlab-data/shared' gitlab_rails['uploads_directory'] = '/gitlab-data/uploads' +gitlab_rails['shared_path'] = '/gitlab-data/shared' gitlab_ci['builds_directory'] = '/gitlab-data/builds' ``` diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 11444464537..3b5ee86b68b 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -82,4 +82,4 @@ GitLab instance if you find this undesirable from a scalability or security point of view. [ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690 -[kubservice]: ../../user/project/integrations/kubernetes.md) +[kubservice]: ../../user/project/integrations/kubernetes.md diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md new file mode 100644 index 00000000000..c352caf1115 --- /dev/null +++ b/doc/administration/pages/index.md @@ -0,0 +1,249 @@ +# GitLab Pages Administration + +> **Notes:** +- [Introduced][ee-80] in GitLab EE 8.3. +- Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +- GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. +- This guide is for Omnibus GitLab installations. If you have installed + GitLab from source, follow the [Pages source installation document](source.md). + +--- + +This document describes how to set up the _latest_ GitLab Pages feature. Make +sure to read the [changelog](#changelog) if you are upgrading to a new GitLab +version as it may include new features and changes needed to be made in your +configuration. + +If you are looking for ways to upload your static content in GitLab Pages, you +probably want to read the [user documentation][pages-userguide]. + +## Overview + +GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server +written in Go that can listen on an external IP address and provide support for +custom domains and custom certificates. It supports dynamic certificates through +SNI and exposes pages using HTTP2 by default. +You are encouraged to read its [README][pages-readme] to fully understand how +it works. + +--- + +In the case of custom domains, the Pages daemon needs to listen on ports `80` +and/or `443`. For that reason, there is some flexibility in the way which you +can set it up: + +1. Run the pages daemon in the same server as GitLab, listening on a secondary IP +1. Run the pages daemon in a separate server. In that case, the + [Pages path](#change-storage-path) must also be present in the server that + the pages daemon is installed, so you will have to share it via network. +1. Run the pages daemon in the same server as GitLab, listening on the same IP + but on different ports. In that case, you will have to proxy the traffic with + a loadbalancer. If you choose that route note that you should use TCP load + balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the + pages will not be able to be served with user provided certificates. For + HTTP it's OK to use HTTP or TCP load balancing. + +In this document, we will proceed assuming the first option. + +## Prerequisites + +Before proceeding with the Pages configuration, you will need to: + +1. Have a separate domain under which the GitLab Pages will be served. In this + document we assume that to be `example.io`. +1. Configure a **wildcard DNS record**. +1. (Optional) Have a **wildcard certificate** for that domain if you decide to + serve Pages under HTTPS. +1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md) + so that your users don't have to bring their own. + +### DNS configuration + +GitLab Pages expect to run on their own virtual host. In your DNS server/provider +you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the +host that GitLab runs. For example, an entry would look like this: + +``` +*.example.io. 1800 IN A 1.2.3.4 +``` + +where `example.io` is the domain under which GitLab Pages will be served +and `1.2.3.4` is the IP address of your GitLab instance. + +> **Note:** +You should not use the GitLab domain to serve user pages. For more information +see the [security section](#security). + +[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record + +## Configuration + +Depending on your needs, you can install GitLab Pages in four different ways. + +### Option 1. Custom domains with HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url "https://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" + gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" + gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_https'] = '1.1.1.2:443' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + +1. [Reconfigure GitLab][reconfigure] + +### Option 2. Custom domains without HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` and `http://page.com` | no | yes | no | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url "http://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['external_http'] = '1.1.1.2:80' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + +1. [Reconfigure GitLab][reconfigure] + +### Option 3. Wildcard HTTPS domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` | yes | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +1. Place the certificate and key inside `/etc/gitlab/ssl` +1. In `/etc/gitlab/gitlab.rb` specify the following configuration: + + ```ruby + pages_external_url 'https://example.io' + + pages_nginx['redirect_http_to_https'] = true + pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" + pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" + ``` + + where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, + respectively. + +1. [Reconfigure GitLab][reconfigure] + +### Option 4. Wildcard HTTP domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` | no | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url 'http://example.io' + ``` + +1. [Reconfigure GitLab][reconfigure] + +## Change storage path + +Follow the steps below to change the default path where GitLab Pages' contents +are stored. + +1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. + If you wish to store them in another location you must set it up in + `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['pages_path'] = "/mnt/storage/pages" + ``` + +1. [Reconfigure GitLab][reconfigure] + +## Set maximum pages size + +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. + +## Backup + +Pages are part of the [regular backup][backup] so there is nothing to configure. + +## Security + +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS attacks. + +## Changelog + +GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features +where added, like custom CNAME and TLS support, and many more are likely to +come. Below is a brief changelog. If no changes were introduced or a version is +missing from the changelog, assume that the documentation is the same as the +latest previous version. + +--- + +**GitLab 8.17 ([documentation][8-17-docs])** + +- GitLab Pages were ported to Community Edition in GitLab 8.17. +- Documentation was refactored to be more modular and easy to follow. + +**GitLab 8.5 ([documentation][8-5-docs])** + +- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the + recommended way to set up GitLab Pages. +- The [NGINX configs][] have changed to reflect this change. So make sure to + update them. +- Custom CNAME and TLS certificates support. +- Documentation was moved to one place. + +**GitLab 8.3 ([documentation][8-3-docs])** + +- GitLab Pages feature was introduced. + +[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md +[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md +[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md +[backup]: ../raketasks/backup_restore.md +[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md +[pages-userguide]: ../../user/project/pages/index.md +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../administration/restart_gitlab.md#installations-from-source +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md new file mode 100644 index 00000000000..d4468b99992 --- /dev/null +++ b/doc/administration/pages/source.md @@ -0,0 +1,323 @@ +# GitLab Pages administration for source installations + +This is the documentation for configuring a GitLab Pages when you have installed +GitLab from source and not using the Omnibus packages. + +You are encouraged to read the [Omnibus documentation](index.md) as it provides +some invaluable information to the configuration of GitLab Pages. Please proceed +to read it before going forward with this guide. + +We also highly recommend that you use the Omnibus GitLab packages, as we +optimize them specifically for GitLab, and we will take care of upgrading GitLab +Pages to the latest supported version. + +## Overview + +[Read the Omnibus overview section.](index.md#overview) + +## Prerequisites + +[Read the Omnibus prerequisites section.](index.md#prerequisites) + +## Configuration + +Depending on your needs, you can install GitLab Pages in four different ways. + +### Option 1. Custom domains with HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` + +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` and `external_https` to the secondary IP on which the pages + daemon will listen for connections: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + + external_http: 1.1.1.2:80 + external_https: 1.1.1.2:443 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, + `external_http` and `external_https` settings that you set above respectively. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX +1. [Restart GitLab][restart] + +### Option 2. Custom domains without HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` and `http://page.com` | no | yes | no | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` + +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` to the secondary IP on which the pages daemon will listen + for connections: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + + external_http: 1.1.1.2:80 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` and `-listen-http` must match the `host` and `external_http` + settings that you set above respectively: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80" + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX +1. [Restart GitLab][restart] + +### Option 3. Wildcard HTTPS domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` | yes | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` +1. In `gitlab.yml`, set the port to `443` and https to `true`: + + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Restart NGINX +1. [Restart GitLab][restart] + +### Option 4. Wildcard HTTP domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` | no | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` + +1. Go to the GitLab installation directory: + + ```bash + cd /home/git/gitlab + ``` + +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and + the `host` to the FQDN under which GitLab Pages will be served: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Restart NGINX +1. [Restart GitLab][restart] + +## NGINX caveats + +>**Note:** +The following information applies only for installations from source. + +Be extra careful when setting up the domain name in the NGINX config. You must +not remove the backslashes. + +If your GitLab pages domain is `example.io`, replace: + +```bash +server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; +``` + +with: + +``` +server_name ~^.*\.example\.io$; +``` + +If you are using a subdomain, make sure to escape all dots (`.`) except from +the first one with a backslash (\). For example `pages.example.io` would be: + +``` +server_name ~^.*\.pages\.example\.io$; +``` + +## Change storage path + +Follow the steps below to change the default path where GitLab Pages' contents +are stored. + +1. Pages are stored by default in `/home/git/gitlab/shared/pages`. + If you wish to store them in another location you must set it up in + `gitlab.yml` under the `pages` section: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + path: /mnt/storage/pages + ``` + +1. [Restart GitLab][restart] + +## Set maximum Pages size + +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. + +## Backup + +Pages are part of the [regular backup][backup] so there is nothing to configure. + +## Security + +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS attacks. + +[backup]: ../raketasks/backup_restore.md +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md +[pages-userguide]: ../../user/project/pages/index.md +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../administration/restart_gitlab.md#installations-from-source +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md index 22f10489a6c..3b8c716eff5 100644 --- a/doc/administration/reply_by_email_postfix_setup.md +++ b/doc/administration/reply_by_email_postfix_setup.md @@ -315,7 +315,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo ## Done! -If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab. +If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./reply_by_email.md) guide to configure GitLab. --- diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index c6685f54a9d..404876f6237 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -51,7 +51,6 @@ Parameters: "state": "active", "created_at": "2012-05-23T08:00:58Z" }, - "expires_at": null, "updated_at": "2012-06-28T10:52:04Z", "created_at": "2012-06-28T10:52:04Z", "web_url": "http://example.com/example/example/snippets/1" diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 8a6baed5987..73dde599b7e 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -53,7 +53,7 @@ Example response: ```json { - "file_name": "app/project.rb", + "file_path": "app/project.rb", "branch_name": "master" } ``` @@ -82,7 +82,7 @@ Example response: ```json { - "file_name": "app/project.rb", + "file_path": "app/project.rb", "branch_name": "master" } ``` @@ -120,7 +120,7 @@ Example response: ```json { - "file_name": "app/project.rb", + "file_path": "app/project.rb", "branch_name": "master" } ``` diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 9748aec17ad..707f0437b7e 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -10,4 +10,5 @@ changes are in V4: - `iid` filter has been removed from `projects/:id/issues` - `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` - Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) - +- Project snippets do not return deprecated field `expires_at` +- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 579135c2052..f00cc854b05 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -442,6 +442,45 @@ and/or `production`) you can see this information in the merge request itself. ![Environment URLs in merge request](img/environments_link_url_mr.png) +### Go directly from source files to public pages on the environment + +> Introduced in GitLab 8.17. + +To go one step further, we can specify a Route Map to get GitLab to show us "View on [environment URL]" buttons to go directly from a file to that file's representation on the deployed website. It will be exposed in a few places: + +| In the diff for a merge request, comparison or commit | In the file view | +| ------ | ------ | +| !["View on env" button in merge request diff](img/view_on_env_mr.png) | !["View on env" button in file view](img/view_on_env_blob.png) | + +To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map. + +A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website). + +This is an example of a route map for [Middleman](https://middlemanapp.com) static websites like [http://about.gitlab.com](https://gitlab.com/gitlab-com/www-gitlab-com): + +```yaml +# Blogposts +- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb + public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ + +# HTML files +- source: /source\/(.+?\.html).*/ # source/index.html.haml + public: '\1' # index.html + +# Other files +- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png + public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png +``` + +Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys: + +- `source`: a regular expression, starting and ending with `/`. Can include capture groups denoted by `()` that can be referred to in the `public` path. Slashes (`/`) can, but don't have to be, escaped as `\/`. +- `public`: a string, starting and ending with `'`. Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`. + +The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups. + +In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`. + --- We now have a full development cycle, where our app is tested, built, deployed diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/img/view_on_env_blob.png Binary files differnew file mode 100644 index 00000000000..f4fe99046f0 --- /dev/null +++ b/doc/ci/img/view_on_env_blob.png diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/img/view_on_env_mr.png Binary files differnew file mode 100644 index 00000000000..47ddb40bdc1 --- /dev/null +++ b/doc/ci/img/view_on_env_mr.png diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index f91b9d350f7..2c7c3ef3c18 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -75,5 +75,5 @@ respective link in the [Pipelines settings] page. [builds]: #builds [jobs]: yaml/README.md#jobs [stages]: yaml/README.md#stages -[runners]: runners/READM +[runners]: runners/README.html [pipelines settings]: ../user/project/pipelines/settings.md diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 06810898cfe..cd492d16747 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -76,7 +76,6 @@ There are a few reserved `keywords` that **cannot** be used as job names: | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | -| coverage | no | Define coverage settings for all jobs | ### image and services @@ -279,23 +278,6 @@ cache: untracked: true ``` -### coverage - -`coverage` allows you to configure how coverage will be filtered out from the -build outputs. Setting this up globally will make all the jobs to use this -setting for output filtering and extracting the coverage information from your -builds. - -Regular expressions are the only valid kind of value expected here. So, using -surrounding `/` is mandatory in order to consistently and explicitly represent -a regular expression string. You must escape special characters if you want to -match them literally. - -A simple example: -```yaml -coverage: /\(\d+\.\d+\) covered\./ -``` - ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job @@ -337,7 +319,7 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | -| coverage | no | Define coverage settings for a given job | +| coverage | no | Define code coverage settings for a given job | ### script @@ -1012,25 +994,23 @@ job: - execute this after my script ``` -### job coverage +### coverage -This entry is pretty much the same as described in the global context in -[`coverage`](#coverage). The only difference is that, by setting it inside -the job level, whatever is set in there will take precedence over what has -been defined in the global level. A quick example of one overriding the -other would be: +`coverage` allows you to configure how code coverage will be extracted from the +job output. -```yaml -coverage: /\(\d+\.\d+\) covered\./ +Regular expressions are the only valid kind of value expected here. So, using +surrounding `/` is mandatory in order to consistently and explicitly represent +a regular expression string. You must escape special characters if you want to +match them literally. +A simple example: + +```yaml job1: coverage: /Code coverage: \d+\.\d+/ ``` -In the example above, considering the context of the job `job1`, the coverage -regex that would be used is `/Code coverage: \d+\.\d+/` instead of -`/\(\d+\.\d+\) covered\./`. - ## Git Strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed @@ -1319,6 +1299,35 @@ with an API call. [Read more in the triggers documentation.](../triggers/README.md) +### pages + +`pages` is a special job that is used to upload static content to GitLab that +can be used to serve your website. It has a special syntax, so the two +requirements below must be met: + +1. Any static content must be placed under a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +The example below simply moves all files from the root of the project to the +`public/` directory. The `.public` workaround is so `cp` doesn't also copy +`public/` to itself in an infinite loop: + +``` +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +Read more on [GitLab Pages user documentation](../../pages/README.md). + ## Validate the .gitlab-ci.yml Each instance of GitLab CI has an embedded debug tool called Lint. diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index 2d1d504202c..df6ac452300 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -20,8 +20,8 @@ The content section contains a header and the content itself. The header describ available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group. -You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle) -along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports. +You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository][gitlab-map-graffle] +along with [PDF][gitlab-map-pdf] and [PNG][gitlab-map-png] exports. ### Adding new tab to header navigation @@ -104,4 +104,4 @@ Do not use both green and blue button in one form. [number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter [gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle [gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf -[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
\ No newline at end of file +[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png diff --git a/doc/install/installation.md b/doc/install/installation.md index b2d5d51d37d..355179960b3 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -271,9 +271,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-16-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab -**Note:** You can change `8-16-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -313,6 +313,9 @@ sudo usermod -aG redis git # Change the permissions of the directory where CI artifacts are stored sudo chmod -R u+rwX shared/artifacts/ + # Change the permissions of the directory where GitLab Pages are stored + sudo chmod -R ug+rwX shared/pages/ + # Copy the example Unicorn config sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb @@ -484,6 +487,10 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat # or else sudo rm -f /etc/nginx/sites-enabled/default sudo editor /etc/nginx/sites-available/gitlab +If you intend to enable GitLab pages, there is a separate Nginx config you need +to use. Read all about the needed configuration at the +[GitLab Pages administration guide](../administration/pages/index.md). + **Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details. ### Test Configuration diff --git a/doc/pages/README.md b/doc/pages/README.md new file mode 100644 index 00000000000..c9715eed598 --- /dev/null +++ b/doc/pages/README.md @@ -0,0 +1 @@ +This document was moved to [user/project/pages](../user/project/pages/index.md). diff --git a/doc/pages/administration.md b/doc/pages/administration.md new file mode 100644 index 00000000000..4eb3bb32c77 --- /dev/null +++ b/doc/pages/administration.md @@ -0,0 +1 @@ +This document was moved to [administration/pages](../administration/pages/index.md). diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index f6b4db71b44..0fb69d63dbe 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -84,6 +84,29 @@ Deleting tmp directories...[DONE] Deleting old backups... [SKIPPING] ``` +## Exclude specific directories from the backup + +You can choose what should be backed up by adding the environment variable `SKIP`. +The available options are: + +* `db` +* `uploads` (attachments) +* `repositories` +* `builds` (CI build output logs) +* `artifacts` (CI build artifacts) +* `lfs` (LFS objects) +* `pages` (pages content) + +Use a comma to specify several options at the same time: + +``` +# use this command if you've installed GitLab with the Omnibus package +sudo gitlab-rake gitlab:backup:create SKIP=db,uploads + +# if you've installed GitLab from source +sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production +``` + ## Upload backups to remote (cloud) storage Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates. diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md index f9a46193547..fee49cc27cc 100644 --- a/doc/raketasks/features.md +++ b/doc/raketasks/features.md @@ -7,7 +7,7 @@ This command will enable the namespaces feature introduced in v4.0. It will move Note: - Because the **repository location will change**, you will need to **update all your git URLs** to point to the new location. -- Username can be changed at [Profile / Account](/profile/account) +- Username can be changed at **Profile ➔ Account**. **Example:** diff --git a/doc/university/README.md b/doc/university/README.md index c798e0d760d..e9f14703789 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -91,7 +91,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) 1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) -1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html) +1. [GitLab Pages Documentation](https://docs.gitlab.com/ce/user/project/pages/) #### 2.2. GitLab Issues diff --git a/doc/university/support/README.md b/doc/university/support/README.md index 6e415e4d219..ca538ef6dc3 100644 --- a/doc/university/support/README.md +++ b/doc/university/support/README.md @@ -172,7 +172,7 @@ Move on to understanding some of GitLab's more advanced features. You can make u - Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings - Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html) - Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html) -- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html) +- Create your first [GitLab Page](https://docs.gitlab.com/ce/administration/pages/) - Get to know the GitLab Codebase by reading through the source code: - Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce) and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce) diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md index 1ee615432aa..3ed601625cf 100755 --- a/doc/university/training/topics/additional_resources.md +++ b/doc/university/training/topics/additional_resources.md @@ -5,4 +5,4 @@ 3. Pro git book [http://git-scm.com/book](http://git-scm.com/book) 4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/) 5. Code School tutorial [http://try.github.io/](http://try.github.io/) -6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com) +6. Contact Us at `subscribers@gitlab.com` diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md index 35afe73708f..9e38df26b6a 100755 --- a/doc/university/training/user_training.md +++ b/doc/university/training/user_training.md @@ -389,4 +389,4 @@ GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/gui Pro git book [http://git-scm.com/book](http://git-scm.com/book) Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/) Code School tutorial [http://try.github.io/](http://try.github.io/) -Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com) +Contact Us at `subscribers@gitlab.com` diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md index fb70eaacbc9..97cd277b424 100644 --- a/doc/update/2.6-to-3.0.md +++ b/doc/update/2.6-to-3.0.md @@ -1,5 +1,5 @@ # From 2.6 to 3.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.6-to-3.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.* ## 1. Stop server & resque diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md index ce46b57c09a..a890aa885d5 100644 --- a/doc/update/2.9-to-3.0.md +++ b/doc/update/2.9-to-3.0.md @@ -1,5 +1,5 @@ # From 2.9 to 3.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.9-to-3.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.* ## 1. Stop server & resque diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md index 6ac83f3b60d..e32508745a2 100644 --- a/doc/update/3.0-to-3.1.md +++ b/doc/update/3.0-to-3.1.md @@ -1,5 +1,5 @@ # From 3.0 to 3.1 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.0-to-3.1.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.* **IMPORTANT!** diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md index df53ed6de83..b370464390e 100644 --- a/doc/update/3.1-to-4.0.md +++ b/doc/update/3.1-to-4.0.md @@ -1,5 +1,5 @@ # From 3.1 to 4.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.1-to-4.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.* ## Important changes diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md index c66c6dd0fd8..7124424bb60 100644 --- a/doc/update/4.0-to-4.1.md +++ b/doc/update/4.0-to-4.1.md @@ -1,5 +1,5 @@ # From 4.0 to 4.1 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.0-to-4.1.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.* ## Important changes diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md index 97367c5f347..8ed5b333a2e 100644 --- a/doc/update/4.1-to-4.2.md +++ b/doc/update/4.1-to-4.2.md @@ -1,5 +1,5 @@ # From 4.1 to 4.2 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.1-to-4.2.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.* ## 1. Stop server & Resque diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md index 7654f4a0131..1ec39218ba8 100644 --- a/doc/update/4.2-to-5.0.md +++ b/doc/update/4.2-to-5.0.md @@ -1,5 +1,5 @@ # From 4.2 to 5.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.2-to-5.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md index c19a819ab5a..9c9950fb2c6 100644 --- a/doc/update/5.0-to-5.1.md +++ b/doc/update/5.0-to-5.1.md @@ -1,5 +1,5 @@ # From 5.0 to 5.1 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.0-to-5.1.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md index 625fcc33852..2aab47d2d7c 100644 --- a/doc/update/5.1-to-5.2.md +++ b/doc/update/5.1-to-5.2.md @@ -1,5 +1,5 @@ # From 5.1 to 5.2 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.2.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md index 547d453914c..e80f1b89c63 100644 --- a/doc/update/5.1-to-5.4.md +++ b/doc/update/5.1-to-5.4.md @@ -1,5 +1,5 @@ # From 5.1 to 5.4 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.4.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.* Also works starting from 5.2. diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md index c992c69678e..1ee175383da 100644 --- a/doc/update/5.1-to-6.0.md +++ b/doc/update/5.1-to-6.0.md @@ -1,5 +1,5 @@ # From 5.1 to 6.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-6.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md index fe8990b6843..2ae50510f63 100644 --- a/doc/update/5.2-to-5.3.md +++ b/doc/update/5.2-to-5.3.md @@ -1,5 +1,5 @@ # From 5.2 to 5.3 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.2-to-5.3.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md index 5f82ad7d444..842e3bb6791 100644 --- a/doc/update/5.3-to-5.4.md +++ b/doc/update/5.3-to-5.4.md @@ -1,5 +1,5 @@ # From 5.3 to 5.4 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.3-to-5.4.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md index f0fee634322..44715984f0c 100644 --- a/doc/update/5.4-to-6.0.md +++ b/doc/update/5.4-to-6.0.md @@ -1,5 +1,5 @@ # From 5.4 to 6.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.4-to-6.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md index 409faf30902..0c672abeb05 100644 --- a/doc/update/6.0-to-6.1.md +++ b/doc/update/6.0-to-6.1.md @@ -1,5 +1,5 @@ # From 6.0 to 6.1 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.0-to-6.1.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.* ## Warning diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md index 150c7ae1c83..d3760cf0619 100644 --- a/doc/update/6.1-to-6.2.md +++ b/doc/update/6.1-to-6.2.md @@ -1,5 +1,5 @@ # From 6.1 to 6.2 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.1-to-6.2.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.* **You should update to 6.1 before installing 6.2 so all the necessary conversions are run.** diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md index b96dfb8add7..91105de2e29 100644 --- a/doc/update/6.2-to-6.3.md +++ b/doc/update/6.2-to-6.3.md @@ -1,5 +1,5 @@ # From 6.2 to 6.3 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.2-to-6.3.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.* **Requires version: 6.1 or 6.2.** diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md index 37028be055f..20b58ed8b25 100644 --- a/doc/update/6.3-to-6.4.md +++ b/doc/update/6.3-to-6.4.md @@ -1,5 +1,5 @@ # From 6.3 to 6.4 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.3-to-6.4.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md index 982381a4db0..5ee0f040b5d 100644 --- a/doc/update/6.4-to-6.5.md +++ b/doc/update/6.4-to-6.5.md @@ -1,5 +1,5 @@ # From 6.4 to 6.5 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.4-to-6.5.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md index bbed2b30215..fa3712f83ad 100644 --- a/doc/update/6.5-to-6.6.md +++ b/doc/update/6.5-to-6.6.md @@ -1,5 +1,5 @@ # From 6.5 to 6.6 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.5-to-6.6.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md index 8e82942a1a0..9c85ed091c5 100644 --- a/doc/update/6.6-to-6.7.md +++ b/doc/update/6.6-to-6.7.md @@ -1,5 +1,5 @@ # From 6.6 to 6.7 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.6-to-6.7.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md index 4fb90639f16..687c1265d9b 100644 --- a/doc/update/6.7-to-6.8.md +++ b/doc/update/6.7-to-6.8.md @@ -1,5 +1,5 @@ # From 6.7 to 6.8 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.7-to-6.8.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.* ## 0. Backup diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md index b9b8b63f652..0205b0c896a 100644 --- a/doc/update/6.8-to-6.9.md +++ b/doc/update/6.8-to-6.9.md @@ -1,5 +1,5 @@ # From 6.8 to 6.9 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.8-to-6.9.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md index 5352fd52f93..4b6e3989893 100644 --- a/doc/update/6.9-to-7.0.md +++ b/doc/update/6.9-to-7.0.md @@ -1,5 +1,5 @@ # From 6.9 to 7.0 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.9-to-7.0.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md index f170a0021b7..1e39fe47ef9 100644 --- a/doc/update/6.x-or-7.x-to-7.14.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -1,5 +1,5 @@ # From 6.x or 7.x to 7.14 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.* This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14. @@ -222,7 +222,7 @@ If all items are green, then congratulations upgrade complete! When using Google omniauth login, changes of the Google account required. Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/). -More details can be found at the [integration documentation](../../../master/doc/integration/google.md). +More details can be found at the [integration documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/integration/google.md). ## 12. Optional optimizations for GitLab setups with MySQL databases diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md index 71f39c44077..c717affebd3 100644 --- a/doc/update/7.0-to-7.1.md +++ b/doc/update/7.0-to-7.1.md @@ -1,5 +1,5 @@ # From 7.0 to 7.1 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.0-to-7.1.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md index 88cb63d7d41..d01f8528e14 100644 --- a/doc/update/7.1-to-7.2.md +++ b/doc/update/7.1-to-7.2.md @@ -1,5 +1,5 @@ # From 7.1 to 7.2 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.1-to-7.2.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.* ## Editable labels diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md index 18f77d6396e..0e91e682175 100644 --- a/doc/update/7.2-to-7.3.md +++ b/doc/update/7.2-to-7.3.md @@ -1,5 +1,5 @@ # From 7.2 to 7.3 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.2-to-7.3.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.* ### 0. Backup diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md index 53e739c06fb..4df9127dd5f 100644 --- a/doc/update/7.3-to-7.4.md +++ b/doc/update/7.3-to-7.4.md @@ -1,5 +1,5 @@ # From 7.3 to 7.4 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.3-to-7.4.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.* ### 0. Stop server diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md new file mode 100644 index 00000000000..1808232c59a --- /dev/null +++ b/doc/update/8.16-to-8.17.md @@ -0,0 +1,239 @@ +# From 8.16 to 8.17 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +We will continue supporting Ruby < 2.3 for the time being but we recommend you +upgrade to Ruby 2.3 if you're running a source installation, as this is the same +version that ships with our Omnibus package. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 8-17-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 8-17-stable-ee +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v4.1.1 +``` + +### 8. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/8-16-stable:config/gitlab.yml.example origin/8-17-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Configure Git to generate packfile bitmaps (introduced in Git 2.0) on +the GitLab server during `git gc`. + +```sh +cd /home/git/gitlab + +sudo -u git -H git config --global repack.writeBitmaps true +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/8-16-stable:lib/support/nginx/gitlab-ssl origin/8-17-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-16-stable:lib/support/nginx/gitlab origin/8-17-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 9. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 10. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.16) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.15 to 8.16](8.15-to-8.16.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/user/account/security.md b/doc/user/account/security.md index 9336dee7451..2459f913583 100644 --- a/doc/user/account/security.md +++ b/doc/user/account/security.md @@ -1 +1 @@ -This document was moved to [profile](../profile/index.md#security). +This document was moved to [profile](../profile/index.md). diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 678fc3ffd1f..e87cae092a5 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -62,11 +62,14 @@ The following table depicts the various user permission levels in a project. | Manage runners | | | | ✓ | ✓ | | Manage build triggers | | | | ✓ | ✓ | | Manage variables | | | | ✓ | ✓ | +| Manage pages | | | | ✓ | ✓ | +| Manage pages domains and certificates | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Force push to protected branches [^3] | | | | | | | Remove protected branches [^3] | | | | | | +| Remove pages | | | | | ✓ | ## Group diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index cc688a7f99c..a23ad79ae1d 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -206,7 +206,7 @@ Sign in and re-enable two-factor authentication as soon as possible. ## Note to GitLab administrators - You need to take special care to that 2FA keeps working after -[restoring a GitLab backup](../raketasks/backup_restore.md). +[restoring a GitLab backup](../../../raketasks/backup_restore.md). - To ensure 2FA authorizes correctly with TOTP server, you may want to ensure your GitLab server's time is synchronized via a service like NTP. Otherwise, diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 47a4a3f85d0..91b35c73b34 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -1,10 +1,7 @@ # GitLab Container Registry -> [Introduced][ce-4040] in GitLab 8.8. - ---- - >**Notes:** +> [Introduced][ce-4040] in GitLab 8.8. - Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker versions earlier than 1.10. - This document is about the user guide. To learn how to enable GitLab Container @@ -98,8 +95,8 @@ delete them. This feature requires GitLab 8.8 and GitLab Runner 1.2. Make sure that your GitLab Runner is configured to allow building Docker images by -following the [Using Docker Build](../ci/docker/using_docker_build.md) -and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry). +following the [Using Docker Build](../../ci/docker/using_docker_build.md) +and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry). ## Limitations @@ -252,4 +249,4 @@ Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ -[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry +[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png Binary files differindex a62e4b792f9..dd3481bc1f6 100644 --- a/doc/user/project/integrations/img/mattermost_config_help.png +++ b/doc/user/project/integrations/img/mattermost_config_help.png diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png Binary files differindex f69817f2b78..7928fb7d495 100644 --- a/doc/user/project/integrations/img/slack_setup.png +++ b/doc/user/project/integrations/img/slack_setup.png diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md index 99aa9e44bdb..7b33327f6cf 100644 --- a/doc/user/project/integrations/kubernetes.md +++ b/doc/user/project/integrations/kubernetes.md @@ -40,7 +40,7 @@ the `ca.crt` contents as the `Custom CA bundle`. ## Deployment variables The Kubernetes service exposes following -[deployment variables](../ci/variables/README.md#deployment-variables) in the +[deployment variables](../../../ci/variables/README.md#deployment-variables) in the GitLab CI build environment: - `KUBE_URL` - equal to the API URL @@ -55,7 +55,7 @@ Added in GitLab 8.15. You must be the project owner or have `master` permissions to use terminals. Support is currently limited to the first container in the first pod of your environment. -When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) +When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals) support to your environments. This is based on the `exec` functionality found in Docker and Kubernetes, so you get a new shell session within your existing containers. To use this integration, you should deploy to Kubernetes using diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index 67cb88104c1..9a09e6d37ea 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -17,7 +17,7 @@ in it. All you have to do is configure it. Read more in the ## Automated Configuration If Mattermost is installed on the same server as GitLab, the configuration process can be -done for you by GitLab. +done for you by GitLab. Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. @@ -29,7 +29,7 @@ commands in Mattermost and then enable the service in GitLab. ### Step 1. Enable custom slash commands in Mattermost This step is only required when using a source install, omnibus installs will be -preconfigured with the right settings. +preconfigured with the right settings. The first thing to do in Mattermost is to enable custom slash commands from the administrator console. @@ -41,8 +41,8 @@ the administrator console. --- -1. Click **Custom integrations** and set **Enable Custom Slash Commands**, - **Enable custom integrations to override usernames**, and **Override +1. Click **Custom integrations** and set **Enable Custom Slash Commands**, + **Enable custom integrations to override usernames**, and **Override custom integrations to override profile picture icons** to true ![Mattermost console](img/mattermost_console_integrations.png) @@ -149,15 +149,14 @@ trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> ## Permissions -The permissions to run the [available commands](#available-commands) derive from -the [permissions you have on the project](../user/permissions.md#project). +The permissions to run the [available commands](#available-slash-commands) derive from +the [permissions you have on the project](../../permissions.md#project). ## Further reading - [Mattermost slash commands documentation][mmslashdocs] - [Omnibus GitLab Mattermost][omnimmdocs] - [omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ [mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html -[ciyaml]: ../ci/yaml/README.md +[ciyaml]: ../../../ci/yaml/README.md diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index d9ff573d185..6efc2a168fa 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -6,7 +6,7 @@ Slack commands give users an extra interface to perform common operations from the chat environment. This allows one to, for example, create an issue as soon as the idea was discussed in chat. For all available commands try the help subcommand, for example: `/gitlab help`, -all review the [full list of commands](../integration/chat_commands.md). +all review the [full list of commands](../../../integration/chat_commands.md). ## Prerequisites diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png Binary files differnew file mode 100644 index 00000000000..a936d8e5dbd --- /dev/null +++ b/doc/user/project/pages/img/pages_create_project.png diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png Binary files differnew file mode 100644 index 00000000000..3f615d3757d --- /dev/null +++ b/doc/user/project/pages/img/pages_create_user_page.png diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png Binary files differnew file mode 100644 index 00000000000..8d34f3b7f38 --- /dev/null +++ b/doc/user/project/pages/img/pages_dns_details.png diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png Binary files differnew file mode 100644 index 00000000000..2bc7cee07a6 --- /dev/null +++ b/doc/user/project/pages/img/pages_multiple_domains.png diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png Binary files differnew file mode 100644 index 00000000000..c3640133bb2 --- /dev/null +++ b/doc/user/project/pages/img/pages_new_domain_button.png diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png Binary files differnew file mode 100644 index 00000000000..adbfb654877 --- /dev/null +++ b/doc/user/project/pages/img/pages_remove.png diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png Binary files differnew file mode 100644 index 00000000000..06d85ab1971 --- /dev/null +++ b/doc/user/project/pages/img/pages_upload_cert.png diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md new file mode 100644 index 00000000000..b814e3fccb2 --- /dev/null +++ b/doc/user/project/pages/index.md @@ -0,0 +1,435 @@ +# GitLab Pages + +> **Notes:** +> - This feature was [introduced][ee-80] in GitLab EE 8.3. +> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. +> - This document is about the user guide. To learn how to enable GitLab Pages +> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). + +With GitLab Pages you can host for free your static websites on GitLab. +Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can +deploy static pages for your individual projects, your user or your group. + +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific +information, if you are using GitLab.com to host your website. + +## Getting started with GitLab Pages + +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + +In general there are two types of pages one might create: + +- Pages per user (`username.example.io`) or per group (`groupname.example.io`) +- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) + +In GitLab, usernames and groupnames are unique and we often refer to them +as namespaces. There can be only one namespace in a GitLab instance. Below you +can see the connection between the type of GitLab Pages, what the project name +that is created on GitLab looks like and the website URL it will be ultimately +be served on. + +| Type of GitLab Pages | The name of the project created in GitLab | Website URL | +| -------------------- | ------------ | ----------- | +| User pages | `username.example.io` | `http(s)://username.example.io` | +| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | +| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | +| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| + +> **Warning:** +> There are some known [limitations](#limitations) regarding namespaces served +> under the general domain name and HTTPS. Make sure to read that section. + +### GitLab Pages requirements + +In brief, this is what you need to upload your website in GitLab Pages: + +1. Find out the general domain name that is used for GitLab Pages + (ask your administrator). This is very important, so you should first make + sure you get that right. +1. Create a project +1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory + of your repository with a specific job named [`pages`][pages] +1. Set up a GitLab Runner to build your website + +> **Note:** +> If [shared runners](../ci/runners/README.md) are enabled by your GitLab +> administrator, you should be able to use them instead of bringing your own. + +### User or group Pages + +For user and group pages, the name of the project should be specific to the +username or groupname and the general domain name that is used for GitLab Pages. +Head over your GitLab instance that supports GitLab Pages and create a +repository named `username.example.io`, where `username` is your username on +GitLab. If the first part of the project name doesn't match exactly your +username, it won’t work, so make sure to get it right. + +To create a group page, the steps are the same like when creating a website for +users. Just make sure that you are creating the project within the group's +namespace. + +![Create a user-based pages project](img/pages_create_user_page.png) + +--- + +After you push some static content to your repository and GitLab Runner uploads +the artifacts to GitLab CI, you will be able to access your website under +`http(s)://username.example.io`. Keep reading to find out how. + +>**Note:** +If your username/groupname contains a dot, for example `foo.bar`, you will not +be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). + +### Project Pages + +GitLab Pages for projects can be created by both user and group accounts. +The steps to create a project page for a user or a group are identical: + +1. Create a new project +1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory + of your repository with a specific job named [`pages`][pages]. +1. Set up a GitLab Runner to build your website + +A user's project will be served under `http(s)://username.example.io/projectname` +whereas a group's project under `http(s)://groupname.example.io/projectname`. + +### Explore the contents of `.gitlab-ci.yml` + +The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that +gives you absolute control over the build process. You can actually watch your +website being built live by following the CI build traces. + +> **Note:** +> Before reading this section, make sure you familiarize yourself with GitLab CI +> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by +> following our [quick start guide](../ci/quick_start/README.md). + +To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the +rules below: + +1. A special job named [`pages`][pages] must be defined +1. Any static content which will be served by GitLab Pages must be placed under + a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +In its simplest form, `.gitlab-ci.yml` looks like: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public +``` + +When the Runner reaches to build the `pages` job, it executes whatever is +defined in the `script` parameter and if the build completes with a non-zero +exit status, it then uploads the `public/` directory to GitLab Pages. + +The `public/` directory should contain all the static content of your website. +Depending on how you plan to publish your website, the steps defined in the +[`script` parameter](../ci/yaml/README.md#script) may differ. + +Be aware that Pages are by default branch/tag agnostic and their deployment +relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the +`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to whatever branch or tag, the Pages will be +overwritten. In the example below, we limit the Pages to be deployed whenever +a commit is pushed only on the `master` branch: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public + only: + - master +``` + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. And since all these parameters were all under a `pages` +job, the contents of the `public` directory will be served by GitLab Pages. + +#### How `.gitlab-ci.yml` looks like when the static content is in your repository + +Supposedly your repository contained the following files: + +``` +├── index.html +├── css +│  └── main.css +└── js + └── main.js +``` + +Then the `.gitlab-ci.yml` example below simply moves all files from the root +directory of the project to the `public/` directory. The `.public` workaround +is so `cp` doesn't also copy `public/` to itself in an infinite loop: + +```yaml +pages: + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +#### How `.gitlab-ci.yml` looks like when using a static generator + +In general, GitLab Pages support any kind of [static site generator][staticgen], +since `.gitlab-ci.yml` can be configured to run any possible command. + +In the root directory of your Git repository, place the source files of your +favorite static generator. Then provide a `.gitlab-ci.yml` file which is +specific to your static generator. + +The example below, uses [Jekyll] to build the static site: + +```yaml +image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 + +pages: # the build job must be named pages + script: + - gem install jekyll # we install jekyll + - jekyll build -d public/ # we tell jekyll to build the site for us + artifacts: + paths: + - public # this is where the site will live and the Runner uploads it in GitLab + only: + - master # this script is only affecting the master branch +``` + +Here, we used the Docker executor and in the first line we specified the base +image against which our builds will run. + +You have to make sure that the generated static files are ultimately placed +under the `public` directory, that's why in the `script` section we run the +`jekyll` command that builds the website and puts all content in the `public/` +directory. Depending on the static generator of your choice, this command will +differ. Search in the documentation of the static generator you will use if +there is an option to explicitly set the output directory. If there is not +such an option, you can always add one more line under `script` to rename the +resulting directory in `public/`. + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. + +--- + +See the [jekyll example project][pages-jekyll] to better understand how this +works. + +For a list of Pages projects, see the [example projects](#example-projects) to +get you started. + +#### How to set up GitLab Pages in a repository where there's also actual code + +Remember that GitLab Pages are by default branch/tag agnostic and their +deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit +the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to a branch that will be used specifically for +your pages. + +That way, you can have your project's code in the `master` branch and use an +orphan branch (let's name it `pages`) that will host your static generator site. + +You can create a new empty branch like this: + +```bash +git checkout --orphan pages +``` + +The first commit made on this new branch will have no parents and it will be +the root of a new history totally disconnected from all the other branches and +commits. Push the source files of your static generator in the `pages` branch. + +Below is a copy of `.gitlab-ci.yml` where the most significant line is the last +one, specifying to execute everything in the `pages` branch: + +``` +image: ruby:2.1 + +pages: + script: + - gem install jekyll + - jekyll build -d public/ + artifacts: + paths: + - public + only: + - pages +``` + +See an example that has different files in the [`master` branch][jekyll-master] +and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which +also includes `.gitlab-ci.yml`. + +[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master +[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages + +## Next steps + +So you have successfully deployed your website, congratulations! Let's check +what more you can do with GitLab Pages. + +### Example projects + +Below is a list of example projects for GitLab Pages with a plain HTML website +or various static site generators. Contributions are very welcome. + +- [Plain HTML](https://gitlab.com/pages/plain-html) +- [Jekyll](https://gitlab.com/pages/jekyll) +- [Hugo](https://gitlab.com/pages/hugo) +- [Middleman](https://gitlab.com/pages/middleman) +- [Hexo](https://gitlab.com/pages/hexo) +- [Brunch](https://gitlab.com/pages/brunch) +- [Metalsmith](https://gitlab.com/pages/metalsmith) +- [Harp](https://gitlab.com/pages/harp) + +Visit the GitLab Pages group for a full list of example projects: +<https://gitlab.com/groups/pages>. + +### Add a custom domain to your Pages website + +If this setting is enabled by your GitLab administrator, you should be able to +see the **New Domain** button when visiting your project's settings through the +gear icon in the top right and then navigating to **Pages**. + +![New domain button](img/pages_new_domain_button.png) + +--- + +You can add multiple domains pointing to your website hosted under GitLab. +Once the domain is added, you can see it listed under the **Domains** section. + +![Pages multiple domains](img/pages_multiple_domains.png) + +--- + +As a last step, you need to configure your DNS and add a CNAME pointing to your +user/group page. Click on the **Details** button of a domain for further +instructions. + +![Pages DNS details](img/pages_dns_details.png) + +--- + +>**Note:** +Currently there is support only for custom domains on per-project basis. That +means that if you add a custom domain (`example.com`) for your user website +(`username.example.io`), a project that is served under `username.example.io/foo`, +will not be accessible under `example.com/foo`. + +### Secure your custom domain website with TLS + +When you add a new custom domain, you also have the chance to add a TLS +certificate. If this setting is enabled by your GitLab administrator, you +should be able to see the option to upload the public certificate and the +private key when adding a new domain. + +![Pages upload cert](img/pages_upload_cert.png) + +### Custom error codes pages + +You can provide your own 403 and 404 error pages by creating the `403.html` and +`404.html` files respectively in the root directory of the `public/` directory +that will be included in the artifacts. Usually this is the root directory of +your project, but that may differ depending on your static generator +configuration. + +If the case of `404.html`, there are different scenarios. For example: + +- If you use project Pages (served under `/projectname/`) and try to access + `/projectname/non/exsiting_file`, GitLab Pages will try to serve first + `/projectname/404.html`, and then `/404.html`. +- If you use user/group Pages (served under `/`) and try to access + `/non/existing_file` GitLab Pages will try to serve `/404.html`. +- If you use a custom domain and try to access `/non/existing_file`, GitLab + Pages will try to serve only `/404.html`. + +### Remove the contents of your pages + +If you ever feel the need to purge your Pages content, you can do so by going +to your project's settings through the gear icon in the top right, and then +navigating to **Pages**. Hit the **Remove pages** button and your Pages website +will be deleted. Simple as that. + +![Remove pages](img/pages_remove.png) + +## GitLab Pages on GitLab.com + +If you are using GitLab.com to host your website, then: + +- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. +- Custom domains and TLS support are enabled. +- Shared runners are enabled by default, provided for free and can be used to + build your website. If you want you can still bring your own Runner. + +The rest of the guide still applies. + +## Limitations + +When using Pages under the general domain of a GitLab instance (`*.example.io`), +you _cannot_ use HTTPS with sub-subdomains. That means that if your +username/groupname contains a dot, for example `foo.bar`, the domain +`https://foo.bar.example.io` will _not_ work. This is a limitation of the +[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you +don't redirect HTTP to HTTPS. + +[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" + +## Redirects in GitLab Pages + +Since you cannot use any custom server configuration files, like `.htaccess` or +any `.conf` file for that matter, if you want to redirect a web page to another +location, you can use the [HTTP meta refresh tag][metarefresh]. + +Some static site generators provide plugins for that functionality so that you +don't have to create and edit HTML files manually. For example, Jekyll has the +[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). + +## Frequently Asked Questions + +### Can I download my generated pages? + +Sure. All you need to do is download the artifacts archive from the build page. + +### Can I use GitLab Pages if my project is private? + +Yes. GitLab Pages don't care whether you set your project's visibility level +to private, internal or public. + +### Do I need to create a user/group website before creating a project website? + +No, you don't. You can create your project first and it will be accessed under +`http(s)://namespace.example.io/projectname`. + +## Known issues + +For a list of known issues, visit GitLab's [public issue tracker]. + +--- + +[jekyll]: http://jekyllrb.com/ +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[gitlab ci]: https://about.gitlab.com/gitlab-ci +[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner +[pages]: ../ci/yaml/README.md#pages +[staticgen]: https://www.staticgen.com/ +[pages-jekyll]: https://gitlab.com/pages/jekyll +[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh +[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages +[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index 675e89e4247..c415d566a7c 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -170,6 +170,5 @@ you commit the changes you will be taken to a new merge request form. ![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png) -![New file button](basicsimages/file_button.png) [ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 -[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md +[issue closing pattern]: ../issues/automatic_issue_closing.md diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index cb1c1a84f8c..be042ddf623 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -14,6 +14,11 @@ > raketask. > - The exports are stored in a temporary [shared directory][tmp] and are deleted > every 24 hours by a specific worker. +> - Group members will get exported as project members, as long as the user has +> master or admin access to the group where the exported project lives. An admin +> in the import side is required to map the users, based on email or username. +> Otherwise, a supplementary comment is left to mention the original author and +> the MRs, notes or issues will be owned by the importer. Existing projects running on any GitLab instance or GitLab.com can be exported with all their related data and be moved into a new GitLab instance. @@ -22,7 +27,7 @@ with all their related data and be moved into a new GitLab instance. | GitLab version | Import/Export version | | -------- | -------- | -| 8.16.2 to current | 0.1.6 | +| 8.17.0 to current | 0.1.6 | | 8.13.0 | 0.1.5 | | 8.12.0 | 0.1.4 | | 8.10.3 | 0.1.3 | diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index 97380bce172..f3c636ed1d5 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -28,7 +28,7 @@ to enable this if not already. When issues/pull requests are being imported, the Bitbucket importer tries to find
the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
-and [**associated their Bitbucket account**][social sign-in]. If the user is not
+and **associated their Bitbucket account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original Bitbucket author is kept.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index 86a016fc6d6..cdacef9832f 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -28,7 +28,7 @@ still be able to import their GitHub repositories with a When issues/pull requests are being imported, the GitHub importer tries to find
the GitHub author/assignee in GitLab's database using the GitHub ID. For this
to work, the GitHub author/assignee should have signed in beforehand in GitLab
-and [**associated their GitHub account**][social sign-in]. If the user is not
+and **associated their GitHub account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original GitHub author is kept.
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index 5f6a718135d..3a6773909d6 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -43,8 +43,8 @@ In `config/gitlab.yml`: ## Storage statistics You can see the total storage used for LFS objects on groups and projects -in the administration area, as well as through the [groups](../api/groups.md) -and [projects APIs](../api/projects.md). +in the administration area, as well as through the [groups](../../api/groups.md) +and [projects APIs](../../api/projects.md). ## Known limitations diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index d033e6b167b..5c14c5db665 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -53,6 +53,13 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings + Scenario: On Project Settings/Pages + Given I visit my project's settings page + And I click the "Pages" tab + Then the active sub nav should be Pages + And no other sub navs should be active + And the active main tab should be Settings + Scenario: On Project Members Given I visit my project's members page Then the active sub nav should be Members diff --git a/features/project/pages.feature b/features/project/pages.feature new file mode 100644 index 00000000000..87d88348d09 --- /dev/null +++ b/features/project/pages.feature @@ -0,0 +1,82 @@ +Feature: Project Pages + Background: + Given I sign in as a user + And I own a project + + Scenario: Pages are disabled + Given pages are disabled + When I visit the Project Pages + Then I should see that GitLab Pages are disabled + + Scenario: I can see the pages usage if not deployed + Given pages are enabled + When I visit the Project Pages + Then I should see the usage of GitLab Pages + + Scenario: I can access the pages if deployed + Given pages are enabled + And pages are deployed + When I visit the Project Pages + Then I should be able to access the Pages + + Scenario: I should message that domains support is disabled + Given pages are enabled + And pages are deployed + And support for external domains is disabled + When I visit the Project Pages + Then I should see that support for domains is disabled + + Scenario: I should see a new domain button + Given pages are enabled + And pages are exposed on external HTTP address + When I visit the Project Pages + And I should be able to add a New Domain + + Scenario: I should be able to add a new domain + Given pages are enabled + And pages are exposed on external HTTP address + When I visit add a new Pages Domain + And I fill the domain + And I click on "Create New Domain" + Then I should see a new domain added + + Scenario: I should be able to add a new domain for project in group namespace + Given I own a project in some group namespace + And pages are enabled + And pages are exposed on external HTTP address + When I visit add a new Pages Domain + And I fill the domain + And I click on "Create New Domain" + Then I should see a new domain added + + Scenario: I should be denied to add the same domain twice + Given pages are enabled + And pages are exposed on external HTTP address + And pages domain is added + When I visit add a new Pages Domain + And I fill the domain + And I click on "Create New Domain" + Then I should see error message that domain already exists + + Scenario: I should message that certificates support is disabled when trying to add a new domain + Given pages are enabled + And pages are exposed on external HTTP address + And pages domain is added + When I visit add a new Pages Domain + Then I should see that support for certificates is disabled + + Scenario: I should be able to add a new domain with certificate + Given pages are enabled + And pages are exposed on external HTTPS address + When I visit add a new Pages Domain + And I fill the domain + And I fill the certificate and key + And I click on "Create New Domain" + Then I should see a new domain added + + Scenario: I can remove the pages if deployed + Given pages are enabled + And pages are deployed + When I visit the Project Pages + And I click Remove Pages + Then The Pages should get removed diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 9f701840f1d..e842d7bec2b 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -35,6 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Deploy Keys') end + step 'I click the "Pages" tab' do + click_link('Pages') + end + step 'the active sub nav should be Members' do ensure_active_sub_nav('Members') end @@ -47,6 +51,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_nav('Deploy Keys') end + step 'the active sub nav should be Pages' do + ensure_active_sub_nav('Pages') + end + # Sub Tabs: Commits step 'I click the "Compare" tab' do diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb new file mode 100644 index 00000000000..c80c6273807 --- /dev/null +++ b/features/steps/project/pages.rb @@ -0,0 +1,139 @@ +class Spinach::Features::ProjectPages < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'pages are enabled' do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + allow(Gitlab.config.pages).to receive(:host).and_return('example.com') + allow(Gitlab.config.pages).to receive(:port).and_return(80) + allow(Gitlab.config.pages).to receive(:https).and_return(false) + end + + step 'pages are disabled' do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + end + + step 'I visit the Project Pages' do + visit namespace_project_pages_path(@project.namespace, @project) + end + + step 'I should see that GitLab Pages are disabled' do + expect(page).to have_content('GitLab Pages are disabled') + end + + step 'I should see the usage of GitLab Pages' do + expect(page).to have_content('Configure pages') + end + + step 'pages are deployed' do + pipeline = @project.ensure_pipeline('HEAD', @project.commit('HEAD').sha) + build = build(:ci_build, + project: @project, + pipeline: pipeline, + ref: 'HEAD', + artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'), + artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') + ) + result = ::Projects::UpdatePagesService.new(@project, build).execute + expect(result[:status]).to eq(:success) + end + + step 'I should be able to access the Pages' do + expect(page).to have_content('Access pages') + end + + step 'I should see that support for domains is disabled' do + expect(page).to have_content('Support for domains and certificates is disabled') + end + + step 'support for external domains is disabled' do + allow(Gitlab.config.pages).to receive(:external_http).and_return(nil) + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) + end + + step 'pages are exposed on external HTTP address' do + allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) + end + + step 'pages are exposed on external HTTPS address' do + allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') + allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443') + end + + step 'I should be able to add a New Domain' do + expect(page).to have_content('New Domain') + end + + step 'I visit add a new Pages Domain' do + visit new_namespace_project_pages_domain_path(@project.namespace, @project) + end + + step 'I fill the domain' do + fill_in 'Domain', with: 'my.test.domain.com' + end + + step 'I click on "Create New Domain"' do + click_button 'Create New Domain' + end + + step 'I should see a new domain added' do + expect(page).to have_content('Domains (1)') + expect(page).to have_content('my.test.domain.com') + end + + step 'pages domain is added' do + @project.pages_domains.create!(domain: 'my.test.domain.com') + end + + step 'I should see error message that domain already exists' do + expect(page).to have_content('Domain has already been taken') + end + + step 'I should see that support for certificates is disabled' do + expect(page).to have_content('Support for custom certificates is disabled') + end + + step 'I fill the certificate and key' do + fill_in 'Certificate (PEM)', with: '-----BEGIN CERTIFICATE----- +MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 +LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ +MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa +SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT +nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w +DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD +VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh +IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ +joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese +5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg +YHi2yesCrOvVXt+lgPTd +-----END CERTIFICATE-----' + + fill_in 'Key (PEM)', with: '-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN +SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t +PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB +kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd +j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ +uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR +5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O +AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K +EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh +Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C +m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH +EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx +63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi +nNp/xedE1YxutQ== +-----END PRIVATE KEY-----' + end + + step 'I click Remove Pages' do + click_link 'Remove pages' + end + + step 'The Pages should get removed' do + expect(@project.pages_deployed?).to be_falsey + end +end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 7a6707a7dfb..dae248b8b7e 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -7,6 +7,12 @@ module SharedProject @project.team << [@user, :master] end + step "I own a project in some group namespace" do + @group = create(:group, name: 'some group') + @project = create(:project, namespace: @group) + @project.team << [@user, :master] + end + step "project exists in some group namespace" do @group = create(:group, name: 'some group') @project = create(:project, :repository, namespace: @group, public_builds: false) diff --git a/lib/api/api.rb b/lib/api/api.rb index 1950d2791ab..eb9792680ff 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,9 +5,11 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + mount ::API::V3::DeployKeys mount ::API::V3::Issues mount ::API::V3::MergeRequests mount ::API::V3::Projects + mount ::API::V3::ProjectSnippets end before { allow_access_with_scope :api } diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 64da7d6b86f..3f5183d46a2 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -1,5 +1,4 @@ module API - # Projects API class DeployKeys < Grape::API before { authenticate! } @@ -16,107 +15,102 @@ module API resource :projects do before { authorize_admin_project } - # Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0 - # Use "projects/:id/deploy_keys/..." instead. - # - %w(keys deploy_keys).each do |path| - desc "Get a specific project's deploy keys" do - success Entities::SSHKey - end - get ":id/#{path}" do - present user_project.deploy_keys, with: Entities::SSHKey - end + desc "Get a specific project's deploy keys" do + success Entities::SSHKey + end + get ":id/deploy_keys" do + present user_project.deploy_keys, with: Entities::SSHKey + end - desc 'Get single deploy key' do - success Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - get ":id/#{path}/:key_id" do - key = user_project.deploy_keys.find params[:key_id] + desc 'Get single deploy key' do + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + get ":id/deploy_keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + present key, with: Entities::SSHKey + end + + desc 'Add new deploy key to currently authenticated user' do + success Entities::SSHKey + end + params do + requires :key, type: String, desc: 'The new deploy key' + requires :title, type: String, desc: 'The name of the deploy key' + end + post ":id/deploy_keys" do + params[:key].strip! + + # Check for an existing key joined to this project + key = user_project.deploy_keys.find_by(key: params[:key]) + if key present key, with: Entities::SSHKey + break end - desc 'Add new deploy key to currently authenticated user' do - success Entities::SSHKey - end - params do - requires :key, type: String, desc: 'The new deploy key' - requires :title, type: String, desc: 'The name of the deploy key' + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by(key: params[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey + break end - post ":id/#{path}" do - params[:key].strip! - # Check for an existing key joined to this project - key = user_project.deploy_keys.find_by(key: params[:key]) - if key - present key, with: Entities::SSHKey - break - end - - # Check for available deploy keys in other projects - key = current_user.accessible_deploy_keys.find_by(key: params[:key]) - if key - user_project.deploy_keys << key - present key, with: Entities::SSHKey - break - end - - # Create a new deploy key - key = DeployKey.new(declared_params(include_missing: false)) - if key.valid? && user_project.deploy_keys << key - present key, with: Entities::SSHKey - else - render_validation_error!(key) - end + # Create a new deploy key + key = DeployKey.new(declared_params(include_missing: false)) + if key.valid? && user_project.deploy_keys << key + present key, with: Entities::SSHKey + else + render_validation_error!(key) end + end - desc 'Enable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - post ":id/#{path}/:key_id/enable" do - key = ::Projects::EnableDeployKeyService.new(user_project, - current_user, declared_params).execute + desc 'Enable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + post ":id/deploy_keys/:key_id/enable" do + key = ::Projects::EnableDeployKeyService.new(user_project, + current_user, declared_params).execute - if key - present key, with: Entities::SSHKey - else - not_found!('Deploy Key') - end + if key + present key, with: Entities::SSHKey + else + not_found!('Deploy Key') end + end - desc 'Disable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/#{path}/:key_id/disable" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - key.destroy + desc 'Disable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/deploy_keys/:key_id/disable" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + key.destroy - present key.deploy_key, with: Entities::SSHKey - end + present key.deploy_key, with: Entities::SSHKey + end - desc 'Delete deploy key for a project' do - success Key - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/#{path}/:key_id" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - if key - key.destroy - else - not_found!('Deploy Key') - end + desc 'Delete deploy key for a project' do + success Key + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/deploy_keys/:key_id" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + if key + key.destroy + else + not_found!('Deploy Key') end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b1ead48caf7..5d7b8e021bb 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -213,9 +213,6 @@ module API expose :author, using: Entities::UserBasic expose :updated_at, :created_at - # TODO (rspeicher): Deprecated; remove in 9.0 - expose(:expires_at) { |snippet| nil } - expose :web_url do |snippet, options| Gitlab::UrlBuilder.build(snippet) end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index a1d1c1432d3..747ceb4e3e0 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -57,6 +57,7 @@ module API requires :shared_runners_text, type: String, desc: 'Shared runners text ' end optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' given metrics_enabled: ->(val) { val } do @@ -116,7 +117,7 @@ module API :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay, + :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb new file mode 100644 index 00000000000..5bbb167755c --- /dev/null +++ b/lib/api/v3/deploy_keys.rb @@ -0,0 +1,122 @@ +module API + module V3 + class DeployKeys < Grape::API + before { authenticate! } + + get "deploy_keys" do + authenticated_as_admin! + + keys = DeployKey.all + present keys, with: ::API::Entities::SSHKey + end + + params do + requires :id, type: String, desc: 'The ID of the project' + end + resource :projects do + before { authorize_admin_project } + + %w(keys deploy_keys).each do |path| + desc "Get a specific project's deploy keys" do + success ::API::Entities::SSHKey + end + get ":id/#{path}" do + present user_project.deploy_keys, with: ::API::Entities::SSHKey + end + + desc 'Get single deploy key' do + success ::API::Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + get ":id/#{path}/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + present key, with: ::API::Entities::SSHKey + end + + desc 'Add new deploy key to currently authenticated user' do + success ::API::Entities::SSHKey + end + params do + requires :key, type: String, desc: 'The new deploy key' + requires :title, type: String, desc: 'The name of the deploy key' + end + post ":id/#{path}" do + params[:key].strip! + + # Check for an existing key joined to this project + key = user_project.deploy_keys.find_by(key: params[:key]) + if key + present key, with: ::API::Entities::SSHKey + break + end + + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by(key: params[:key]) + if key + user_project.deploy_keys << key + present key, with: ::API::Entities::SSHKey + break + end + + # Create a new deploy key + key = DeployKey.new(declared_params(include_missing: false)) + if key.valid? && user_project.deploy_keys << key + present key, with: ::API::Entities::SSHKey + else + render_validation_error!(key) + end + end + + desc 'Enable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success ::API::Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + post ":id/#{path}/:key_id/enable" do + key = ::Projects::EnableDeployKeyService.new(user_project, + current_user, declared_params).execute + + if key + present key, with: ::API::Entities::SSHKey + else + not_found!('Deploy Key') + end + end + + desc 'Disable a deploy key for a project' do + detail 'This feature was added in GitLab 8.11' + success ::API::Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/#{path}/:key_id/disable" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + key.destroy + + present key.deploy_key, with: ::API::Entities::SSHKey + end + + desc 'Delete deploy key for a project' do + success Key + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + end + delete ":id/#{path}/:key_id" do + key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + if key + key.destroy + else + not_found!('Deploy Key') + end + end + end + end + end + end +end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb new file mode 100644 index 00000000000..3cc0dc968a8 --- /dev/null +++ b/lib/api/v3/entities.rb @@ -0,0 +1,16 @@ +module API + module V3 + module Entities + class ProjectSnippet < Grape::Entity + expose :id, :title, :file_name + expose :author, using: ::API::Entities::UserBasic + expose :updated_at, :created_at + expose(:expires_at) { |snippet| nil } + + expose :web_url do |snippet, options| + Gitlab::UrlBuilder.build(snippet) + end + end + end + end +end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index be3ecc29449..081d45165e8 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -50,7 +50,7 @@ module API resource :issues do desc "Get currently authenticated user's issues" do - success Entities::Issue + success ::API::Entities::Issue end params do optional :state, type: String, values: %w[opened closed all], default: 'all', @@ -60,7 +60,7 @@ module API get do issues = find_issues(scope: 'authored') - present paginate(issues), with: Entities::Issue, current_user: current_user + present paginate(issues), with: ::API::Entities::Issue, current_user: current_user end end @@ -69,7 +69,7 @@ module API end resource :groups do desc 'Get a list of group issues' do - success Entities::Issue + success ::API::Entities::Issue end params do optional :state, type: String, values: %w[opened closed all], default: 'opened', @@ -81,7 +81,7 @@ module API issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true) - present paginate(issues), with: Entities::Issue, current_user: current_user + present paginate(issues), with: ::API::Entities::Issue, current_user: current_user end end @@ -93,7 +93,7 @@ module API desc 'Get a list of project issues' do detail 'iid filter is deprecated have been removed on V4' - success Entities::Issue + success ::API::Entities::Issue end params do optional :state, type: String, values: %w[opened closed all], default: 'all', @@ -106,22 +106,22 @@ module API issues = find_issues(project_id: project.id) - present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project + present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project end desc 'Get a single project issue' do - success Entities::Issue + success ::API::Entities::Issue end params do requires :issue_id, type: Integer, desc: 'The ID of a project issue' end get ":id/issues/:issue_id" do issue = find_project_issue(params[:issue_id]) - present issue, with: Entities::Issue, current_user: current_user, project: user_project + present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project end desc 'Create a new project issue' do - success Entities::Issue + success ::API::Entities::Issue end params do requires :title, type: String, desc: 'The title of an issue' @@ -153,14 +153,14 @@ module API end if issue.valid? - present issue, with: Entities::Issue, current_user: current_user, project: user_project + present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project else render_validation_error!(issue) end end desc 'Update an existing issue' do - success Entities::Issue + success ::API::Entities::Issue end params do requires :issue_id, type: Integer, desc: 'The ID of a project issue' @@ -186,14 +186,14 @@ module API declared_params(include_missing: false)).execute(issue) if issue.valid? - present issue, with: Entities::Issue, current_user: current_user, project: user_project + present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project else render_validation_error!(issue) end end desc 'Move an existing issue' do - success Entities::Issue + success ::API::Entities::Issue end params do requires :issue_id, type: Integer, desc: 'The ID of a project issue' @@ -208,7 +208,7 @@ module API begin issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) - present issue, with: Entities::Issue, current_user: current_user, project: user_project + present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project rescue ::Issues::MoveService::MoveError => error render_api_error!(error.message, 400) end diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 1af70cf58cc..129f9d850e9 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -39,7 +39,7 @@ module API desc 'List merge requests' do detail 'iid filter is deprecated have been removed on V4' - success Entities::MergeRequest + success ::API::Entities::MergeRequest end params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', @@ -66,11 +66,11 @@ module API end merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) - present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project end desc 'Create a merge request' do - success Entities::MergeRequest + success ::API::Entities::MergeRequest end params do requires :title, type: String, desc: 'The title of the merge request' @@ -89,7 +89,7 @@ module API merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute if merge_request.valid? - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project else handle_merge_request_errors! merge_request.errors end @@ -114,34 +114,34 @@ module API if status == :deprecated detail DEPRECATION_MESSAGE end - success Entities::MergeRequest + success ::API::Entities::MergeRequest end get path do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project end desc 'Get the commits of a merge request' do - success Entities::RepoCommit + success ::API::Entities::RepoCommit end get "#{path}/commits" do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.commits, with: Entities::RepoCommit + present merge_request.commits, with: ::API::Entities::RepoCommit end desc 'Show the merge request changes' do - success Entities::MergeRequestChanges + success ::API::Entities::MergeRequestChanges end get "#{path}/changes" do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request, with: Entities::MergeRequestChanges, current_user: current_user + present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user end desc 'Update a merge request' do - success Entities::MergeRequest + success ::API::Entities::MergeRequest end params do optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' @@ -162,14 +162,14 @@ module API merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) if merge_request.valid? - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project else handle_merge_request_errors! merge_request.errors end end desc 'Merge a merge request' do - success Entities::MergeRequest + success ::API::Entities::MergeRequest end params do optional :merge_commit_message, type: String, desc: 'Custom merge commit message' @@ -209,11 +209,11 @@ module API .execute(merge_request) end - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project end desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do - success Entities::MergeRequest + success ::API::Entities::MergeRequest end post "#{path}/cancel_merge_when_build_succeeds" do merge_request = find_project_merge_request(params[:merge_request_id]) @@ -227,19 +227,19 @@ module API desc 'Get the comments of a merge request' do detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' - success Entities::MRNote + success ::API::Entities::MRNote end params do use :pagination end get "#{path}/comments" do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present paginate(merge_request.notes.fresh), with: Entities::MRNote + present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote end desc 'Post a comment to a merge request' do detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' - success Entities::MRNote + success ::API::Entities::MRNote end params do requires :note, type: String, desc: 'The text of the comment' @@ -256,14 +256,14 @@ module API note = ::Notes::CreateService.new(user_project, current_user, opts).execute if note.save - present note, with: Entities::MRNote + present note, with: ::API::Entities::MRNote else render_api_error!("Failed to save note #{note.errors.messages}", 400) end end desc 'List issues that will be closed on merge' do - success Entities::MRNote + success ::API::Entities::MRNote end params do use :pagination diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb new file mode 100644 index 00000000000..9f95d4395fa --- /dev/null +++ b/lib/api/v3/project_snippets.rb @@ -0,0 +1,135 @@ +module API + module V3 + class ProjectSnippets < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + + def snippets_for_current_user + finder_params = { filter: :by_project, project: user_project } + SnippetsFinder.new.execute(current_user, finder_params) + end + end + + desc 'Get all project snippets' do + success ::API::V3::Entities::ProjectSnippet + end + params do + use :pagination + end + get ":id/snippets" do + present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet + end + + desc 'Get a single project snippet' do + success ::API::V3::Entities::ProjectSnippet + end + params do + requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' + end + get ":id/snippets/:snippet_id" do + snippet = snippets_for_current_user.find(params[:snippet_id]) + present snippet, with: ::API::V3::Entities::ProjectSnippet + end + + desc 'Create a new project snippet' do + success ::API::V3::Entities::ProjectSnippet + end + params do + requires :title, type: String, desc: 'The title of the snippet' + requires :file_name, type: String, desc: 'The file name of the snippet' + requires :code, type: String, desc: 'The content of the snippet' + requires :visibility_level, type: Integer, + values: [Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC], + desc: 'The visibility level of the snippet' + end + post ":id/snippets" do + authorize! :create_project_snippet, user_project + snippet_params = declared_params.merge(request: request, api: true) + snippet_params[:content] = snippet_params.delete(:code) + + snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute + + if snippet.persisted? + present snippet, with: ::API::V3::Entities::ProjectSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Update an existing project snippet' do + success ::API::V3::Entities::ProjectSnippet + end + params do + requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' + optional :title, type: String, desc: 'The title of the snippet' + optional :file_name, type: String, desc: 'The file name of the snippet' + optional :code, type: String, desc: 'The content of the snippet' + optional :visibility_level, type: Integer, + values: [Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC], + desc: 'The visibility level of the snippet' + at_least_one_of :title, :file_name, :code, :visibility_level + end + put ":id/snippets/:snippet_id" do + snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id)) + not_found!('Snippet') unless snippet + + authorize! :update_project_snippet, snippet + + snippet_params = declared_params(include_missing: false) + snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present? + + UpdateSnippetService.new(user_project, current_user, snippet, + snippet_params).execute + + if snippet.persisted? + present snippet, with: ::API::V3::Entities::ProjectSnippet + else + render_validation_error!(snippet) + end + end + + desc 'Delete a project snippet' + params do + requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' + end + delete ":id/snippets/:snippet_id" do + snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) + not_found!('Snippet') unless snippet + + authorize! :admin_project_snippet, snippet + snippet.destroy + end + + desc 'Get a raw project snippet' + params do + requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' + end + get ":id/snippets/:snippet_id/raw" do + snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) + not_found!('Snippet') unless snippet + + env['api.format'] = :txt + content_type 'text/plain' + present snippet.content + end + end + end + end +end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index bac7d485a22..6796da83f07 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -74,32 +74,32 @@ module API def present_projects(projects, options = {}) options = options.reverse_merge( - with: Entities::Project, + with: ::API::Entities::Project, current_user: current_user, simple: params[:simple], ) projects = filter_projects(projects) projects = projects.with_statistics if options[:statistics] - options[:with] = Entities::BasicProjectDetails if options[:simple] + options[:with] = ::API::Entities::BasicProjectDetails if options[:simple] present paginate(projects), options end end desc 'Get a list of visible projects for authenticated user' do - success Entities::BasicProjectDetails + success ::API::Entities::BasicProjectDetails end params do use :collection_params end get '/visible' do - entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails present_projects ProjectsFinder.new.execute(current_user), with: entity end desc 'Get a projects list for authenticated user' do - success Entities::BasicProjectDetails + success ::API::Entities::BasicProjectDetails end params do use :collection_params @@ -108,11 +108,11 @@ module API authenticate! present_projects current_user.authorized_projects, - with: Entities::ProjectWithAccess + with: ::API::Entities::ProjectWithAccess end desc 'Get an owned projects list for authenticated user' do - success Entities::BasicProjectDetails + success ::API::Entities::BasicProjectDetails end params do use :collection_params @@ -122,12 +122,12 @@ module API authenticate! present_projects current_user.owned_projects, - with: Entities::ProjectWithAccess, + with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics] end desc 'Gets starred project for the authenticated user' do - success Entities::BasicProjectDetails + success ::API::Entities::BasicProjectDetails end params do use :collection_params @@ -139,7 +139,7 @@ module API end desc 'Get all projects for admin user' do - success Entities::BasicProjectDetails + success ::API::Entities::BasicProjectDetails end params do use :collection_params @@ -148,11 +148,11 @@ module API get '/all' do authenticated_as_admin! - present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics] + present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics] end desc 'Search for projects the current user has access to' do - success Entities::Project + success ::API::Entities::Project end params do requires :query, type: String, desc: 'The project name to be searched' @@ -164,11 +164,11 @@ module API projects = search_service.objects('projects', params[:page]) projects = projects.reorder(params[:order_by] => params[:sort]) - present paginate(projects), with: Entities::Project + present paginate(projects), with: ::API::Entities::Project end desc 'Create new project' do - success Entities::Project + success ::API::Entities::Project end params do requires :name, type: String, desc: 'The name of the project' @@ -181,7 +181,7 @@ module API project = ::Projects::CreateService.new(current_user, attrs).execute if project.saved? - present project, with: Entities::Project, + present project, with: ::API::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, project) else if project.errors[:limit_reached].present? @@ -192,7 +192,7 @@ module API end desc 'Create new project for a specified user. Only available to admin users.' do - success Entities::Project + success ::API::Entities::Project end params do requires :name, type: String, desc: 'The name of the project' @@ -210,7 +210,7 @@ module API project = ::Projects::CreateService.new(user, attrs).execute if project.saved? - present project, with: Entities::Project, + present project, with: ::API::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, project) else render_validation_error!(project) @@ -223,26 +223,26 @@ module API end resource :projects, requirements: { id: /[^\/]+/ } do desc 'Get a single project' do - success Entities::ProjectWithAccess + success ::API::Entities::ProjectWithAccess end get ":id" do - entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails present user_project, with: entity, current_user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project) end desc 'Get events for a single project' do - success Entities::Event + success ::API::Entities::Event end params do use :pagination end get ":id/events" do - present paginate(user_project.events.recent), with: Entities::Event + present paginate(user_project.events.recent), with: ::API::Entities::Event end desc 'Fork new project for the current user or provided namespace.' do - success Entities::Project + success ::API::Entities::Project end params do optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' @@ -268,13 +268,13 @@ module API if forked_project.errors.any? conflict!(forked_project.errors.messages) else - present forked_project, with: Entities::Project, + present forked_project, with: ::API::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, forked_project) end end desc 'Update an existing project' do - success Entities::Project + success ::API::Entities::Project end params do optional :name, type: String, desc: 'The name of the project' @@ -298,7 +298,7 @@ module API result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute if result[:status] == :success - present user_project, with: Entities::Project, + present user_project, with: ::API::Entities::Project, user_can_admin_project: can?(current_user, :admin_project, user_project) else render_validation_error!(user_project) @@ -306,29 +306,29 @@ module API end desc 'Archive a project' do - success Entities::Project + success ::API::Entities::Project end post ':id/archive' do authorize!(:archive_project, user_project) user_project.archive! - present user_project, with: Entities::Project + present user_project, with: ::API::Entities::Project end desc 'Unarchive a project' do - success Entities::Project + success ::API::Entities::Project end post ':id/unarchive' do authorize!(:archive_project, user_project) user_project.unarchive! - present user_project, with: Entities::Project + present user_project, with: ::API::Entities::Project end desc 'Star a project' do - success Entities::Project + success ::API::Entities::Project end post ':id/star' do if current_user.starred?(user_project) @@ -337,19 +337,19 @@ module API current_user.toggle_star(user_project) user_project.reload - present user_project, with: Entities::Project + present user_project, with: ::API::Entities::Project end end desc 'Unstar a project' do - success Entities::Project + success ::API::Entities::Project end delete ':id/star' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload - present user_project, with: Entities::Project + present user_project, with: ::API::Entities::Project else not_modified! end @@ -390,7 +390,7 @@ module API end desc 'Share the project with a group' do - success Entities::ProjectGroupLink + success ::API::Entities::ProjectGroupLink end params do requires :group_id, type: Integer, desc: 'The ID of a group' @@ -412,7 +412,7 @@ module API link = user_project.project_group_links.new(declared_params(include_missing: false)) if link.save - present link, with: Entities::ProjectGroupLink + present link, with: ::API::Entities::ProjectGroupLink else render_api_error!(link.errors.full_messages.first, 409) end @@ -440,7 +440,7 @@ module API end desc 'Get the users list of a project' do - success Entities::UserBasic + success ::API::Entities::UserBasic end params do optional :search, type: String, desc: 'Return list of users matching the search criteria' @@ -450,7 +450,7 @@ module API users = user_project.team.users users = users.search(params[:search]) if params[:search].present? - present paginate(users), with: Entities::UserBasic + present paginate(users), with: ::API::Entities::UserBasic end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index cefbfdce3bb..f099c0651ac 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,6 +1,6 @@ module Backup class Manager - ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry] + ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry] FOLDERS_TO_BACKUP = %w[repositories db] FILE_NAME_SUFFIX = '_gitlab_backup.tar' diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb new file mode 100644 index 00000000000..215ded93bfe --- /dev/null +++ b/lib/backup/pages.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Pages < Files + def initialize + super('pages', Gitlab.config.pages.path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index ede97cc0504..a4ec8f0ff2f 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -33,11 +33,8 @@ module Gitlab entry :cache, Entry::Cache, description: 'Configure caching between build jobs.' - entry :coverage, Entry::Coverage, - description: 'Coverage configuration for this pipeline.' - helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache, :coverage, :jobs + :variables, :stages, :types, :cache, :jobs def compose!(_deps = nil) super(self) do diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 55b8f888d53..dc2537d36aa 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -35,6 +35,20 @@ module Gitlab order end + def self.nulls_first_order(field, direction = 'ASC') + order = "#{field} #{direction}" + + if Gitlab::Database.postgresql? + order << ' NULLS FIRST' + else + # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL + # columns. In the (default) ascending order, `0` comes first. + order.prepend("#{field} IS NULL, ") if direction == 'DESC' + end + + order + end + def self.random Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()" end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index a40c44eb1bc..b64db5d01ae 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -35,6 +35,8 @@ module Gitlab handler.execute end + private + def build_mail Mail::Message.new(@raw) rescue Encoding::UndefinedConversionError, @@ -54,7 +56,24 @@ module Gitlab end def key_from_additional_headers(mail) - Array(mail.references).find do |mail_id| + references = ensure_references_array(mail.references) + + find_key_from_references(references) + end + + def ensure_references_array(references) + case references + when Array + references + when String + # Handle emails from clients which append with commas, + # example clients are Microsoft exchange and iOS app + Gitlab::IncomingEmail.scan_fallback_references(references) + end + end + + def find_key_from_references(references) + references.find do |mail_id| key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id) break key if key end diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index a09577ae48d..8b8e48aac76 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -32,6 +32,10 @@ module Gitlab @user.id end + def include?(old_author_id) + map.keys.include?(old_author_id) && map[old_author_id] != default_user_id + end + private def missing_keys_tracking_hash diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 2fbf437ec26..b79be62245b 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -5,8 +5,9 @@ module Gitlab attr_reader :full_path - def initialize(project:, shared:) + def initialize(project:, current_user:, shared:) @project = project + @current_user = current_user @shared = shared @full_path = File.join(@shared.export_path, ImportExport.project_filename) end @@ -24,7 +25,29 @@ module Gitlab private def project_json_tree - @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree) + project_json['project_members'] += group_members_json + + project_json.to_json + end + + def project_json + @project_json ||= @project.as_json(reader.project_tree) + end + + def reader + @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared) + end + + def group_members_json + group_members.as_json(reader.group_members_tree).each do |group_member| + group_member['source_type'] = 'Project' # Make group members project members of the future import + end + end + + def group_members + return [] unless @current_user.can?(:admin_group, @project.group) + + MembersFinder.new(@project.project_members, @project.group).execute(@current_user) end end end diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb index 5021a1a14ce..a1e7159fe42 100644 --- a/lib/gitlab/import_export/reader.rb +++ b/lib/gitlab/import_export/reader.rb @@ -21,6 +21,10 @@ module Gitlab false end + def group_members_tree + @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user)) + end + private # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 0319d7707a8..fae792237d9 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -89,7 +89,7 @@ module Gitlab end def has_author?(old_author_id) - admin_user? && @members_mapper.map.keys.include?(old_author_id) + admin_user? && @members_mapper.include?(old_author_id) end def missing_author_note(updated_at, author_name) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index b91012d6405..c9122a23568 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -4,8 +4,6 @@ module Gitlab WILDCARD_PLACEHOLDER = '%{key}'.freeze class << self - FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze - def enabled? config.enabled && config.address end @@ -37,10 +35,14 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX) - return unless match + message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/ - match[1] + mail_id[message_id_regexp, 1] + end + + def scan_fallback_references(references) + # It's looking for each <...> + references.scan(/(?!<)[^<>]+(?=>)/) end def config diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb new file mode 100644 index 00000000000..fb215f27cbd --- /dev/null +++ b/lib/gitlab/pages_transfer.rb @@ -0,0 +1,7 @@ +module Gitlab + class PagesTransfer < ProjectTransfer + def root_dir + Gitlab.config.pages.path + end + end +end diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb new file mode 100644 index 00000000000..1bba0b78e2f --- /dev/null +++ b/lib/gitlab/project_transfer.rb @@ -0,0 +1,35 @@ +module Gitlab + class ProjectTransfer + def move_project(project_path, namespace_path_was, namespace_path) + new_namespace_folder = File.join(root_dir, namespace_path) + FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder) + from = File.join(root_dir, namespace_path_was, project_path) + to = File.join(root_dir, namespace_path, project_path) + move(from, to, "") + end + + def rename_project(path_was, path, namespace_path) + base_dir = File.join(root_dir, namespace_path) + move(path_was, path, base_dir) + end + + def rename_namespace(path_was, path) + move(path_was, path) + end + + def root_dir + raise NotImplementedError + end + + private + + def move(path_was, path, base_dir = nil) + base_dir = root_dir unless base_dir + from = File.join(base_dir, path_was) + to = File.join(base_dir, path) + FileUtils.mv(from, to) + rescue Errno::ENOENT + false + end + end +end diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb index 70e7f25d518..4bc76ea033f 100644 --- a/lib/gitlab/recaptcha.rb +++ b/lib/gitlab/recaptcha.rb @@ -10,5 +10,9 @@ module Gitlab true end end + + def self.enabled? + current_application_settings.recaptcha_enabled + end end end diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb new file mode 100644 index 00000000000..89985d90c10 --- /dev/null +++ b/lib/gitlab/route_map.rb @@ -0,0 +1,52 @@ +module Gitlab + class RouteMap + class FormatError < StandardError; end + + def initialize(data) + begin + entries = YAML.safe_load(data) + rescue + raise FormatError, 'Route map needs to be valid YAML' + end + + raise FormatError, 'Route map needs to be an array' unless entries.is_a?(Array) + + @map = entries.map { |entry| parse_entry(entry) } + end + + def public_path_for_source_path(path) + mapping = @map.find { |mapping| path =~ mapping[:source] } + return unless mapping + + path.sub(mapping[:source], mapping[:public]) + end + + private + + def parse_entry(entry) + raise FormatError, 'Route map entry needs to be a hash' unless entry.is_a?(Hash) + raise FormatError, 'Route map entry requires a source key' unless entry.has_key?('source') + raise FormatError, 'Route map entry requires a public key' unless entry.has_key?('public') + + source_regexp = entry['source'] + public_path = entry['public'] + + unless source_regexp.start_with?('/') && source_regexp.end_with?('/') + raise FormatError, 'Route map entry source needs to start and end in a slash (/)' + end + + source_regexp = source_regexp[1...-1].gsub('\/', '/') + + begin + source_regexp = Regexp.new("^#{source_regexp}$") + rescue RegexpError => e + raise FormatError, "Route map entry source needs to be a valid regular expression: #{e}" + end + + { + source: source_regexp, + public: public_path + } + end + end +end diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb index be8fcc7b2d2..81701831a6a 100644 --- a/lib/gitlab/uploads_transfer.rb +++ b/lib/gitlab/uploads_transfer.rb @@ -1,33 +1,5 @@ module Gitlab - class UploadsTransfer - def move_project(project_path, namespace_path_was, namespace_path) - new_namespace_folder = File.join(root_dir, namespace_path) - FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder) - from = File.join(root_dir, namespace_path_was, project_path) - to = File.join(root_dir, namespace_path, project_path) - move(from, to, "") - end - - def rename_project(path_was, path, namespace_path) - base_dir = File.join(root_dir, namespace_path) - move(path_was, path, base_dir) - end - - def rename_namespace(path_was, path) - move(path_was, path) - end - - private - - def move(path_was, path, base_dir = nil) - base_dir = root_dir unless base_dir - from = File.join(base_dir, path_was) - to = File.join(base_dir, path) - FileUtils.mv(from, to) - rescue Errno::ENOENT - false - end - + class UploadsTransfer < ProjectTransfer def root_dir File.join(Rails.root, "public", "uploads") end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index c7953af29dd..a4e966e4016 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -13,7 +13,19 @@ module Gitlab scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } - scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only } + scope :public_to_user, -> (user) do + if user + if user.admin? + all + elsif !user.external? + public_and_internal_only + else + public_only + end + else + public_only + end + end end PRIVATE = 0 unless const_defined?(:PRIVATE) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 31b00ff128a..5fd7f0f98bd 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -42,6 +42,11 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" +gitlab_pages_enabled=false +gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd) +gitlab_pages_pid_path="$pid_path/gitlab-pages.pid" +gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090" +gitlab_pages_log="$app_root/log/gitlab-pages.log" shell_path="/bin/bash" # Read configuration variable file if it is present @@ -89,13 +94,20 @@ check_pids(){ mpid=0 fi fi + if [ "$gitlab_pages_enabled" = true ]; then + if [ -f "$gitlab_pages_pid_path" ]; then + gppid=$(cat "$gitlab_pages_pid_path") + else + gppid=0 + fi + fi } ## Called when we have started the two processes and are waiting for their pid files. wait_for_pids(){ # We are sleeping a bit here mostly because sidekiq is slow at writing its pid i=0; - while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do + while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; } || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; }; do sleep 0.1; i=$((i+1)) if [ $((i%10)) = 0 ]; then @@ -144,7 +156,15 @@ check_status(){ mail_room_status="-1" fi fi - if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then + if [ "$gitlab_pages_enabled" = true ]; then + if [ $gppid -ne 0 ]; then + kill -0 "$gppid" 2>/dev/null + gitlab_pages_status="$?" + else + gitlab_pages_status="-1" + fi + fi + if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; }; then gitlab_status=0 else # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html @@ -186,12 +206,19 @@ check_stale_pids(){ exit 1 fi fi + if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then + echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran." + if ! rm "$gitlab_pages_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi } ## If no parts of the service is running, bail out. exit_if_not_running(){ check_stale_pids - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then echo "GitLab is not running." exit fi @@ -213,6 +240,9 @@ start_gitlab() { if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then echo "Starting GitLab MailRoom" fi + if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then + echo "Starting GitLab Pages" + fi # Then check if the service is running. If it is: don't start again. if [ "$web_status" = "0" ]; then @@ -252,6 +282,16 @@ start_gitlab() { fi fi + if [ "$gitlab_pages_enabled" = true ]; then + if [ "$gitlab_pages_status" = "0" ]; then + echo "The GitLab Pages is already running with pid $spid, not restarting" + else + $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \ + $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \ + >> $gitlab_pages_log 2>&1 & + fi + fi + # Wait for the pids to be planted wait_for_pids # Finally check the status to tell wether or not GitLab is running @@ -278,13 +318,17 @@ stop_gitlab() { echo "Shutting down GitLab MailRoom" RAILS_ENV=$RAILS_ENV bin/mail_room stop fi + if [ "$gitlab_pages_status" = "0" ]; then + echo "Shutting down gitlab-pages" + kill -- $(cat $gitlab_pages_pid_path) + fi # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. - while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do + while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; do sleep 1 check_status printf "." - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then printf "\n" break fi @@ -298,6 +342,7 @@ stop_gitlab() { if [ "$mail_room_enabled" = true ]; then rm "$mail_room_pid_path" 2>/dev/null fi + rm -f "$gitlab_pages_pid_path" print_status } @@ -305,7 +350,7 @@ stop_gitlab() { ## Prints the status of GitLab and its components. print_status() { check_status - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then echo "GitLab is not running." return fi @@ -331,7 +376,14 @@ print_status() { printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n" fi fi - if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then + if [ "$gitlab_pages_enabled" = true ]; then + if [ "$gitlab_pages_status" = "0" ]; then + echo "The GitLab Pages with pid $mpid is running." + else + printf "The GitLab Pages is \033[31mnot running\033[0m.\n" + fi + fi + if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; }; then printf "GitLab and all its components are \033[32mup and running\033[0m.\n" fi } @@ -362,7 +414,7 @@ reload_gitlab(){ ## Restarts Sidekiq and Unicorn. restart_gitlab(){ check_status - if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then + if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; then stop_gitlab fi start_gitlab diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index cc8617b72ca..e5797d8fe3c 100755..100644 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" +# The GitLab Pages Daemon needs either a separate IP address on which it will +# listen or use different ports than 80 or 443 that will be forwarded to GitLab +# Pages Daemon. +# +# To enable HTTP support for custom domains add the `-listen-http` directive +# in `gitlab_pages_options` below. +# The value of -listen-http must be set to `gitlab.yml > pages > external_http` +# as well. For example: +# +# -listen-http 1.1.1.1:80 +# +# To enable HTTPS support for custom domains add the `-listen-https`, +# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below. +# The value of -listen-https must be set to `gitlab.yml > pages > external_https` +# as well. For example: +# +# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key +# +# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`. +# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature. +gitlab_pages_enabled=false +gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090" +gitlab_pages_log="$app_root/log/gitlab-pages.log" + # mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled. # This is required for the Reply by email feature. # The default is "false" diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages new file mode 100644 index 00000000000..d9746c5c1aa --- /dev/null +++ b/lib/support/nginx/gitlab-pages @@ -0,0 +1,28 @@ +## GitLab +## + +## Pages serving host +server { + listen 0.0.0.0:80; + listen [::]:80 ipv6only=on; + + ## Replace this with something like pages.gitlab.com + server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; + + ## Individual nginx logs for GitLab pages + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_error.log; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # The same address as passed to GitLab Pages: `-listen-proxy` + proxy_pass http://localhost:8090/; + } + + # Define custom error pages + error_page 403 /403.html; + error_page 404 /404.html; +} diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl new file mode 100644 index 00000000000..a1ccf266835 --- /dev/null +++ b/lib/support/nginx/gitlab-pages-ssl @@ -0,0 +1,77 @@ +## GitLab +## + +## Redirects all HTTP traffic to the HTTPS host +server { + ## Either remove "default_server" from the listen line below, + ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab + ## to be served if you visit any address that your server responds to, eg. + ## the ip address of the server (http://x.x.x.x/) + listen 0.0.0.0:80; + listen [::]:80 ipv6only=on; + + ## Replace this with something like pages.gitlab.com + server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_tokens off; ## Don't show the nginx version number, a security best practice + + return 301 https://$http_host$request_uri; + + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_access.log; +} + +## Pages serving host +server { + listen 0.0.0.0:443 ssl; + listen [::]:443 ipv6only=on ssl http2; + + ## Replace this with something like pages.gitlab.com + server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_tokens off; ## Don't show the nginx version number, a security best practice + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ + ssl on; + ssl_certificate /etc/nginx/ssl/gitlab-pages.crt; + ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key; + + # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + + ## See app/controllers/application_controller.rb for headers set + + ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. + ## Replace with your ssl_trusted_certificate. For more info see: + ## - https://medium.com/devops-programming/4445f4862461 + ## - https://www.ruby-forum.com/topic/4419319 + ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx + # ssl_stapling on; + # ssl_stapling_verify on; + # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; + + ## [Optional] Generate a stronger DHE parameter: + ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 + ## + # ssl_dhparam /etc/ssl/certs/dhparam.pem; + + ## Individual nginx logs for GitLab pages + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_error.log; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # The same address as passed to GitLab Pages: `-listen-proxy` + proxy_pass http://localhost:8090/; + } + + # Define custom error pages + error_page 403 /403.html; + error_page 404 /404.html; +} diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index a9f1255e8cf..1650263b98d 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -13,6 +13,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:uploads:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke + Rake::Task["gitlab:backup:pages:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke Rake::Task["gitlab:backup:registry:create"].invoke @@ -56,6 +57,7 @@ namespace :gitlab do Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads') Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') + Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?('pages') Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry') Rake::Task['gitlab:shell:setup'].invoke @@ -159,6 +161,25 @@ namespace :gitlab do end end + namespace :pages do + task create: :environment do + $progress.puts "Dumping pages ... ".color(:blue) + + if ENV["SKIP"] && ENV["SKIP"].include?("pages") + $progress.puts "[SKIPPED]".color(:cyan) + else + Backup::Pages.new.dump + $progress.puts "done".color(:green) + end + end + + task restore: :environment do + $progress.puts "Restoring pages ... ".color(:blue) + Backup::Pages.new.restore + $progress.puts "done".color(:green) + end + end + namespace :lfs do task create: :environment do $progress.puts "Dumping lfs objects ... ".color(:blue) diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake index 9980e0b7984..ea2698da606 100644 --- a/lib/tasks/grape.rake +++ b/lib/tasks/grape.rake @@ -2,7 +2,11 @@ namespace :grape do desc 'Print compiled grape routes' task routes: :environment do API::API.routes.each do |route| - puts route + puts "#{route.options[:method]} #{route.path} - #{route_description(route.options)}" end end + + def route_description(options) + options[:settings][:description][:description] if options[:settings][:description] + end end diff --git a/package.json b/package.json index 9581d966237..c4206eabad1 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "jasmine-core": "^2.5.2", "jasmine-jquery": "^2.1.1", "karma": "^1.3.0", + "karma-coverage": "^1.1.1", "karma-jasmine": "^1.1.0", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", diff --git a/shared/pages/.gitkeep b/shared/pages/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/shared/pages/.gitkeep diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 5f27f336f72..4b89381eb96 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -326,7 +326,7 @@ describe Projects::IssuesController do end describe 'POST #create' do - def post_new_issue(attrs = {}) + def post_new_issue(issue_attrs = {}, additional_params = {}) sign_in(user) project = create(:empty_project, :public) project.team << [user, :developer] @@ -334,8 +334,8 @@ describe Projects::IssuesController do post :create, { namespace_id: project.namespace.to_param, project_id: project.to_param, - issue: { title: 'Title', description: 'Description' }.merge(attrs) - } + issue: { title: 'Title', description: 'Description' }.merge(issue_attrs) + }.merge(additional_params) project.issues.first end @@ -378,24 +378,81 @@ describe Projects::IssuesController do context 'Akismet is enabled' do before do + stub_application_setting(recaptcha_enabled: true) allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) - allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) end - def post_spam_issue - post_new_issue(title: 'Spam Title', description: 'Spam lives here') - end + context 'when an issue is not identified as a spam' do + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false) + end - it 'rejects an issue recognized as spam' do - expect{ post_spam_issue }.not_to change(Issue, :count) - expect(response).to render_template(:new) + it 'does not create an issue' do + expect { post_new_issue(title: '') }.not_to change(Issue, :count) + end end - it 'creates a spam log' do - post_spam_issue - spam_logs = SpamLog.all - expect(spam_logs.count).to eq(1) - expect(spam_logs[0].title).to eq('Spam Title') + context 'when an issue is identified as a spam' do + before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } + + context 'when captcha is not verified' do + def post_spam_issue + post_new_issue(title: 'Spam Title', description: 'Spam lives here') + end + + before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } + + it 'rejects an issue recognized as a spam' do + expect { post_spam_issue }.not_to change(Issue, :count) + end + + it 'creates a spam log' do + post_spam_issue + spam_logs = SpamLog.all + + expect(spam_logs.count).to eq(1) + expect(spam_logs.first.title).to eq('Spam Title') + expect(spam_logs.first.recaptcha_verified).to be_falsey + end + + it 'does not create an issue when it is not valid' do + expect { post_new_issue(title: '') }.not_to change(Issue, :count) + end + + it 'does not create an issue when recaptcha is not enabled' do + stub_application_setting(recaptcha_enabled: false) + + expect { post_spam_issue }.not_to change(Issue, :count) + end + end + + context 'when captcha is verified' do + let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') } + + def post_verified_issue + post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } ) + end + + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true) + end + + it 'accepts an issue after recaptcha is verified' do + expect { post_verified_issue }.to change(Issue, :count) + end + + it 'marks spam log as recaptcha_verified' do + expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true) + end + + it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do + spam_log = create(:spam_log) + + expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }. + not_to change { SpamLog.last.recaptcha_verified } + end + end end end @@ -405,7 +462,7 @@ describe Projects::IssuesController do end it 'creates a user agent detail' do - expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + expect { post_new_issue }.to change(UserAgentDetail, :count).by(1) end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e019541e74f..d18e8c37901 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -22,23 +22,35 @@ describe Projects::MergeRequestsController do render_views let(:fork_project) { create(:forked_project_with_submodules) } + before { fork_project.team << [user, :master] } - before do - fork_project.team << [user, :master] + context 'when rendering HTML response' do + it 'renders new merge request widget template' do + submit_new_merge_request + + expect(response).to be_success + end end - it 'renders it' do - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - } + context 'when rendering JSON response' do + it 'renders JSON including serialized pipelines' do + submit_new_merge_request(format: :json) - expect(response).to be_success + expect(json_response).to have_key('pipelines') + expect(response).to be_ok + end end end + + def submit_new_merge_request(format: :html) + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' }, + format: format + end end shared_examples "loads labels" do |action| @@ -689,15 +701,8 @@ describe Projects::MergeRequestsController do format: :json end - it 'responds with a rendered HTML partial' do - expect(response) - .to render_template('projects/merge_requests/show/_pipelines') - expect(json_response).to have_key 'html' - end - it 'responds with serialized pipelines' do - expect(json_response).to have_key 'pipelines' - expect(json_response['pipelines']).not_to be_empty + expect(json_response).not_to be_empty end end end diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb new file mode 100644 index 00000000000..2362df895a8 --- /dev/null +++ b/spec/controllers/projects/pages_domains_controller_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Projects::PagesDomainsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project + } + end + + before do + sign_in(user) + project.team << [user, :master] + end + + describe 'GET show' do + let!(:pages_domain) { create(:pages_domain, project: project) } + + it "displays the 'show' page" do + get(:show, request_params.merge(id: pages_domain.domain)) + + expect(response).to have_http_status(200) + expect(response).to render_template('show') + end + end + + describe 'GET new' do + it "displays the 'new' page" do + get(:new, request_params) + + expect(response).to have_http_status(200) + expect(response).to render_template('new') + end + end + + describe 'POST create' do + let(:pages_domain_params) do + build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain) + end + + it "creates a new pages domain" do + expect do + post(:create, request_params.merge(pages_domain: pages_domain_params)) + end.to change { PagesDomain.count }.by(1) + + expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project)) + end + end + + describe 'DELETE destroy' do + let!(:pages_domain) { create(:pages_domain, project: project) } + + it "deletes the pages domain" do + expect do + delete(:destroy, request_params.merge(id: pages_domain.domain)) + end.to change { PagesDomain.count }.by(-1) + + expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project)) + end + end +end diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb new file mode 100644 index 00000000000..e9a91cff1b3 --- /dev/null +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -0,0 +1,20 @@ +require('spec_helper') + +describe Projects::Settings::CiCdController do + let(:project) { create(:empty_project, :public, :access_requestable) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET show' do + it 'renders show with 200 status code' do + get :show, namespace_id: project.namespace, project_id: project + + expect(response).to have_http_status(200) + expect(response).to render_template(:show) + end + end +end diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb new file mode 100644 index 00000000000..9fa358f7d62 --- /dev/null +++ b/spec/controllers/projects/variables_controller_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Projects::VariablesController do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.team << [user, :master] + end + + describe 'POST #create' do + context 'variable is valid' do + it 'shows a success flash message' do + post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, + variable: { key: "one", value: "two" } + + expect(flash[:notice]).to include 'Variables were successfully updated.' + expect(response).to redirect_to(namespace_project_settings_ci_cd_path(project.namespace, project)) + end + end + + context 'variable is invalid' do + it 'shows an alert flash message' do + post :create, namespace_id: project.namespace.to_param, project_id: project.to_param, + variable: { key: "..one", value: "two" } + + expect(response).to render_template("projects/variables/show") + end + end + end + + describe 'POST #update' do + let(:variable) { create(:ci_variable) } + + context 'updating a variable with valid characters' do + before do + variable.gl_project_id = project.id + project.variables << variable + end + + it 'shows a success flash message' do + post :update, namespace_id: project.namespace.to_param, project_id: project.to_param, + id: variable.id, variable: { key: variable.key, value: 'two' } + + expect(flash[:notice]).to include 'Variable was successfully updated.' + expect(response).to redirect_to(namespace_project_variables_path(project.namespace, project)) + end + + it 'renders the action #show if the variable key is invalid' do + post :update, namespace_id: project.namespace.to_param, project_id: project.to_param, + id: variable.id, variable: { key: '?', value: variable.value } + + expect(response).to have_http_status(200) + expect(response).to render_template :show + end + end + end +end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 42fbfe89368..8cc216445eb 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -44,7 +44,7 @@ describe RegistrationsController do post(:create, user_params) expect(response).to render_template(:new) - expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' + expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' end it 'redirects to the dashboard when the recaptcha is solved' do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index b7bb9290712..3173aae664c 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe SearchController do let(:user) { create(:user) } - let(:project) { create(:empty_project, :public) } before do sign_in(user) @@ -22,7 +21,7 @@ describe SearchController do before { sign_out(user) } it "doesn't expose comments on issues" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :issues_private) note = create(:note_on_issue, project: project) get :show, project_id: project.id, scope: 'notes', search: note.note @@ -31,17 +30,8 @@ describe SearchController do end end - it "doesn't expose comments on issues" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) - note = create(:note_on_issue, project: project) - - get :show, project_id: project.id, scope: 'notes', search: note.note - - expect(assigns[:search_objects].count).to eq(0) - end - it "doesn't expose comments on merge_requests" do - project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :merge_requests_private) note = create(:note_on_merge_request, project: project) get :show, project_id: project.id, scope: 'notes', search: note.note @@ -50,7 +40,7 @@ describe SearchController do end it "doesn't expose comments on snippets" do - project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :snippets_private) note = create(:note_on_project_snippet, project: project) get :show, project_id: project.id, scope: 'notes', search: note.note diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb new file mode 100644 index 00000000000..6d2e45f41ba --- /dev/null +++ b/spec/factories/pages_domains.rb @@ -0,0 +1,153 @@ +FactoryGirl.define do + factory :pages_domain, class: 'PagesDomain' do + domain 'my.domain.com' + + trait :with_certificate do + certificate '-----BEGIN CERTIFICATE----- +MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 +LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ +MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa +SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT +nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w +DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD +VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh +IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ +joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese +5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg +YHi2yesCrOvVXt+lgPTd +-----END CERTIFICATE-----' + end + + trait :with_key do + key '-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN +SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t +PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB +kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd +j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ +uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR +5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O +AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K +EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh +Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C +m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH +EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx +63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi +nNp/xedE1YxutQ== +-----END PRIVATE KEY-----' + end + + trait :with_missing_chain do + # This certificate is signed with different key + # And misses the CA to build trust chain + certificate '-----BEGIN CERTIFICATE----- +MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0 +IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS +dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAw8RWetIUT0YymSuKvBpClzDv/jQdX0Ch+2iF7f4Lm3lcmoUuXgyhl/WRe5K9 +ONuMHPQlZbeavEbvWb0BsU7geInhsjd/zAu3EP17jfSIXToUdSD20wcSG/yclLdZ +qhb6NCtHTJKFUI8BktoS7kafkdvmeem/UJFzlvcA6VMyGDkS8ZN39a45R1jGmPEl +Yk0g1jW7lSKcBLjU1O/Csv59LyWXqBP6jR1vB8ijlUf1IyK8gOk7NHF13GHl7Z3A +/8zwuEt/pB3yK92o71P+FnSEcJ23zcAalz6H9ajVTzRr/AXttineBNVYnEuPXW+V +Rsboe+bBO/e4pVKXnQ1F3aMT7QIDAQABo28wbTAMBgNVHRMBAf8EAjAAMB0GA1Ud +DgQWBBSFwo3rhc26lD8ZVaBVcUY1NyCOLDALBgNVHQ8EBAMCBeAwEQYJYIZIAYb4 +QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI +hvcNAQEFBQADggEBABppUhunuT7qArM9gZ2gLgcOK8qyZWU8AJulvloaCZDvqGVs +Qom0iEMBrrt5+8bBevNiB49Tz7ok8NFgLzrlEnOw6y6QGjiI/g8sRKEiXl+ZNX8h +s8VN6arqT348OU8h2BixaXDmBF/IqZVApGhR8+B4fkCt0VQmdzVuHGbOQXMWJCpl +WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF +m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+ +VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w= +-----END CERTIFICATE-----' + end + + trait :with_trusted_chain do + # This contains + # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA' + # [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority' + certificate '-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy +MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh +bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh +bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0 +Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6 +ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51 +UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n +c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY +MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz +30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG +BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv +bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB +AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E +T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v +ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p +mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/ +e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps +P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY +dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc +2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG +V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4 +HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX +j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII +0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap +lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf ++AZxAeKCINT+b72x +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk +ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF +eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow +gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD +VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw +AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6 +2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr +ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt +4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq +m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/ +vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT +8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE +IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO +KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO +GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/ +s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g +JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD +AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9 +MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy +bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6 +Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ +zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj +Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY +Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5 +B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx +PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR +pu/xO28QOG8= +-----END CERTIFICATE-----' + end + + trait :with_expired_certificate do + certificate '-----BEGIN CERTIFICATE----- +MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp +cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow +HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF +AAOBjQAwgYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2ge +NR1qlNFaSvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLyS +NT438kdTnY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEA +ATANBgkqhkiG9w0BAQUFAAOBgQBNj+vWvneyW1KkbVK+b/cVmnYPSfbkHrYK6m8X +Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+ +Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9 +ZDXgrA== +-----END CERTIFICATE-----' + end + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 715b2a27b30..c80b09e9b9d 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -56,6 +56,25 @@ FactoryGirl.define do end end + trait(:wiki_enabled) { wiki_access_level ProjectFeature::ENABLED } + trait(:wiki_disabled) { wiki_access_level ProjectFeature::DISABLED } + trait(:wiki_private) { wiki_access_level ProjectFeature::PRIVATE } + trait(:builds_enabled) { builds_access_level ProjectFeature::ENABLED } + trait(:builds_disabled) { builds_access_level ProjectFeature::DISABLED } + trait(:builds_private) { builds_access_level ProjectFeature::PRIVATE } + trait(:snippets_enabled) { snippets_access_level ProjectFeature::ENABLED } + trait(:snippets_disabled) { snippets_access_level ProjectFeature::DISABLED } + trait(:snippets_private) { snippets_access_level ProjectFeature::PRIVATE } + trait(:issues_disabled) { issues_access_level ProjectFeature::DISABLED } + trait(:issues_enabled) { issues_access_level ProjectFeature::ENABLED } + trait(:issues_private) { issues_access_level ProjectFeature::PRIVATE } + trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED } + trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED } + trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE } + trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } + trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } + trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb index 12fc4ec4486..6f1545418eb 100644 --- a/spec/factories/timelogs.rb +++ b/spec/factories/timelogs.rb @@ -4,6 +4,6 @@ FactoryGirl.define do factory :timelog do time_spent 3600 user - association :trackable, factory: :issue + issue end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index a586f8d3184..c0807b8c507 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -211,7 +211,7 @@ describe "Admin::Users", feature: true do fill_in "user_email", with: "bigbang@mail.com" fill_in "user_password", with: "AValidPassword1" fill_in "user_password_confirmation", with: "AValidPassword1" - check "user_admin" + choose "user_access_level_admin" click_button "Save changes" end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb new file mode 100644 index 00000000000..1cf0d11d448 --- /dev/null +++ b/spec/features/boards/modal_filter_spec.rb @@ -0,0 +1,259 @@ +require 'rails_helper' + +describe 'Issue Boards add issue modal filtering', :feature, :js do + include WaitForAjax + include WaitForVueResource + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:planning) { create(:label, project: project, name: 'Planning') } + let!(:list1) { create(:list, board: board, label: planning, position: 0) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:issue1) { create(:issue, project: project) } + + before do + project.team << [user, :master] + + login_as(user) + end + + it 'shows empty state when no results found' do + visit_board + + page.within('.add-issues-modal') do + find('.form-control').native.send_keys('testing empty state') + + wait_for_vue_resource + + expect(page).to have_content('There are no issues to show.') + end + end + + it 'restores filters when closing' do + visit_board + + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Upcoming' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 0) + + click_button 'Cancel' + end + + click_button('Add issues') + + page.within('.add-issues-modal') do + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + context 'author' do + let!(:issue) { create(:issue, project: project, author: user2) } + + before do + project.team << [user2, :developer] + + visit_board + end + + it 'filters by any author' do + page.within('.add-issues-modal') do + click_button 'Author' + + wait_for_ajax + + click_link 'Any Author' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by selected user' do + page.within('.add-issues-modal') do + click_button 'Author' + + wait_for_ajax + + click_link user2.name + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'assignee' do + let!(:issue) { create(:issue, project: project, assignee: user2) } + + before do + project.team << [user2, :developer] + + visit_board + end + + it 'filters by any assignee' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + click_link 'Any Assignee' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by unassigned' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + click_link 'Unassigned' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'filters by selected user' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + page.within '.dropdown-menu-user' do + click_link user2.name + end + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'milestone' do + let(:milestone) { create(:milestone, project: project) } + let!(:issue) { create(:issue, project: project, milestone: milestone) } + + before do + visit_board + end + + it 'filters by any milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Any Milestone' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by upcoming milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Upcoming' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 0) + end + end + + it 'filters by selected milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link milestone.name + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'label' do + let(:label) { create(:label, project: project) } + let!(:issue) { create(:labeled_issue, project: project, labels: [label]) } + + before do + visit_board + end + + it 'filters by any label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link 'Any Label' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by no label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link 'No Label' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'filters by label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link label.title + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + def visit_board + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + click_button('Add issues') + end +end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 511c95b758f..2f49e89b4e4 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -64,10 +64,6 @@ feature 'Environment', :feature do expect(page).to have_link('Re-deploy') end - scenario 'does not show stop button' do - expect(page).not_to have_link('Stop') - end - scenario 'does not show terminal button' do expect(page).not_to have_terminal_button end @@ -116,27 +112,43 @@ feature 'Environment', :feature do end end - context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + context 'when environment is available' do + context 'with stop action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - scenario 'does show stop button' do - expect(page).to have_link('Stop') - end + scenario 'does show stop button' do + expect(page).to have_link('Stop') + end - scenario 'does allow to stop environment' do - click_link('Stop') + scenario 'does allow to stop environment' do + click_link('Stop') - expect(page).to have_content('close_app') - end + expect(page).to have_content('close_app') + end - context 'for reporter' do - let(:role) { :reporter } + context 'for reporter' do + let(:role) { :reporter } - scenario 'does not show stop button' do - expect(page).not_to have_link('Stop') + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end end end + + context 'without stop action' do + scenario 'does allow to stop environment' do + click_link('Stop') + end + end + end + + context 'when environment is stopped' do + given(:environment) { create(:environment, project: project, state: :stopped) } + + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end end end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index c033b693213..78be7d36f47 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') end + + context 'for available environment' do + given(:environment) { create(:environment, project: project, state: :available) } + + scenario 'does not shows stop button' do + expect(page).not_to have_selector('.stop-env-link') + end + end + + context 'for stopped environment' do + given(:environment) { create(:environment, project: project, state: :stopped) } + + scenario 'does not shows stop button' do + expect(page).not_to have_selector('.stop-env-link') + end + end end context 'with deployments' do diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 78a11ffee99..b55078c3bf6 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do include_examples 'project features apply to issuables', MergeRequest context 'archived issuable' do - let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) } + let(:project_archived) { create(:project, :archived, :merge_requests_enabled, group: group) } let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } let(:access_level) { ProjectFeature::ENABLED } let(:user) { user_in_group } diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 5079eb8dd00..c6a88e1b7b0 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -1,39 +1,47 @@ -require 'rails_helper' +require 'spec_helper' describe 'Dropdown label', js: true, feature: true do - include WaitForAjax - - let!(:project) { create(:empty_project) } - let!(:user) { create(:user) } - let!(:bug_label) { create(:label, project: project, title: 'bug') } - let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } - let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } - let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } - let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') } - let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} - let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} + let(:project) { create(:empty_project) } + let(:user) { create(:user) } let(:filtered_search) { find('.filtered-search') } let(:js_dropdown_label) { '#js-dropdown-label' } + let(:filter_dropdown) { find("#{js_dropdown_label} .filter-dropdown") } + + shared_context 'with labels' do + let!(:bug_label) { create(:label, project: project, title: 'bug-label') } + let!(:uppercase_label) { create(:label, project: project, title: 'BUG-LABEL') } + let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } + let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') } + let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()') } + let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') } + end - def send_keys_to_filtered_search(input) - input.split("").each do |i| - filtered_search.send_keys(i) - sleep 3 - wait_for_ajax - sleep 3 - end + def init_label_search + filtered_search.set('label:') + # This ensures the dropdown is shown + expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading') end - def dropdown_label_size - page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size + def search_for_label(label) + init_label_search + filtered_search.send_keys(label) end def click_label(text) - find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click + filter_dropdown.find('.filter-dropdown-item', text: text).click + end + + def dropdown_label_size + filter_dropdown.all('.filter-dropdown-item').size + end + + def clear_search_field + find('.filtered-search-input-container .clear-search').click end before do - project.team << [user, :master] + project.add_master(user) login_as(user) create(:issue, project: project) @@ -42,11 +50,12 @@ describe 'Dropdown label', js: true, feature: true do describe 'keyboard navigation' do it 'selects label' do - send_keys_to_filtered_search('label:') + bug_label = create(:label, project: project, title: 'bug-label') + init_label_search filtered_search.native.send_keys(:down, :down, :enter) - expect(filtered_search.value).to eq("label:~#{special_label.name} ") + expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end end @@ -54,171 +63,177 @@ describe 'Dropdown label', js: true, feature: true do it 'opens when the search bar has label:' do filtered_search.set('label:') - expect(page).to have_css(js_dropdown_label, visible: true) + expect(page).to have_css(js_dropdown_label) end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) end - it 'should show loading indicator when opened' do + it 'shows loading indicator when opened and hides it when loaded' do filtered_search.set('label:') - expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) - end - - it 'should hide loading indicator when loaded' do - send_keys_to_filtered_search('label:') - - expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') + expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading') + expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading') end - it 'should load all the labels when opened' do - send_keys_to_filtered_search('label:') + it 'loads all the labels when opened' do + bug_label = create(:label, project: project, title: 'bug-label') + filtered_search.set('label:') - expect(dropdown_label_size).to be > 0 + expect(filter_dropdown).to have_content(bug_label.title) + expect(dropdown_label_size).to eq(1) end end describe 'filtering' do - before do - filtered_search.set('label') - end - - it 'filters by name' do - send_keys_to_filtered_search(':b') + include_context 'with labels' - expect(dropdown_label_size).to eq(2) + before do + init_label_search end - it 'filters by case insensitive name' do - send_keys_to_filtered_search(':B') + it 'filters by case-insensitive name with or without symbol' do + search_for_label('b') + expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible + expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible expect(dropdown_label_size).to eq(2) - end - it 'filters by name with symbol' do - send_keys_to_filtered_search(':~bu') + clear_search_field + init_label_search - expect(dropdown_label_size).to eq(2) - end - - it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search(':~BU') + search_for_label('~bu') + expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible + expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible expect(dropdown_label_size).to eq(2) end - it 'filters by multiple words' do - send_keys_to_filtered_search(':Hig') + it 'filters by multiple words with or without symbol' do + filtered_search.send_keys('Hig') + expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible expect(dropdown_label_size).to eq(1) - end - it 'filters by multiple words with symbol' do - send_keys_to_filtered_search(':~Hig') + clear_search_field + init_label_search + + filtered_search.send_keys('~Hig') + expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible expect(dropdown_label_size).to eq(1) end - it 'filters by multiple words containing single quotes' do - send_keys_to_filtered_search(':won\'t') + it 'filters by multiple words containing single quotes with or without symbol' do + filtered_search.send_keys('won\'t') + expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible expect(dropdown_label_size).to eq(1) - end - it 'filters by multiple words containing single quotes with symbol' do - send_keys_to_filtered_search(':~won\'t') + clear_search_field + init_label_search + + filtered_search.send_keys('~won\'t') + expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible expect(dropdown_label_size).to eq(1) end - it 'filters by multiple words containing double quotes' do - send_keys_to_filtered_search(':won"t') + it 'filters by multiple words containing double quotes with or without symbol' do + filtered_search.send_keys('won"t') + expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible expect(dropdown_label_size).to eq(1) - end - it 'filters by multiple words containing double quotes with symbol' do - send_keys_to_filtered_search(':~won"t') + clear_search_field + init_label_search + filtered_search.send_keys('~won"t') + + expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible expect(dropdown_label_size).to eq(1) end - it 'filters by special characters' do - send_keys_to_filtered_search(':^+') + it 'filters by special characters with or without symbol' do + filtered_search.send_keys('^+') + expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible expect(dropdown_label_size).to eq(1) - end - it 'filters by special characters with symbol' do - send_keys_to_filtered_search(':~^+') + clear_search_field + init_label_search + filtered_search.send_keys('~^+') + + expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible expect(dropdown_label_size).to eq(1) end end describe 'selecting from dropdown' do + include_context 'with labels' + before do - filtered_search.set('label:') + init_label_search end it 'fills in the label name when the label has not been filled' do click_label(bug_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end it 'fills in the label name when the label is partially filled' do - send_keys_to_filtered_search('bu') + filtered_search.send_keys('bu') click_label(bug_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~#{bug_label.title} ") end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ") end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ") end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ") end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ") end it 'fills in the label name with special characters' do click_label(special_label.title) - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:~#{special_label.title} ") end it 'selects `no label`' do - find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click + find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click - expect(page).to have_css(js_dropdown_label, visible: false) + expect(page).not_to have_css(js_dropdown_label) expect(filtered_search.value).to eq("label:none ") end end @@ -226,44 +241,47 @@ describe 'Dropdown label', js: true, feature: true do describe 'input has existing content' do it 'opens label dropdown with existing search term' do filtered_search.set('searchTerm label:') - expect(page).to have_css(js_dropdown_label, visible: true) + + expect(page).to have_css(js_dropdown_label) end it 'opens label dropdown with existing author' do filtered_search.set('author:@person label:') - expect(page).to have_css(js_dropdown_label, visible: true) + + expect(page).to have_css(js_dropdown_label) end it 'opens label dropdown with existing assignee' do filtered_search.set('assignee:@person label:') - expect(page).to have_css(js_dropdown_label, visible: true) + + expect(page).to have_css(js_dropdown_label) end it 'opens label dropdown with existing label' do filtered_search.set('label:~urgent label:') - expect(page).to have_css(js_dropdown_label, visible: true) + + expect(page).to have_css(js_dropdown_label) end it 'opens label dropdown with existing milestone' do filtered_search.set('milestone:%v2.0 label:') - expect(page).to have_css(js_dropdown_label, visible: true) + + expect(page).to have_css(js_dropdown_label) end end describe 'caching requests' do it 'caches requests after the first load' do - filtered_search.set('label') - send_keys_to_filtered_search(':') - initial_size = dropdown_label_size + create(:label, project: project, title: 'bug-label') + init_label_search - expect(initial_size).to be > 0 + expect(dropdown_label_size).to eq(1) create(:label, project: project) - find('.filtered-search-input-container .clear-search').click - filtered_search.set('label') - send_keys_to_filtered_search(':') + clear_search_field + init_label_search - expect(dropdown_label_size).to eq(initial_size) + expect(dropdown_label_size).to eq(1) end end end diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb new file mode 100644 index 00000000000..4bc9b49f889 --- /dev/null +++ b/spec/features/issues/spam_issues_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +describe 'New issue', feature: true do + include StubENV + + let(:project) { create(:project, :public) } + let(:user) { create(:user)} + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + + current_application_settings.update!( + akismet_enabled: true, + akismet_api_key: 'testkey', + recaptcha_enabled: true, + recaptcha_site_key: 'test site key', + recaptcha_private_key: 'test private key' + ) + + project.team << [user, :master] + login_as(user) + end + + context 'when identified as a spam' do + before do + WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200) + + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'creates an issue after solving reCaptcha' do + fill_in 'issue_title', with: 'issue title' + fill_in 'issue_description', with: 'issue description' + + click_button 'Submit issue' + + # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha + # recaptcha verification is skipped in test environment and it always returns true + expect(page).not_to have_content('issue title') + expect(page).to have_css('.recaptcha') + + click_button 'Submit issue' + + expect(page.find('.issue-details h2.title')).to have_content('issue title') + expect(page.find('.issue-details .description')).to have_content('issue description') + end + end + + context 'when not identified as a spam' do + before do + WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200) + + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'creates an issue' do + fill_in 'issue_title', with: 'issue title' + fill_in 'issue_description', with: 'issue description' + + click_button 'Submit issue' + + expect(page.find('.issue-details h2.title')).to have_content('issue title') + expect(page.find('.issue-details .description')).to have_content('issue description') + end + end +end diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb new file mode 100644 index 00000000000..b08bd36bde9 --- /dev/null +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -0,0 +1,100 @@ +require 'rails_helper' + +feature 'Mini Pipeline Graph', :js, :feature do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + + let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) } + let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + + before do + build.run + + login_as(user) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should display a mini pipeline graph' do + expect(page).to have_selector('.mr-widget-pipeline-graph') + end + + describe 'build list toggle' do + let(:toggle) do + find('.mini-pipeline-graph-dropdown-toggle') + first('.mini-pipeline-graph-dropdown-toggle') + end + + it 'should expand when hovered' do + before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + + toggle.hover + + after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + + expect(before_width).to be < after_width + end + + it 'should show dropdown caret when hovered' do + toggle.hover + + expect(toggle).to have_selector('.fa-caret-down') + end + + it 'should show tooltip when hovered' do + toggle.hover + + expect(toggle.find(:xpath, '..')).to have_selector('.tooltip') + end + end + + describe 'builds list menu' do + let(:toggle) do + find('.mini-pipeline-graph-dropdown-toggle') + first('.mini-pipeline-graph-dropdown-toggle') + end + + before do + toggle.click + wait_for_ajax + end + + it 'should open when toggle is clicked' do + expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu') + end + + it 'should close when toggle is clicked again' do + toggle.click + + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') + end + + it 'should close when clicking somewhere else' do + find('body').click + + expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') + end + + describe 'build list build item' do + let(:build_item) do + find('.mini-pipeline-graph-dropdown-item') + first('.mini-pipeline-graph-dropdown-item') + end + + it 'should visit the build page when clicked' do + build_item.click + find('.build-page') + + expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build)) + end + + it 'should show tooltip when hovered' do + build_item.hover + + expect(build_item.find(:xpath, '..')).to have_selector('.tooltip') + end + end + end +end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 7d1805f5001..fb3a1ae4bd0 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -5,30 +5,53 @@ describe 'Merge request', :feature, :js do let(:project) { create(:project) } let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project) } before do project.team << [user, :master] login_as(user) + end - visit new_namespace_project_merge_request_path( - project.namespace, - project, - merge_request: { - source_project_id: project.id, - target_project_id: project.id, - source_branch: 'feature', - target_branch: 'master' - } - ) + context 'new merge request' do + before do + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'feature', + target_branch: 'master' + } + ) + end + + it 'shows widget status after creating new merge request' do + click_button 'Submit merge request' + + expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically') + + wait_for_ajax + + expect(page).to have_selector('.accept_merge_request') + end end - it 'shows widget status after creating new merge request' do - click_button 'Submit merge request' + context 'view merge request' do + let!(:environment) { create(:environment, project: project) } + let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) } - expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically') + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end - wait_for_ajax + it 'shows environments link' do + wait_for_ajax - expect(page).to have_selector('.accept_merge_request') + page.within('.mr-widget-heading') do + expect(page).to have_content("Deployed to #{environment.name}") + expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url) + end + end end end diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb new file mode 100644 index 00000000000..30e2d587267 --- /dev/null +++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +feature 'Blob shortcuts', feature: true do + include TreeHelper + let(:project) { create(:project, :public, :repository) } + let(:path) { project.repository.ls_files(project.repository.root_ref)[0] } + let(:sha) { project.repository.commit.sha } + + describe 'On a file(blob)', js: true do + def get_absolute_url(path = "") + "http://#{page.server.host}:#{page.server.port}#{path}" + end + + def visit_blob(fragment = nil) + visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment) + end + + describe 'pressing "y"' do + it 'redirects to permalink with commit sha' do + visit_blob + + find('body').native.send_key('y') + + expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))), url: true) + end + + it 'maintains fragment hash when redirecting' do + fragment = "L1" + visit_blob(fragment) + + find('body').native.send_key('y') + + expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)), url: true) + end + end + end +end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 33f1c323af1..268d420c594 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project commit pipelines' do +feature 'project commit pipelines', js: true do given(:project) { create(:project) } background do diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 7baf7913424..7baf7913424 100644 --- a/spec/features/projects/commits/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb diff --git a/spec/features/compare_spec.rb b/spec/features/projects/compare_spec.rb index 43eb4000e58..43eb4000e58 100644 --- a/spec/features/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb new file mode 100644 index 00000000000..11793c0f303 --- /dev/null +++ b/spec/features/projects/pages_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +feature 'Pages', feature: true do + given(:project) { create(:empty_project) } + given(:user) { create(:user) } + given(:role) { :master } + + background do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + + project.team << [user, role] + + login_as(user) + end + + shared_examples 'no pages deployed' do + scenario 'does not see anything to destroy' do + visit namespace_project_pages_path(project.namespace, project) + + expect(page).not_to have_link('Remove pages') + expect(page).not_to have_text('Only the project owner can remove pages') + end + end + + context 'when user is the owner' do + background do + project.namespace.update(owner: user) + end + + context 'when pages deployed' do + background do + allow_any_instance_of(Project).to receive(:pages_deployed?) { true } + end + + scenario 'sees "Remove pages" link' do + visit namespace_project_pages_path(project.namespace, project) + + expect(page).to have_link('Remove pages') + end + end + + it_behaves_like 'no pages deployed' + end + + context 'when the user is not the owner' do + context 'when pages deployed' do + background do + allow_any_instance_of(Project).to receive(:pages_deployed?) { true } + end + + scenario 'sees "Only the project owner can remove pages" text' do + visit namespace_project_pages_path(project.namespace, project) + + expect(page).to have_text('Only the project owner can remove pages') + end + end + + it_behaves_like 'no pages deployed' + end +end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 042a1ccab51..f5adb53a2dc 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Setup Mattermost slash commands', feature: true do - include WaitForAjax - let(:user) { create(:user) } let(:project) { create(:empty_project) } let(:service) { project.create_mattermost_slash_commands_service } @@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do visit edit_namespace_project_service_path(project.namespace, project, service) end - describe 'user visits the mattermost slash command config page', js: true do + describe 'user visits the mattermost slash command config page' do it 'shows a help message' do - wait_for_ajax + expect(page).to have_content("This service allows users to perform common") + end + + it 'shows a token placeholder' do + token_placeholder = find_field('service_token')['placeholder'] - expect(page).to have_content("This service allows GitLab users to perform common") + expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') end it 'shows the token after saving' do @@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do select_element = find('select#mattermost_team_id') selected_option = select_element.find('option[selected]') - expect(select_element['disabled']).to be(true) + expect(select_element['disabled']).to eq('disabled') expect(selected_option).to have_content(team_name.to_s) end @@ -93,7 +95,7 @@ feature 'Setup Mattermost slash commands', feature: true do select_element = find('select#mattermost_team_id') selected_option = select_element.find('option[selected]') - expect(select_element['disabled']).to be(false) + expect(select_element['disabled']).to be(nil) expect(selected_option).to have_content('Select team...') # The 'Select team...' placeholder is item `0`. expect(select_element.all('option').count).to eq(3) @@ -135,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") end + + it 'shows a token placeholder' do + token_placeholder = find_field('service_token')['placeholder'] + + expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') + end end end diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb index 32b32f7ae8e..db903a0c8f0 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Slack slash commands', feature: true do - include WaitForAjax - given(:user) { create(:user) } given(:project) { create(:project) } given(:service) { project.create_slack_slash_commands_service } @@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do background do project.team << [user, :master] login_as(user) - end - - scenario 'user visits the slack slash command config page and shows a help message', js: true do visit edit_namespace_project_service_path(project.namespace, project, service) + end - wait_for_ajax + it 'shows a token placeholder' do + token_placeholder = find_field('service_token')['placeholder'] - expect(page).to have_content('This service allows GitLab users to perform common') + expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') end - scenario 'shows the token after saving' do - visit edit_namespace_project_service_path(project.namespace, project, service) + it 'shows a help message' do + expect(page).to have_content('This service allows users to perform common') + end + it 'shows the token after saving' do fill_in 'service_token', with: 'token' click_on 'Save' @@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do expect(value).to eq('token') end - scenario 'shows the correct trigger url' do - visit edit_namespace_project_service_path(project.namespace, project, service) - + it 'shows the correct trigger url' do value = find_field('url').value expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 034b75c2e51..6815039d5ed 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -12,13 +12,8 @@ feature 'Project settings > Merge Requests', feature: true, js: true do end context 'when Merge Request and Pipelines are initially enabled' do - before do - project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) - end - context 'when Pipelines are initially enabled' do before do - project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) visit edit_project_path(project) end diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb new file mode 100644 index 00000000000..ce5c5f21167 --- /dev/null +++ b/spec/features/projects/view_on_env_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe 'View on environment', js: true do + include WaitForAjax + + let(:branch_name) { 'feature' } + let(:file_path) { 'files/ruby/feature.rb' } + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + + before do + project.add_master(user) + end + + context 'when the branch has a route map' do + let(:route_map) do + <<-MAP.strip_heredoc + - source: /files/(.*)\\..*/ + public: '\\1' + MAP + end + + before do + Files::CreateService.new( + project, + user, + start_branch: branch_name, + target_branch: branch_name, + commit_message: "Add .gitlab/route-map.yml", + file_path: '.gitlab/route-map.yml', + file_content: route_map + ).execute + + # Update the file so that we still have a commit that will have a file on the environment + Files::UpdateService.new( + project, + user, + start_branch: branch_name, + target_branch: branch_name, + commit_message: "Update feature", + file_path: file_path, + file_content: "# Noop" + ).execute + end + + context 'and an active deployment' do + let(:sha) { project.commit(branch_name).sha } + let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') } + let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) } + + context 'when visiting the diff of a merge request for the branch' do + let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) } + + before do + login_as(user) + + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + + wait_for_ajax + end + + it 'has a "View on env" button' do + within '.diffs' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + end + + context 'when visiting a comparison for the branch' do + before do + login_as(user) + + visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting a comparison for the commit' do + before do + login_as(user) + + visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting a blob on the branch' do + before do + login_as(user) + + visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path)) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting a blob on the commit' do + before do + login_as(user) + + visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path)) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting the commit' do + before do + login_as(user) + + visit namespace_project_commit_path(project.namespace, project, sha) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + end + end +end diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb index b4f5f6b3fc5..20219f3cc9a 100644 --- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe 'Projects > Wiki > User views wiki in project page', feature: true do let(:user) { create(:user) } - let(:project) { create(:empty_project) } before do project.team << [user, :master] @@ -10,12 +9,11 @@ describe 'Projects > Wiki > User views wiki in project page', feature: true do end context 'when repository is disabled for project' do - before do - project.project_feature.update!( - repository_access_level: ProjectFeature::DISABLED, - merge_requests_access_level: ProjectFeature::DISABLED, - builds_access_level: ProjectFeature::DISABLED - ) + let(:project) do + create(:empty_project, + :repository_disabled, + :merge_requests_disabled, + :builds_disabled) end context 'when wiki homepage contains a link' do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 92d5a2fbc48..24af062d763 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -96,6 +96,20 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for(:external) } end + describe "GET /:project_path/settings/ci_cd" do + subject { namespace_project_settings_ci_cd_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } + end + describe "GET /:project_path/blob" do let(:commit) { project.repository.commit } subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index b616e488487..c511dcfa18e 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -92,8 +92,22 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for(:reporter).of(project) } it { is_expected.to be_allowed_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } it { is_expected.to be_denied_for(:external) } + end + + describe "GET /:project_path/settings/ci_cd" do + subject { namespace_project_settings_ci_cd_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } end describe "GET /:project_path/blob" do diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index ded85e837f4..d8cc012c27e 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -96,6 +96,20 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for(:external) } end + describe "GET /:project_path/settings/ci_cd" do + subject { namespace_project_settings_ci_cd_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } + end + describe "GET /:project_path/pipelines" do subject { namespace_project_pipelines_path(project.namespace, project) } diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 72354834c5a..4a7511589d6 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -7,7 +7,7 @@ describe 'Triggers' do before do @project = FactoryGirl.create :empty_project @project.team << [user, :master] - visit namespace_project_triggers_path(@project.namespace, @project) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) end context 'create a trigger' do diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index ff30ffd7820..9a4bc027004 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -10,7 +10,7 @@ describe 'Project variables', js: true do project.team << [user, :master] project.variables << variable - visit namespace_project_variables_path(project.namespace, project) + visit namespace_project_settings_ci_cd_path(project.namespace, project) end it 'shows list of variables' do diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb new file mode 100644 index 00000000000..0c063f6d5ee --- /dev/null +++ b/spec/finders/environments_finder_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe EnvironmentsFinder do + describe '#execute' do + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + let(:environment) { create(:environment, project: project) } + + before do + project.add_master(user) + end + + context 'tagged deployment' do + before do + create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) + end + + it 'returns environment when with_tags is set' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute) + .to contain_exactly(environment) + end + + it 'does not return environment when no with_tags is set' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute) + .to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute) + .to be_empty + end + end + + context 'branch deployment' do + before do + create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + end + + it 'returns environment when ref is set' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute) + .to contain_exactly(environment) + end + + it 'does not environment when ref is different' do + expect(described_class.new(project, user, ref: 'feature', commit: project.commit).execute) + .to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute) + .to be_empty + end + + it 'returns environment when commit constraint is not set' do + expect(described_class.new(project, user, ref: 'master').execute) + .to contain_exactly(environment) + end + end + + context 'commit deployment' do + before do + create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + end + + it 'returns environment' do + expect(described_class.new(project, user, commit: project.commit).execute) + .to contain_exactly(environment) + end + end + + context 'recently updated' do + context 'when last deployment to environment is the most recent one' do + before do + create(:deployment, environment: environment, ref: 'feature') + end + + it 'finds recently updated environment' do + expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute) + .to contain_exactly(environment) + end + end + + context 'when last deployment to environment is not the most recent' do + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: environment, ref: 'master') + end + + it 'does not find environment' do + expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute) + .to be_empty + end + end + + context 'when there are two environments that deploy to the same branch' do + let(:second_environment) { create(:environment, project: project) } + + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: second_environment, ref: 'feature') + end + + it 'finds both environments' do + expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute) + .to contain_exactly(environment, second_environment) + end + end + end + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 97737d7ddc7..12ab1d6dde8 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -224,7 +224,7 @@ describe IssuesFinder do let(:scope) { nil } it "doesn't return team-only issues to non team members" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :issues_private) issue = create(:issue, project: project) expect(issues).not_to include(issue) diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index bac653ea451..f8b05d4e9bc 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -9,8 +9,6 @@ describe NotesFinder do end describe '#execute' do - it 'finds notes on snippets when project is public and user isnt a member' - it 'finds notes on merge requests' do create(:note_on_merge_request, project: project) @@ -45,9 +43,11 @@ describe NotesFinder do context 'on restricted projects' do let(:project) do - create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, - snippets_access_level: ProjectFeature::PRIVATE, - merge_requests_access_level: ProjectFeature::PRIVATE) + create(:empty_project, + :public, + :issues_private, + :snippets_private, + :merge_requests_private) end it 'publicly excludes notes on merge requests' do diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml new file mode 100644 index 00000000000..6823db0cfc8 --- /dev/null +++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml @@ -0,0 +1,42 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: reply@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +In-Reply-To: <issue_1@localhost> +References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>,<exchange@microsoft.com> +Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +I could not disagree more. I am obviously biased but adventure time is the +greatest show ever created. Everyone should watch it. + +- Jake out + + +On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta +<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote: +> +> +> +> eviltrout posted in 'Adventure Time Sux' on Discourse Meta: +> +> --- +> hey guys everyone knows adventure time sucks! +> +> --- +> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 +> +> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). +> diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz Binary files differnew file mode 100644 index 00000000000..d0e89378b3e --- /dev/null +++ b/spec/fixtures/pages.tar.gz diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip Binary files differnew file mode 100644 index 00000000000..9558fcd4b94 --- /dev/null +++ b/spec/fixtures/pages.zip diff --git a/spec/fixtures/pages.zip.meta b/spec/fixtures/pages.zip.meta Binary files differnew file mode 100644 index 00000000000..1e6198a15f0 --- /dev/null +++ b/spec/fixtures/pages.zip.meta diff --git a/spec/fixtures/pages_empty.tar.gz b/spec/fixtures/pages_empty.tar.gz Binary files differnew file mode 100644 index 00000000000..5c2afa1a8f6 --- /dev/null +++ b/spec/fixtures/pages_empty.tar.gz diff --git a/spec/fixtures/pages_empty.zip b/spec/fixtures/pages_empty.zip Binary files differnew file mode 100644 index 00000000000..db3f0334c12 --- /dev/null +++ b/spec/fixtures/pages_empty.zip diff --git a/spec/fixtures/pages_empty.zip.meta b/spec/fixtures/pages_empty.zip.meta Binary files differnew file mode 100644 index 00000000000..d0b93b3b9c0 --- /dev/null +++ b/spec/fixtures/pages_empty.zip.meta diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index 727c25ff529..a2c008790f9 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -26,4 +26,23 @@ describe CommitsHelper do not_to include('onmouseover="alert(1)"') end end + + describe '#view_on_environment_button' do + let(:project) { create(:empty_project) } + let(:environment) { create(:environment, external_url: 'http://example.com') } + let(:path) { 'source/file.html' } + let(:sha) { RepoHelpers.sample_commit.id } + + before do + allow(environment).to receive(:external_url_for).with(path, sha).and_return('http://example.com/file.html') + end + + it 'returns a link tag linking to the file in the environment' do + html = helper.view_on_environment_button(sha, path, environment) + node = Nokogiri::HTML.parse(html).at_css('a') + + expect(node[:title]).to eq('View on example.com') + expect(node[:href]).to eq('http://example.com/file.html') + end + end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 550b4a66a6a..8d268dbcf36 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -63,9 +63,11 @@ describe MergeRequestsHelper do end end - describe 'mr_widget_refresh_url' do - let(:project) { create(:empty_project) } - let(:merge_request) { create(:merge_request, source_project: project) } + describe '#mr_widget_refresh_url' do + let(:guest) { create(:user) } + let(:project) { create(:project, :public) } + let(:project_fork) { Projects::ForkService.new(project, guest).execute } + let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) } it 'returns correct url for MR' do expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh" @@ -74,7 +76,7 @@ describe MergeRequestsHelper do end it 'returns empty string for nil' do - expect(mr_widget_refresh_url(nil)).to end_with('') + expect(mr_widget_refresh_url(nil)).to eq('') end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 8d1570aa6f3..aca0bb1d794 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -203,7 +203,6 @@ describe ProjectsHelper do context "when project moves from public to private" do before do - project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED) project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6 new file mode 100644 index 00000000000..5f0f26a013c --- /dev/null +++ b/spec/javascripts/commit/pipelines/mock_data.js.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-unused-vars */ +const pipeline = { + id: 73, + user: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', + }, + path: '/root/review-app/pipelines/73', + details: { + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73', + }, + duration: null, + finished_at: '2017-01-25T00:00:17.130Z', + stages: [{ + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73#build', + }, + path: '/root/review-app/pipelines/73#build', + dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build', + }], + artifacts: [], + manual_actions: [ + { + name: 'stop_review', + path: '/root/review-app/builds/1463/play', + }, + { + name: 'name', + path: '/root/review-app/builds/1490/play', + }, + ], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, + }, + ref: + { + name: 'master', + path: '/root/review-app/tree/master', + tag: false, + branch: true, + }, + commit: { + id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4', + short_id: 'fbd79f04', + title: 'Update .gitlab-ci.yml', + author_name: 'Administrator', + author_email: 'admin@example.com', + created_at: '2017-01-16T12:13:57.000-05:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + message: 'Update .gitlab-ci.yml', + author: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + }, + retry_path: '/root/review-app/pipelines/73/retry', + created_at: '2017-01-16T17:13:59.800Z', + updated_at: '2017-01-25T00:00:17.132Z', +}; diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 new file mode 100644 index 00000000000..c2b61632827 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 @@ -0,0 +1,106 @@ +/* global pipeline, Vue */ + +require('vue-resource'); +require('flash'); +require('~/commit/pipelines/pipelines_store'); +require('~/commit/pipelines/pipelines_service'); +require('~/commit/pipelines/pipelines_table'); +require('~vue_shared/vue_resource_interceptor'); +require('./mock_data'); + +describe('Pipelines table in Commits and Merge requests', () => { + preloadFixtures('pipelines_table'); + + beforeEach(() => { + loadFixtures('pipelines_table'); + }); + + describe('successfull request', () => { + describe('without pipelines', () => { + const pipelinesEmptyResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesEmptyResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesEmptyResponse, + ); + }); + + it('should render the empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 1); + }); + }); + + describe('with pipelines', () => { + const pipelinesResponse = (request, next) => { + next(request.respondWith(JSON.stringify([pipeline]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesResponse, + ); + }); + + it('should render a table with the received pipelines', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const pipelinesErrorResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesErrorResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesErrorResponse, + ); + }); + + it('should render empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 new file mode 100644 index 00000000000..a5a16544ffb --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -0,0 +1,30 @@ +require('~commit/pipelines/pipelines_store'); + +describe('Store', () => { + const store = gl.commits.pipelines.PipelinesStore; + + beforeEach(() => { + store.create(); + }); + + it('should start with a blank state', () => { + expect(store.state.pipelines.length).toBe(0); + }); + + it('should store an array of pipelines', () => { + const pipelines = [ + { + id: '1', + name: 'pipeline', + }, + { + id: '2', + name: 'pipeline_2', + }, + ]; + + store.store(pipelines); + + expect(store.state.pipelines.length).toBe(pipelines.length); + }); +}); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 9858f346c83..d87cc0996c9 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -119,7 +119,7 @@ describe('Environment item', () => { }, ], }, - 'stoppable?': true, + 'stop_action?': true, environment_path: 'root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-10T15:55:58.778Z', diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 58f6fb96afb..80e1cbc6f4d 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -50,7 +50,7 @@ const environmentsList = [ }, manual_actions: [], }, - 'stoppable?': true, + 'stop_action?': true, environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', @@ -105,7 +105,7 @@ const environmentsList = [ }, manual_actions: [], }, - 'stoppable?': false, + 'stop_action?': false, environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', @@ -116,7 +116,7 @@ const environmentsList = [ state: 'available', environment_type: 'review', last_deployment: null, - 'stoppable?': true, + 'stop_action?': true, environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', @@ -127,7 +127,7 @@ const environmentsList = [ state: 'available', environment_type: 'review', last_deployment: null, - 'stoppable?': true, + 'stop_action?': true, environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', @@ -143,7 +143,7 @@ const environment = { external_url: 'http://production.', environment_type: null, last_deployment: {}, - 'stoppable?': false, + 'stop_action?': false, environment_path: '/root/review-app/environments/4', stop_path: '/root/review-app/environments/4/stop', created_at: '2016-12-16T11:51:04.690Z', diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 index 10a316f31b4..f4b0d60db34 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -36,5 +36,40 @@ require('~/filtered_search/dropdown_user'); expect(dropdownUser.getSearchInput()).toBe('larry boy'); }); }); + + describe('config droplabAjaxFilter\'s endpoint', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + }); + + it('should return endpoint', () => { + window.gon = { + relative_url_root: '', + }; + const dropdown = new gl.DropdownUser(); + + expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); + + it('should return endpoint when relative_url_root is undefined', () => { + const dropdown = new gl.DropdownUser(); + + expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); + + it('should return endpoint with relative url when available', () => { + window.gon = { + relative_url_root: '/gitlab_directory', + }; + const dropdown = new gl.DropdownUser(); + + expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); + }); + + afterEach(() => { + window.gon = {}; + }); + }); }); })(); diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml new file mode 100644 index 00000000000..fbe4a434f76 --- /dev/null +++ b/spec/javascripts/fixtures/pipelines_table.html.haml @@ -0,0 +1,2 @@ +#commit-pipeline-table-view{ data: { endpoint: "endpoint" } } +.pipeline-svgs{ data: { "commit_icon_svg": "svg"} } diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index fbb06f3948b..61e83d73afb 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -41,6 +41,19 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.handleLocationHash', () => { + beforeEach(() => { + window.history.pushState({}, null, '#definição'); + }); + + it('decodes hash parameter', () => { + spyOn(window.document, 'getElementById').and.callThrough(); + gl.utils.handleLocationHash(); + expect(window.document.getElementById).toHaveBeenCalledWith('definição'); + expect(window.document.getElementById).toHaveBeenCalledWith('user-content-definição'); + }); + }); + describe('gl.utils.getParameterByName', () => { beforeEach(() => { window.history.pushState({}, null, '?scope=all&p=2'); @@ -73,5 +86,37 @@ require('~/lib/utils/common_utils'); expect(normalized[NGINX].nginx).toBe('ok'); }); }); + + describe('gl.utils.isMetaClick', () => { + it('should identify meta click on Windows/Linux', () => { + const e = { + metaKey: false, + ctrlKey: true, + which: 1, + }; + + expect(gl.utils.isMetaClick(e)).toBe(true); + }); + + it('should identify meta click on macOS', () => { + const e = { + metaKey: true, + ctrlKey: false, + which: 1, + }; + + expect(gl.utils.isMetaClick(e)).toBe(true); + }); + + it('should identify as meta click on middle-click or Mouse-wheel click', () => { + const e = { + metaKey: false, + ctrlKey: false, + which: 2, + }; + + expect(gl.utils.isMetaClick(e)).toBe(true); + }); + }); }); })(); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index d20a59df041..92a0f1c05f7 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -61,6 +61,56 @@ require('vendor/jquery.scrollTo'); expect($('#diffs')).toHaveClass('active'); }); }); + describe('#opensInNewTab', function () { + var commitsLink; + var tabUrl; + + beforeEach(function () { + commitsLink = '.commits-tab li a'; + tabUrl = $(commitsLink).attr('href'); + + spyOn($.fn, 'attr').and.returnValue(tabUrl); + }); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual('_blank'); + }); + + this.class.clickTab({ + metaKey: false, + ctrlKey: true, + which: 1, + stopImmediatePropagation: function () {} + }); + }); + it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual('_blank'); + }); + + this.class.clickTab({ + metaKey: true, + ctrlKey: false, + which: 1, + stopImmediatePropagation: function () {} + }); + }); + it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual('_blank'); + }); + + this.class.clickTab({ + metaKey: false, + ctrlKey: false, + which: 2, + stopImmediatePropagation: function () {} + }); + }); + }); describe('#setCurrentAction', function () { beforeEach(function () { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index a6994f6edf4..7cdade01e00 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -31,7 +31,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should call getBuildsList', () => { const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); @@ -41,7 +41,7 @@ require('~/mini_pipeline_graph_dropdown'); it('should make a request to the endpoint provided in the html', () => { const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); - new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); document.querySelector('.js-builds-dropdown-button').click(); expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index e0b52f767e4..bfe3d2df79d 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -9,19 +9,20 @@ require('~/project_select'); require('~/project'); (function() { - window.gon || (window.gon = {}); - - window.gon.api_version = 'v3'; - describe('Project Title', function() { preloadFixtures('static/project_title.html.raw'); loadJSONFixtures('projects.json'); beforeEach(function() { loadFixtures('static/project_title.html.raw'); + + window.gon = {}; + window.gon.api_version = 'v3'; + return this.project = new Project(); }); - return describe('project list', function() { + + describe('project list', function() { var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); @@ -48,5 +49,9 @@ require('~/project'); return expect($('.header-content').hasClass('open')).toBe(false); }); }); + + afterEach(() => { + window.gon = {}; + }); }); }).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index c79e30e9481..9572b52ec1e 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -14,11 +14,6 @@ require('vendor/fuzzaldrin-plus'); userId = 1; - window.gon || (window.gon = {}); - - window.gon.current_user_id = userId; - window.gon.current_username = userName; - dashboardIssuesPath = '/dashboard/issues'; dashboardMRsPath = '/dashboard/merge_requests'; @@ -117,6 +112,16 @@ require('vendor/fuzzaldrin-plus'); widget = new gl.SearchAutocomplete; // Prevent turbolinks from triggering within gl_dropdown spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); + + window.gon = {}; + window.gon.current_user_id = userId; + window.gon.current_username = userName; + + return widget = new gl.SearchAutocomplete; + }); + + afterEach(function() { + window.gon = {}; }); it('should show Dashboard specific dropdown menu', function() { var list; diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6 index bbd914de4ea..84d169a3fb9 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -require('~/vue_common_component/commit'); +require('~/vue_shared/components/committ'); describe('Commit component', () => { let props; diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 new file mode 100644 index 00000000000..e83a1749e82 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 @@ -0,0 +1,89 @@ +/* global pipeline */ + +require('~vue_shared/components/pipelines_table_row'); +require('./mock_data'); + +describe('Pipelines Table Row', () => { + let component; + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + + component = new gl.pipelines.PipelinesTableRowComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipeline, + svgs: {}, + }, + }); + }); + + it('should render a table row', () => { + expect(component.$el).toEqual('TR'); + }); + + describe('status column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td.commit-link a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render status text', () => { + expect( + component.$el.querySelector('td.commit-link a').textContent, + ).toContain(pipeline.details.status.text); + }); + }); + + describe('information column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render pipeline ID', () => { + expect( + component.$el.querySelector('td:nth-child(2) a > span').textContent, + ).toEqual(`#${pipeline.id}`); + }); + + describe('when a user is provided', () => { + it('should render user information', () => { + expect( + component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + ).toEqual(pipeline.user.web_url); + + expect( + component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + ).toEqual(pipeline.user.name); + }); + }); + }); + + describe('commit column', () => { + it('should render link to commit', () => { + expect( + component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'), + ).toEqual(pipeline.commit.commit_path); + }); + }); + + describe('stages column', () => { + it('should render an icon for each stage', () => { + expect( + component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + ).toEqual(pipeline.details.stages.length); + }); + }); + + describe('actions column', () => { + it('should render the provided actions', () => { + expect( + component.$el.querySelectorAll('td:nth-child(6) ul li').length, + ).toEqual(pipeline.details.manual_actions.length); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 new file mode 100644 index 00000000000..adc9ea904cc --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 @@ -0,0 +1,66 @@ +/* global pipeline */ + +require('~vue_shared/components/pipelines_table'); +require('~lib/utils/datetime_utility'); +require('./mock_data'); + +describe('Pipelines Table', () => { + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + describe('table', () => { + let component; + beforeEach(() => { + component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + }); + + it('should render a table', () => { + expect(component.$el).toEqual('TABLE'); + }); + + it('should render table head with correct columns', () => { + expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status'); + expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline'); + expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit'); + expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages'); + expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual(''); + expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual(''); + }); + }); + + describe('without data', () => { + it('should render an empty table', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); + }); + }); + + describe('with data', () => { + it('should render rows', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [pipeline], + svgs: {}, + }, + }); + + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); + }); + }); +}); diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 index 8935c474ee5..e84f0dcfe67 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -1,5 +1,5 @@ require('~/lib/utils/common_utils'); -require('~/vue_pagination/index'); +require('~/vue_shared/components/table_pagination'); describe('Pagination component', () => { let component; diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 49349035b3b..008c15c4de3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -5,27 +5,18 @@ module Ci let(:path) { 'path' } describe '#build_attributes' do - context 'Coverage entry' do + describe 'coverage entry' do subject { described_class.new(config, path).build_attributes(:rspec) } - let(:config_base) { { rspec: { script: "rspec" } } } - let(:config) { YAML.dump(config_base) } - - context 'when config has coverage set at the global scope' do - before do - config_base.update(coverage: '/\(\d+\.\d+\) covered/') - end - - context "and 'rspec' job doesn't have coverage set" do - it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + describe 'code coverage regexp' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + coverage: '/Code coverage: \d+\.\d+/' }) end - context "but 'rspec' job also has coverage set" do - before do - config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' - end - - it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + it 'includes coverage regexp in build attributes' do + expect(subject) + .to include(coverage_regex: 'Code coverage: \d+\.\d+') end end end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index d4f1780b174..432a99dce33 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -10,10 +10,10 @@ describe Gitlab::Ci::Config::Entry::Global do context 'when filtering all the entry/node names' do it 'contains the expected node names' do - node_names = described_class.nodes.keys - expect(node_names).to match_array(%i[before_script image services - after_script variables stages - types cache coverage]) + expect(described_class.nodes.keys) + .to match_array(%i[before_script image services + after_script variables stages + types cache]) end end end @@ -40,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 9 + expect(global.descendants.count).to eq 8 end it 'creates node object using valid class' do @@ -181,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 9 + expect(global.descendants.count).to eq 8 end it 'contains unspecified nodes' do diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 01b2a55b63c..e18a219ef36 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::ContributionsCalendar do end let(:feature_project) do - create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project| + create(:empty_project, :public, :issues_private) do |project| create(:project_member, user: contributor, project: project).project end end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 3031559c613..b142b3a2781 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do end end + describe '.nulls_first_order' do + context 'when using PostgreSQL' do + before { expect(described_class).to receive(:postgresql?).and_return(true) } + + it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'} + it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'} + end + + context 'when using MySQL' do + before { expect(described_class).to receive(:postgresql?).and_return(false) } + + it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'} + it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'} + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 17a4ef25210..b300feaabe1 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -174,6 +174,12 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do it_behaves_like 'an email that contains a mail key', 'References' end + + context 'mail key is in the References header with a comma' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml') } + + it_behaves_like 'an email that contains a mail key', 'References' + end end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 116ab16ae74..a55bd4387e0 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -94,8 +94,6 @@ describe Gitlab::GitAccess, lib: true do context 'when repository is enabled' do it 'give access to download code' do - public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) - expect(subject.allowed?).to be_truthy end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 4a0cdc6887e..1ae293416e4 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -36,8 +36,6 @@ describe Gitlab::GitAccessWiki, lib: true do context 'when wiki feature is enabled' do it 'give access to download wiki code' do - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) - expect(subject.allowed?).to be_truthy end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 72421832ffc..afd78abdc9b 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -202,7 +202,7 @@ describe Gitlab::GithubImport::Importer, lib: true do end end - let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) } + let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") } let(:credentials) { { user: 'joe' } } context 'when importing a GitHub project' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 20241d4d63e..06617f3b007 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -192,6 +192,7 @@ project: - environments - deployments - project_feature +- pages_domains - authorized_users - project_authorizations - route @@ -202,5 +203,6 @@ award_emoji: priorities: - label timelogs: -- trackable +- issue +- merge_request - user diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index f2cb028206f..b9d4e59e770 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -116,5 +116,27 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(members_mapper.map[exported_user_id]).to eq(user2.id) end end + + context 'importing group members' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, namespace: group) } + let(:members_mapper) do + described_class.new( + exported_members: exported_members, user: user, project: project) + end + + before do + group.add_users([user, user2], GroupMember::DEVELOPER) + user.update(email: 'invite@test.com') + end + + it 'maps the importer' do + expect(members_mapper.map[-1]).to eq(user.id) + end + + it 'maps the group member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + end end end 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 40d7d59f03b..0af13ba8e47 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) } + let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } @@ -121,13 +121,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end context 'with group' do - let!(:project) do + let!(:project) do create(:empty_project, - name: 'project', - path: 'project', - builds_access_level: ProjectFeature::DISABLED, - issues_access_level: ProjectFeature::DISABLED, - group: create(:group)) + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) end it 'has group labels' do diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 1d65b24c2c9..3628adefc0c 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeSaver, services: true do describe 'saves the project tree into a json object' do let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:project_tree_saver) { described_class.new(project: project, shared: shared) } + let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:user) { create(:user) } let(:project) { setup_project } @@ -92,7 +92,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end it 'has pipeline builds' do - expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1) + expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1) end it 'has pipeline commits' do @@ -112,13 +112,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end it 'has project and group labels' do - label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']} + label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] } expect(label_types).to match_array(['ProjectLabel', 'GroupLabel']) end it 'has priorities associated to labels' do - priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']} + priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities'] } expect(priorities.flatten).not_to be_empty end @@ -140,6 +140,51 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(project_tree_saver.save).to be true end + + context 'group members' do + let(:user2) { create(:user, email: 'group@member.com') } + let(:member_emails) do + saved_project_json['project_members'].map do |pm| + pm['user']['email'] + end + end + + before do + Group.first.add_developer(user2) + end + + it 'does not export group members if it has no permission' do + Group.first.add_developer(user) + + expect(member_emails).not_to include('group@member.com') + end + + it 'does not export group members as master' do + Group.first.add_master(user) + + expect(member_emails).not_to include('group@member.com') + end + + it 'exports group members as group owner' do + Group.first.add_owner(user) + + expect(member_emails).to include('group@member.com') + end + + context 'as admin' do + let(:user) { create(:admin) } + + it 'exports group members as admin' do + expect(member_emails).to include('group@member.com') + end + + it 'exports group members as project members' do + member_types = saved_project_json['project_members'].map { |pm| pm['source_type'] } + + expect(member_types).to all(eq('Project')) + end + end + end end end @@ -152,6 +197,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do project = create(:project, :public, :repository, + :issues_disabled, + :wiki_enabled, + :builds_private, issues: [issue], snippets: [snippet], releases: [release], @@ -167,10 +215,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do commit_status = create(:commit_status, project: project) ci_pipeline = create(:ci_pipeline, - project: project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch, - statuses: [commit_status]) + project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + statuses: [commit_status]) create(:ci_build, pipeline: ci_pipeline, project: project) create(:milestone, project: project) @@ -185,10 +233,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') - project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE) - project end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index 3ceb1e7e803..48d74b07e27 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -86,6 +86,10 @@ describe Gitlab::ImportExport::Reader, lib: true do expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }]) end + it 'generates the correct hash for group members' do + expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } }) + end + def setup_yaml(hash) allow(YAML).to receive(:load_file).with(test_config).and_return(hash) end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 95b230e4f5c..c5ac702d831 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -350,8 +350,8 @@ LabelPriority: Timelog: - id - time_spent -- trackable_id -- trackable_type +- merge_request_id +- issue_id - user_id - created_at - updated_at diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 7e951e3fcdd..698bd72d0f8 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -90,4 +90,19 @@ describe Gitlab::IncomingEmail, lib: true do expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') end end + + context 'self.scan_fallback_references' do + let(:references) do + '<issue_1@localhost>' + + ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' + + ',<exchange@microsoft.com>' + end + + it 'returns reply key' do + expect(described_class.scan_fallback_references(references)) + .to eq(%w[issue_1@localhost + reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost + exchange@microsoft.com]) + end + end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 92e3624a8d8..9a8096208db 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -163,7 +163,7 @@ describe Gitlab::ProjectSearchResults, lib: true do end it "doesn't list issue notes when access is restricted" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :issues_private) note = create(:note_on_issue, project: project) results = described_class.new(user, project, note.note) @@ -172,7 +172,7 @@ describe Gitlab::ProjectSearchResults, lib: true do end it "doesn't list merge_request notes when access is restricted" do - project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :merge_requests_private) note = create(:note_on_merge_request, project: project) results = described_class.new(user, project, note.note) diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb index 4092f7fb638..e2d6b1b9ab7 100644 --- a/spec/lib/gitlab/uploads_transfer_spec.rb +++ b/spec/lib/gitlab/project_transfer_spec.rb @@ -1,9 +1,10 @@ require 'spec_helper' -describe Gitlab::UploadsTransfer, lib: true do +describe Gitlab::ProjectTransfer, lib: true do before do @root_dir = File.join(Rails.root, "public", "uploads") - @upload_transfer = Gitlab::UploadsTransfer.new + @project_transfer = Gitlab::ProjectTransfer.new + allow(@project_transfer).to receive(:root_dir).and_return(@root_dir) @project_path_was = "test_project_was" @project_path = "test_project" @@ -21,7 +22,7 @@ describe Gitlab::UploadsTransfer, lib: true do describe '#move_project' do it "moves project upload to another namespace" do FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) - @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path) + @project_transfer.move_project(@project_path, @namespace_path_was, @namespace_path) expected_path = File.join(@root_dir, @namespace_path, @project_path) expect(Dir.exist?(expected_path)).to be_truthy @@ -31,7 +32,7 @@ describe Gitlab::UploadsTransfer, lib: true do describe '#rename_project' do it "renames project" do FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was)) - @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path) + @project_transfer.rename_project(@project_path_was, @project_path, @namespace_path) expected_path = File.join(@root_dir, @namespace_path, @project_path) expect(Dir.exist?(expected_path)).to be_truthy @@ -41,7 +42,7 @@ describe Gitlab::UploadsTransfer, lib: true do describe '#rename_namespace' do it "renames namespace" do FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) - @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path) + @project_transfer.rename_namespace(@namespace_path_was, @namespace_path) expected_path = File.join(@root_dir, @namespace_path, @project_path) expect(Dir.exist?(expected_path)).to be_truthy diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb new file mode 100644 index 00000000000..459fa8a63a9 --- /dev/null +++ b/spec/lib/gitlab/route_map_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Gitlab::RouteMap, lib: true do + describe '#initialize' do + context 'when the data is not YAML' do + it 'raises an error' do + expect { described_class.new('"') }. + to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/) + end + end + + context 'when the data is not a YAML array' do + it 'raises an error' do + expect { described_class.new(YAML.dump('foo')) }. + to raise_error(Gitlab::RouteMap::FormatError, /an array/) + end + end + + context 'when an entry is not a hash' do + it 'raises an error' do + expect { described_class.new(YAML.dump(['foo'])) }. + to raise_error(Gitlab::RouteMap::FormatError, /a hash/) + end + end + + context 'when an entry does not have a source key' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /source key/) + end + end + + context 'when an entry does not have a public key' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /public key/) + end + end + + context 'when an entry source does not start and end with a slash' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'source' => 'index.html', 'public' => 'index.html' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /a slash/) + end + end + + context 'when an entry source is not a valid regex' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /regular expression/) + end + end + + context 'when all is good' do + it 'returns a route map' do + route_map = described_class.new(YAML.dump([{ 'source' => '/index\.html/', 'public' => 'index.html' }])) + + expect(route_map.public_path_for_source_path('index.html')).to eq('index.html') + end + end + end + + describe '#public_path_for_source_path' do + subject do + described_class.new(<<-'MAP'.strip_heredoc) + # Blogposts + - source: /source/posts/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb + public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ + + # HTML files + - source: /source/(.+?\.html).*/ # source/index.html.haml + public: '\1' # index.html + + # Other files + - source: /source/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png + public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png + MAP + end + + it 'returns the public path for a provided source path' do + expect(subject.public_path_for_source_path('source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb')).to eq('2017/01/30/around-the-world-in-6-releases/') + + expect(subject.public_path_for_source_path('source/index.html.haml')).to eq('index.html') + + expect(subject.public_path_for_source_path('source/images/blogimages/around-the-world-in-6-releases-cover.png')).to eq('images/blogimages/around-the-world-in-6-releases-cover.png') + + expect(subject.public_path_for_source_path('.gitlab/route-map.yml')).to be_nil + end + end +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 2f4a33a1868..30f8fdf91b2 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -247,7 +247,7 @@ describe Ability, lib: true do end describe '.project_disabled_features_rules' do - let(:project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED) } + let(:project) { create(:empty_project, :wiki_disabled) } subject { described_class.allowed(project.owner, project) } diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index fc4435a2f64..080ff2f3f43 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -77,8 +77,8 @@ describe Deployment, models: true do end end - describe '#stoppable?' do - subject { deployment.stoppable? } + describe '#stop_action?' do + subject { deployment.stop_action? } context 'when no other actions' do let(:deployment) { build(:deployment) } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index eba392044bf..960f29f3805 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -7,8 +7,6 @@ describe Environment, models: true do it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } - it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } - it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } @@ -22,6 +20,20 @@ describe Environment, models: true do it { is_expected.to validate_length_of(:external_url).is_at_most(255) } it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } + describe '.order_by_last_deployed_at' do + let(:project) { create(:project) } + let!(:environment1) { create(:environment, project: project) } + let!(:environment2) { create(:environment, project: project) } + let!(:environment3) { create(:environment, project: project) } + let!(:deployment1) { create(:deployment, environment: environment1) } + let!(:deployment2) { create(:deployment, environment: environment2) } + let!(:deployment3) { create(:deployment, environment: environment1) } + + it 'returns the environments in order of having been last deployed' do + expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1]) + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") @@ -112,8 +124,8 @@ describe Environment, models: true do end end - describe '#stoppable?' do - subject { environment.stoppable? } + describe '#stop_action?' do + subject { environment.stop_action? } context 'when no other actions' do it { is_expected.to be_falsey } @@ -142,17 +154,39 @@ describe Environment, models: true do end end - describe '#stop!' do + describe '#stop_with_action!' do let(:user) { create(:user) } - subject { environment.stop!(user) } + subject { environment.stop_with_action!(user) } before do - expect(environment).to receive(:stoppable?).and_call_original + expect(environment).to receive(:available?).and_call_original end context 'when no other actions' do - it { is_expected.to be_nil } + context 'environment is available' do + before do + environment.update(state: :available) + end + + it do + subject + + expect(environment).to be_stopped + end + end + + context 'environment is already stopped' do + before do + environment.update(state: :stopped) + end + + it do + subject + + expect(environment).to be_stopped + end + end end context 'when matching action is defined' do @@ -301,4 +335,33 @@ describe Environment, models: true do end end end + + describe '#external_url_for' do + let(:source_path) { 'source/file.html' } + let(:sha) { RepoHelpers.sample_commit.id } + + before do + environment.external_url = 'http://example.com' + end + + context 'when the public path is not known' do + before do + allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil) + end + + it 'returns nil' do + expect(environment.external_url_for(source_path, sha)).to be_nil + end + end + + context 'when the public path is known' do + before do + allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return('file.html') + end + + it 'returns the full external URL' do + expect(environment.external_url_for(source_path, sha)).to eq('http://example.com/file.html') + end + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9ca50555191..a4e6eb4e3a6 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -300,4 +300,17 @@ describe Group, models: true do expect(group.members_with_parents).to include(master) end end + + describe '#user_ids_for_project_authorizations' do + it 'returns the user IDs for which to refresh authorizations' do + master = create(:user) + developer = create(:user) + + group.add_user(master, GroupMember::MASTER) + group.add_user(developer, GroupMember::DEVELOPER) + + expect(group.user_ids_for_project_authorizations). + to include(master.id, developer.id) + end + end end diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 582b54c0712..c60bd7af958 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -37,8 +37,6 @@ describe Guest, lib: true do context 'when repository is enabled' do it 'allows to pull the repo' do - public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) - expect(Guest.can?(:download_code, public_project)).to eq(true) end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 32ed1e96749..e1e99300489 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1005,10 +1005,16 @@ describe MergeRequest, models: true do end end - describe "#environments" do + describe "#environments_for" do let(:project) { create(:project, :repository) } + let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } + before do + merge_request.source_project.add_master(user) + merge_request.target_project.add_master(user) + end + context 'with multiple environments' do let(:environments) { create_list(:environment, 3, project: project) } @@ -1018,7 +1024,7 @@ describe MergeRequest, models: true do end it 'selects deployed environments' do - expect(merge_request.environments).to contain_exactly(environments.first) + expect(merge_request.environments_for(user)).to contain_exactly(environments.first) end end @@ -1042,7 +1048,7 @@ describe MergeRequest, models: true do end it 'selects deployed environments' do - expect(merge_request.environments).to contain_exactly(source_environment) + expect(merge_request.environments_for(user)).to contain_exactly(source_environment) end context 'with environments on target project' do @@ -1053,7 +1059,7 @@ describe MergeRequest, models: true do end it 'selects deployed environments' do - expect(merge_request.environments).to contain_exactly(source_environment, target_environment) + expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment) end end end @@ -1064,7 +1070,7 @@ describe MergeRequest, models: true do end it 'returns an empty array' do - expect(merge_request.environments).to be_empty + expect(merge_request.environments_for(user)).to be_empty end end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4e96f19eb6f..7bb1657bc3a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -218,4 +218,11 @@ describe Namespace, models: true do expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group]) end end + + describe '#user_ids_for_project_authorizations' do + it 'returns the user IDs for which to refresh authorizations' do + expect(namespace.user_ids_for_project_authorizations). + to eq([namespace.owner_id]) + end + end end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb new file mode 100644 index 00000000000..e6a4583a8fb --- /dev/null +++ b/spec/models/pages_domain_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe PagesDomain, models: true do + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + describe :validate_domain do + subject { build(:pages_domain, domain: domain) } + + context 'is unique' do + let(:domain) { 'my.domain.com' } + + it { is_expected.to validate_uniqueness_of(:domain) } + end + + context 'valid domain' do + let(:domain) { 'my.domain.com' } + + it { is_expected.to be_valid } + end + + context 'valid hexadecimal-looking domain' do + let(:domain) { '0x12345.com'} + + it { is_expected.to be_valid } + end + + context 'no domain' do + let(:domain) { nil } + + it { is_expected.not_to be_valid } + end + + context 'invalid domain' do + let(:domain) { '0123123' } + + it { is_expected.not_to be_valid } + end + + context 'domain from .example.com' do + let(:domain) { 'my.domain.com' } + + before { allow(Settings.pages).to receive(:host).and_return('domain.com') } + + it { is_expected.not_to be_valid } + end + end + + describe 'validate certificate' do + subject { domain } + + context 'when only certificate is specified' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.not_to be_valid } + end + + context 'when only key is specified' do + let(:domain) { build(:pages_domain, :with_key) } + + it { is_expected.not_to be_valid } + end + + context 'with matching key' do + let(:domain) { build(:pages_domain, :with_certificate, :with_key) } + + it { is_expected.to be_valid } + end + + context 'for not matching key' do + let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) } + + it { is_expected.not_to be_valid } + end + end + + describe :url do + subject { domain.url } + + context 'without the certificate' do + let(:domain) { build(:pages_domain) } + + it { is_expected.to eq('http://my.domain.com') } + end + + context 'with a certificate' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to eq('https://my.domain.com') } + end + end + + describe :has_matching_key? do + subject { domain.has_matching_key? } + + context 'for matching key' do + let(:domain) { build(:pages_domain, :with_certificate, :with_key) } + + it { is_expected.to be_truthy } + end + + context 'for invalid key' do + let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) } + + it { is_expected.to be_falsey } + end + end + + describe :has_intermediates? do + subject { domain.has_intermediates? } + + context 'for self signed' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to be_truthy } + end + + context 'for missing certificate chain' do + let(:domain) { build(:pages_domain, :with_missing_chain) } + + it { is_expected.to be_falsey } + end + + context 'for trusted certificate chain' do + # We only validate that we can to rebuild the trust chain, for certificates + # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store. + # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine + + let(:domain) { build(:pages_domain, :with_trusted_chain) } + + it { is_expected.to be_truthy } + end + end + + describe :expired? do + subject { domain.expired? } + + context 'for valid' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to be_falsey } + end + + context 'for expired' do + let(:domain) { build(:pages_domain, :with_expired_certificate) } + + it { is_expected.to be_truthy } + end + end + + describe :subject do + let(:domain) { build(:pages_domain, :with_certificate) } + + subject { domain.subject } + + it { is_expected.to eq('/CN=test-certificate') } + end + + describe :certificate_text do + let(:domain) { build(:pages_domain, :with_certificate) } + + subject { domain.certificate_text } + + # We test only existence of output, since the output is long + it { is_expected.not_to be_empty } + end +end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 8589f1eb712..09a4448d387 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -57,7 +57,6 @@ describe ProjectFeature do context 'when feature is enabled for everyone' do it "returns true" do features.each do |feature| - project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) expect(project.feature_available?(:issues, user)).to eq(true) end end @@ -104,7 +103,6 @@ describe ProjectFeature do it "returns true when feature is enabled for everyone" do features.each do |feature| - project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) expect(project.public_send("#{feature}_enabled?")).to eq(true) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48b085781e7..2129bcbd74d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -60,6 +60,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:pages_domains) } it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) } it { is_expected.to have_many(:users_star_projects).dependent(:destroy) } it { is_expected.to have_many(:environments).dependent(:destroy) } @@ -632,7 +633,7 @@ describe Project, models: true do end describe '#has_wiki?' do - let(:no_wiki_project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) } + let(:no_wiki_project) { create(:empty_project, :wiki_disabled, has_external_wiki: false) } let(:wiki_enabled_project) { create(:empty_project) } let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) } @@ -1067,6 +1068,22 @@ describe Project, models: true do end end + describe '#pages_deployed?' do + let(:project) { create :empty_project } + + subject { project.pages_deployed? } + + context 'if public folder does exist' do + before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) } + + it { is_expected.to be_truthy } + end + + context "if public folder doesn't exist" do + it { is_expected.to be_falsey } + end + end + describe '.search' do let(:project) { create(:empty_project, description: 'kitten mittens') } @@ -1699,100 +1716,6 @@ describe Project, models: true do end end - describe '#environments_for' do - let(:project) { create(:project, :repository) } - let(:environment) { create(:environment, project: project) } - - context 'tagged deployment' do - before do - create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) - end - - it 'returns environment when with_tags is set' do - expect(project.environments_for('master', commit: project.commit, with_tags: true)) - .to contain_exactly(environment) - end - - it 'does not return environment when no with_tags is set' do - expect(project.environments_for('master', commit: project.commit)) - .to be_empty - end - - it 'does not return environment when commit is not part of deployment' do - expect(project.environments_for('master', commit: project.commit('feature'))) - .to be_empty - end - end - - context 'branch deployment' do - before do - create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) - end - - it 'returns environment when ref is set' do - expect(project.environments_for('master', commit: project.commit)) - .to contain_exactly(environment) - end - - it 'does not environment when ref is different' do - expect(project.environments_for('feature', commit: project.commit)) - .to be_empty - end - - it 'does not return environment when commit is not part of deployment' do - expect(project.environments_for('master', commit: project.commit('feature'))) - .to be_empty - end - - it 'returns environment when commit constraint is not set' do - expect(project.environments_for('master')) - .to contain_exactly(environment) - end - end - end - - describe '#environments_recently_updated_on_branch' do - let(:project) { create(:project, :repository) } - let(:environment) { create(:environment, project: project) } - - context 'when last deployment to environment is the most recent one' do - before do - create(:deployment, environment: environment, ref: 'feature') - end - - it 'finds recently updated environment' do - expect(project.environments_recently_updated_on_branch('feature')) - .to contain_exactly(environment) - end - end - - context 'when last deployment to environment is not the most recent' do - before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: environment, ref: 'master') - end - - it 'does not find environment' do - expect(project.environments_recently_updated_on_branch('feature')) - .to be_empty - end - end - - context 'when there are two environments that deploy to the same branch' do - let(:second_environment) { create(:environment, project: project) } - - before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: second_environment, ref: 'feature') - end - - it 'finds both environments' do - expect(project.environments_recently_updated_on_branch('feature')) - .to contain_exactly(environment, second_environment) - end - end - end - describe '#deployment_variables' do context 'when project has no deployment service' do let(:project) { create(:empty_project) } @@ -1841,7 +1764,110 @@ describe Project, models: true do it { expect(Project.inside_path(path)).to eq([project1]) } end + describe '#route_map_for' do + let(:project) { create(:project) } + let(:route_map) do + <<-MAP.strip_heredoc + - source: /source/(.*)/ + public: '\\1' + MAP + end + + before do + project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + end + + context 'when there is a .gitlab/route-map.yml at the commit' do + context 'when the route map is valid' do + it 'returns a route map' do + map = project.route_map_for(project.commit.sha) + expect(map).to be_a_kind_of(Gitlab::RouteMap) + end + end + + context 'when the route map is invalid' do + let(:route_map) { 'INVALID' } + + it 'returns nil' do + expect(project.route_map_for(project.commit.sha)).to be_nil + end + end + end + + context 'when there is no .gitlab/route-map.yml at the commit' do + it 'returns nil' do + expect(project.route_map_for(project.commit.parent.sha)).to be_nil + end + end + end + + describe '#public_path_for_source_path' do + let(:project) { create(:project) } + let(:route_map) do + Gitlab::RouteMap.new(<<-MAP.strip_heredoc) + - source: /source/(.*)/ + public: '\\1' + MAP + end + let(:sha) { project.commit.id } + + context 'when there is a route map' do + before do + allow(project).to receive(:route_map_for).with(sha).and_return(route_map) + end + + context 'when the source path is mapped' do + it 'returns the public path' do + expect(project.public_path_for_source_path('source/file.html', sha)).to eq('file.html') + end + end + + context 'when the source path is not mapped' do + it 'returns nil' do + expect(project.public_path_for_source_path('file.html', sha)).to be_nil + end + end + end + + context 'when there is no route map' do + before do + allow(project).to receive(:route_map_for).with(sha).and_return(nil) + end + + it 'returns nil' do + expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil + end + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end + + describe '#pages_url' do + let(:group) { create :group, name: group_name } + let(:project) { create :empty_project, namespace: group, name: project_name } + let(:domain) { 'Example.com' } + + subject { project.pages_url } + + before do + allow(Settings.pages).to receive(:host).and_return(domain) + allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com') + end + + context 'group page' do + let(:group_name) { 'Group' } + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq("http://group.example.com") } + end + + context 'project page' do + let(:group_name) { 'Group' } + let(:project_name) { 'Project' } + + it { is_expected.to eq("http://group.example.com/project") } + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 53b98ba05f8..9bfa6409607 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1782,4 +1782,40 @@ describe Repository, models: true do repository.refresh_method_caches(%i(readme license)) end end + + describe '#gitlab_ci_yml_for' do + before do + repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false) + end + + context 'when there is a .gitlab-ci.yml at the commit' do + it 'returns the content' do + expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT') + end + end + + context 'when there is no .gitlab-ci.yml at the commit' do + it 'returns nil' do + expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil + end + end + end + + describe '#route_map_for' do + before do + repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + end + + context 'when there is a .gitlab/route-map.yml at the commit' do + it 'returns the content' do + expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT') + end + end + + context 'when there is no .gitlab/route-map.yml at the commit' do + it 'returns nil' do + expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil + end + end + end end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb index f08935b6425..ebc694213b6 100644 --- a/spec/models/timelog_spec.rb +++ b/spec/models/timelog_spec.rb @@ -2,9 +2,37 @@ require 'rails_helper' RSpec.describe Timelog, type: :model do subject { build(:timelog) } + let(:issue) { create(:issue) } + let(:merge_request) { create(:merge_request) } it { is_expected.to be_valid } it { is_expected.to validate_presence_of(:time_spent) } it { is_expected.to validate_presence_of(:user) } + + describe 'Issuable validation' do + it 'is invalid if issue_id and merge_request_id are missing' do + subject.attributes = { issue: nil, merge_request: nil } + + expect(subject).to be_invalid + end + + it 'is invalid if issue_id and merge_request_id are set' do + subject.attributes = { issue: issue, merge_request: merge_request } + + expect(subject).to be_invalid + end + + it 'is valid if only issue_id is set' do + subject.attributes = { issue: issue, merge_request: nil } + + expect(subject).to be_valid + end + + it 'is valid if only merge_request_id is set' do + subject.attributes = { merge_request: merge_request, issue: nil } + + expect(subject).to be_valid + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6d58b1455c4..fe88ec63af6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -23,9 +23,9 @@ describe User, models: true do it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } - it { is_expected.to have_many(:assigned_issues).dependent(:destroy) } + it { is_expected.to have_many(:assigned_issues).dependent(:nullify) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } - it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } @@ -1232,7 +1232,7 @@ describe User, models: true do end it 'does not include projects for which issues are disabled' do - project = create(:empty_project, issues_access_level: ProjectFeature::DISABLED) + project = create(:empty_project, :issues_disabled) expect(user.projects_where_can_admin_issues.to_a).to be_empty expect(user.can?(:admin_issue, project)).to eq(false) @@ -1422,4 +1422,37 @@ describe User, models: true do expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true) end end + + describe '#access_level=' do + let(:user) { build(:user) } + + it 'does nothing for an invalid access level' do + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:regular) + expect(user.admin).to be false + end + + it "assigns the 'admin' access level" do + user.access_level = :admin + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + end + + it "doesn't clear existing access levels when an invalid access level is passed in" do + user.access_level = :admin + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + end + + it "accepts string values in addition to symbols" do + user.access_level = 'admin' + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index eeab9827d99..0a5edf35f59 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -10,61 +10,59 @@ describe ProjectPolicy, models: true do let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:guest_permissions) do - [ - :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label, - :read_milestone, :read_project_snippet, :read_project_member, - :read_note, :create_project, :create_issue, :create_note, - :upload_file + %i[ + read_project read_board read_list read_wiki read_issue read_label + read_milestone read_project_snippet read_project_member + read_note create_project create_issue create_note + upload_file ] end let(:reporter_permissions) do - [ - :download_code, :fork_project, :create_project_snippet, :update_issue, - :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build, - :read_container_image, :read_pipeline, :read_environment, :read_deployment, - :read_merge_request, :download_wiki_code + %i[ + download_code fork_project create_project_snippet update_issue + admin_issue admin_label admin_list read_commit_status read_build + read_container_image read_pipeline read_environment read_deployment + read_merge_request download_wiki_code ] end let(:team_member_reporter_permissions) do - [ - :build_download_code, :build_read_container_image - ] + %i[build_download_code build_read_container_image] end let(:developer_permissions) do - [ - :admin_merge_request, :update_merge_request, :create_commit_status, - :update_commit_status, :create_build, :update_build, :create_pipeline, - :update_pipeline, :create_merge_request, :create_wiki, :push_code, - :resolve_note, :create_container_image, :update_container_image, - :create_environment, :create_deployment + %i[ + admin_merge_request update_merge_request create_commit_status + update_commit_status create_build update_build create_pipeline + update_pipeline create_merge_request create_wiki push_code + resolve_note create_container_image update_container_image + create_environment create_deployment ] end let(:master_permissions) do - [ - :push_code_to_protected_branches, :update_project_snippet, :update_environment, - :update_deployment, :admin_milestone, :admin_project_snippet, - :admin_project_member, :admin_note, :admin_wiki, :admin_project, - :admin_commit_status, :admin_build, :admin_container_image, - :admin_pipeline, :admin_environment, :admin_deployment + %i[ + push_code_to_protected_branches update_project_snippet update_environment + update_deployment admin_milestone admin_project_snippet + admin_project_member admin_note admin_wiki admin_project + admin_commit_status admin_build admin_container_image + admin_pipeline admin_environment admin_deployment ] end let(:public_permissions) do - [ - :download_code, :fork_project, :read_commit_status, :read_pipeline, - :read_container_image, :build_download_code, :build_read_container_image, - :download_wiki_code + %i[ + download_code fork_project read_commit_status read_pipeline + read_container_image build_download_code build_read_container_image + download_wiki_code ] end let(:owner_permissions) do - [ - :change_namespace, :change_visibility_level, :rename_project, :remove_project, - :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue + %i[ + change_namespace change_visibility_level rename_project remove_project + archive_project remove_fork_project destroy_merge_request destroy_issue ] end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb new file mode 100644 index 00000000000..d0758af57dd --- /dev/null +++ b/spec/policies/project_snippet_policy_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe ProjectSnippetPolicy, models: true do + let(:current_user) { create(:user) } + + let(:author_permissions) do + [ + :update_project_snippet, + :admin_project_snippet + ] + end + + subject { described_class.abilities(current_user, project_snippet).to_set } + + context 'public snippet' do + let(:project_snippet) { create(:project_snippet, :public) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + end + + context 'internal snippet' do + let(:project_snippet) { create(:project_snippet, :internal) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + end + + context 'private snippet' do + let(:project_snippet) { create(:project_snippet, :private) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'snippet author' do + let(:project_snippet) { create(:project_snippet, :private, author: current_user) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.to include(*author_permissions) + end + end + + context 'project team member' do + before { project_snippet.project.team << [current_user, :developer] } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'admin user' do + let(:current_user) { create(:admin) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.to include(*author_permissions) + end + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 863da19f294..cca00df9591 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -425,7 +425,7 @@ describe API::Issues, api: true do end it 'returns no issues when user has access to project but not issues' do - restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + restricted_project = create(:empty_project, :public, :issues_private) create(:issue, project: restricted_project) get api("/projects/#{restricted_project.id}/issues", non_member) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 45d5ae267c5..eea76c7bb94 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -7,18 +7,6 @@ describe API::ProjectSnippets, api: true do let(:user) { create(:user) } let(:admin) { create(:admin) } - describe 'GET /projects/:project_id/snippets/:id' do - # TODO (rspeicher): Deprecated; remove in 9.0 - it 'always exposes expires_at as nil' do - snippet = create(:project_snippet, author: admin) - - get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) - - expect(json_response).to have_key('expires_at') - expect(json_response['expires_at']).to be_nil - end - end - describe 'GET /projects/:project_id/snippets/' do let(:user) { create(:user) } diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb new file mode 100644 index 00000000000..f5bdf408c5e --- /dev/null +++ b/spec/requests/api/v3/deploy_keys_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +describe API::V3::DeployKeys, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:empty_project, creator_id: user.id) } + let(:project2) { create(:empty_project, creator_id: user.id) } + let(:deploy_key) { create(:deploy_key, public: true) } + + let!(:deploy_keys_project) do + create(:deploy_keys_project, project: project, deploy_key: deploy_key) + end + + describe 'GET /deploy_keys' do + context 'when unauthenticated' do + it 'should return authentication error' do + get v3_api('/deploy_keys') + + expect(response.status).to eq(401) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 403 error' do + get v3_api('/deploy_keys', user) + + expect(response.status).to eq(403) + end + end + + context 'when authenticated as admin' do + it 'should return all deploy keys' do + get v3_api('/deploy_keys', admin) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) + end + end + end + + %w(deploy_keys keys).each do |path| + describe "GET /projects/:id/#{path}" do + before { deploy_key } + + it 'should return array of ssh keys' do + get v3_api("/projects/#{project.id}/#{path}", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(deploy_key.title) + end + end + + describe "GET /projects/:id/#{path}/:key_id" do + it 'should return a single key' do + get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(deploy_key.title) + end + + it 'should return 404 Not Found with invalid ID' do + get v3_api("/projects/#{project.id}/#{path}/404", admin) + + expect(response).to have_http_status(404) + end + end + + describe "POST /projects/:id/deploy_keys" do + it 'should not create an invalid ssh key' do + post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' } + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('key is missing') + end + + it 'should not create a key without title' do + post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key' + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('title is missing') + end + + it 'should create new ssh key' do + key_attrs = attributes_for :another_key + + expect do + post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs + end.to change{ project.deploy_keys.count }.by(1) + end + + it 'returns an existing ssh key when attempting to add a duplicate' do + expect do + post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title } + end.not_to change { project.deploy_keys.count } + + expect(response).to have_http_status(201) + end + + it 'joins an existing ssh key to a new project' do + expect do + post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title } + end.to change { project2.deploy_keys.count }.by(1) + + expect(response).to have_http_status(201) + end + end + + describe "DELETE /projects/:id/#{path}/:key_id" do + before { deploy_key } + + it 'should delete existing key' do + expect do + delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin) + end.to change{ project.deploy_keys.count }.by(-1) + end + + it 'should return 404 Not Found with invalid ID' do + delete v3_api("/projects/#{project.id}/#{path}/404", admin) + + expect(response).to have_http_status(404) + end + end + + describe "POST /projects/:id/#{path}/:key_id/enable" do + let(:project2) { create(:empty_project) } + + context 'when the user can admin the project' do + it 'enables the key' do + expect do + post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin) + end.to change { project2.deploy_keys.count }.from(0).to(1) + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(deploy_key.id) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 404 error' do + post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user) + + expect(response).to have_http_status(404) + end + end + end + + describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do + context 'when the user can admin the project' do + it 'disables the key' do + expect do + delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin) + end.to change { project.deploy_keys.count }.from(1).to(0) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(deploy_key.id) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 404 error' do + delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user) + + expect(response).to have_http_status(404) + end + end + end + end +end diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb new file mode 100644 index 00000000000..3700477f0db --- /dev/null +++ b/spec/requests/api/v3/project_snippets_spec.rb @@ -0,0 +1,188 @@ +require 'rails_helper' + +describe API::ProjectSnippets, api: true do + include ApiHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET /projects/:project_id/snippets/:id' do + # TODO (rspeicher): Deprecated; remove in 9.0 + it 'always exposes expires_at as nil' do + snippet = create(:project_snippet, author: admin) + + get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) + + expect(json_response).to have_key('expires_at') + expect(json_response['expires_at']).to be_nil + end + end + + describe 'GET /projects/:project_id/snippets/' do + let(:user) { create(:user) } + + it 'returns all snippets available to team member' do + project.add_developer(user) + public_snippet = create(:project_snippet, :public, project: project) + internal_snippet = create(:project_snippet, :internal, project: project) + private_snippet = create(:project_snippet, :private, project: project) + + get v3_api("/projects/#{project.id}/snippets/", user) + + expect(response).to have_http_status(200) + expect(json_response.size).to eq(3) + expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) + expect(json_response.last).to have_key('web_url') + end + + it 'hides private snippets from regular user' do + create(:project_snippet, :private, project: project) + + get v3_api("/projects/#{project.id}/snippets/", user) + expect(response).to have_http_status(200) + expect(json_response.size).to eq(0) + end + end + + describe 'POST /projects/:project_id/snippets/' do + let(:params) do + { + title: 'Test Title', + file_name: 'test.rb', + code: 'puts "hello world"', + visibility_level: Snippet::PUBLIC + } + end + + it 'creates a new snippet' do + post v3_api("/projects/#{project.id}/snippets/", admin), params + + expect(response).to have_http_status(201) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(params[:code]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.visibility_level).to eq(params[:visibility_level]) + end + + it 'returns 400 for missing parameters' do + params.delete(:title) + + post v3_api("/projects/#{project.id}/snippets/", admin), params + + expect(response).to have_http_status(400) + end + + context 'when the snippet is spam' do + def create_snippet(project, snippet_params = {}) + project.add_developer(user) + + post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + end + + describe 'PUT /projects/:project_id/snippets/:id/' do + let(:snippet) { create(:project_snippet, author: admin) } + + it 'updates snippet' do + new_content = 'New content' + + put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content + + expect(response).to have_http_status(200) + snippet.reload + expect(snippet.content).to eq(new_content) + end + + it 'returns 404 for invalid snippet id' do + put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo' + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + + it 'returns 400 for missing parameters' do + put v3_api("/projects/#{project.id}/snippets/1234", admin) + + expect(response).to have_http_status(400) + end + end + + describe 'DELETE /projects/:project_id/snippets/:id/' do + let(:snippet) { create(:project_snippet, author: admin) } + + it 'deletes snippet' do + admin = create(:admin) + snippet = create(:project_snippet, author: admin) + + delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) + + expect(response).to have_http_status(200) + end + + it 'returns 404 for invalid snippet id' do + delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + describe 'GET /projects/:project_id/snippets/:id/raw' do + let(:snippet) { create(:project_snippet, author: admin) } + + it 'returns raw text' do + get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to eq(snippet.content) + end + + it 'returns 404 for invalid snippet id' do + delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end +end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 4a16824de04..87786e85621 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -57,7 +57,7 @@ describe 'Git HTTP requests', lib: true do end context 'but the repo is disabled' do - let(:project) { create(:project, repository_access_level: ProjectFeature::DISABLED, wiki_access_level: ProjectFeature::ENABLED) } + let(:project) { create(:project, :repository_disabled, :wiki_enabled) } let(:wiki) { ProjectWiki.new(project) } let(:path) { "/#{wiki.repository.path_with_namespace}.git" } @@ -141,7 +141,7 @@ describe 'Git HTTP requests', lib: true do context 'when the repo is public' do context 'but the repo is disabled' do it 'does not allow to clone the repo' do - project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED) + project = create(:project, :public, :repository_disabled) download("#{project.path_with_namespace}.git", {}) do |response| expect(response).to have_http_status(:unauthorized) @@ -151,7 +151,7 @@ describe 'Git HTTP requests', lib: true do context 'but the repo is enabled' do it 'allows to clone the repo' do - project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED) + project = create(:project, :public, :repository_enabled) download("#{project.path_with_namespace}.git", {}) do |response| expect(response).to have_http_status(:ok) @@ -161,7 +161,7 @@ describe 'Git HTTP requests', lib: true do context 'but only project members are allowed' do it 'does not allow to clone the repo' do - project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE) + project = create(:project, :public, :repository_private) download("#{project.path_with_namespace}.git", {}) do |response| expect(response).to have_http_status(:unauthorized) @@ -360,10 +360,6 @@ describe 'Git HTTP requests', lib: true do let(:project) { build.project } let(:other_project) { create(:empty_project) } - before do - project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) - end - context 'when build created by system is authenticated' do it "downloads get status 200" do clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 96889abee79..a5bc62ef6c2 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -27,35 +27,42 @@ describe 'project routing' do # let(:actions) { [:index] } # let(:controller) { 'issues' } # end + # + # # Different controller name and path + # it_behaves_like 'RESTful project resources' do + # let(:controller) { 'pages_domains' } + # let(:controller_path) { 'pages/domains' } + # end shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } + let(:controller_path) { controller } it 'to #index' do - expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) + expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) end it 'to #create' do - expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) + expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) end it 'to #new' do - expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) + expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) end it 'to #edit' do - expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) + expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) end it 'to #show' do - expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) + expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) end it 'to #update' do - expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) + expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) end it 'to #destroy' do - expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) + expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) end end @@ -539,4 +546,20 @@ describe 'project routing' do 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq') end end + + describe Projects::PagesDomainsController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:show, :new, :create, :destroy] } + let(:controller) { 'pages_domains' } + let(:controller_path) { 'pages/domains' } + end + + it 'to #destroy with a valid domain name' do + expect(delete('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com') + end + + it 'to #show with a valid domain' do + expect(get('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com') + end + end end diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index 6f7d1a5d28d..560f83d94f7 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -42,10 +42,10 @@ describe Ci::StopEnvironmentsService, services: true do end end - context 'when environment is not stoppable' do + context 'when environment is not stopped' do before do allow_any_instance_of(Environment) - .to receive(:stoppable?).and_return(false) + .to receive(:state).and_return(:stopped) end it 'does not stop environment' do diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 531180e48a1..7c0fccb9d41 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -51,7 +51,7 @@ describe Groups::UpdateService, services: true do end context 'rename group' do - let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) } before do internal_group.add_user(user, Gitlab::Access::MASTER) diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index ac3834c32ff..30578ee4c7d 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -181,5 +181,107 @@ describe Issues::CreateService, services: true do expect(issue.title).to be_nil end end + + context 'checking spam' do + let(:opts) do + { + title: 'Awesome issue', + description: 'please fix', + request: double(:request, env: {}) + } + end + + before do + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + end + + context 'when recaptcha was verified' do + let(:log_user) { user } + let(:spam_logs) { create_list(:spam_log, 2, user: log_user, title: 'Awesome issue') } + + before do + opts[:recaptcha_verified] = true + opts[:spam_log_id] = spam_logs.last.id + + expect(AkismetService).not_to receive(:new) + end + + it 'does no mark an issue as a spam ' do + expect(issue).not_to be_spam + end + + it 'an issue is valid ' do + expect(issue.valid?).to be_truthy + end + + it 'does not assign a spam_log to an issue' do + expect(issue.spam_log).to be_nil + end + + it 'marks related spam_log as recaptcha_verified' do + expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true) + end + + context 'when spam log does not belong to a user' do + let(:log_user) { create(:user) } + + it 'does not mark spam_log as recaptcha_verified' do + expect { issue }.not_to change{SpamLog.last.recaptcha_verified} + end + end + + context 'when spam log title does not match the issue title' do + before do + opts[:title] = 'Another issue' + end + + it 'does not mark spam_log as recaptcha_verified' do + expect { issue }.not_to change{SpamLog.last.recaptcha_verified} + end + end + end + + context 'when recaptcha was not verified' do + context 'when akismet detects spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + it 'marks an issue as a spam ' do + expect(issue).to be_spam + end + + it 'an issue is not valid ' do + expect(issue.valid?).to be_falsey + end + + it 'creates a new spam_log' do + expect{issue}.to change{SpamLog.count}.from(0).to(1) + end + + it 'assigns a spam_log to an issue' do + expect(issue.spam_log).to eq(SpamLog.last) + end + end + + context 'when akismet does not detect spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false) + end + + it 'does not mark an issue as a spam ' do + expect(issue).not_to be_spam + end + + it 'an issue is valid ' do + expect(issue.valid?).to be_truthy + end + + it 'does not assign a spam_log to an issue' do + expect(issue.spam_log).to be_nil + end + end + end + end end end diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb new file mode 100644 index 00000000000..aa63fe3a5c1 --- /dev/null +++ b/spec/services/pages_service_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe PagesService, services: true do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::DataBuilder::Build.build(build) } + let(:service) { PagesService.new(data) } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + end + + context 'execute asynchronously for pages job' do + before { build.name = 'pages' } + + context 'on success' do + before { build.success } + + it 'executes worker' do + expect(PagesWorker).to receive(:perform_async) + service.execute + end + end + + %w(pending running failed canceled).each do |status| + context "on #{status}" do + before { build.status = status } + + it 'does not execute worker' do + expect(PagesWorker).not_to receive(:perform_async) + service.execute + end + end + end + end + + context 'for other jobs' do + before do + build.name = 'other job' + build.success + end + + it 'does not execute worker' do + expect(PagesWorker).not_to receive(:perform_async) + service.execute + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index a1539b69401..af515ad2e0e 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -90,10 +90,6 @@ describe Projects::CreateService, '#execute', services: true do end context 'global builds_enabled true does enable CI by default' do - before do - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) - end - it { is_expected.to be_truthy } end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 90771825f5c..3faa88c00a1 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -9,12 +9,27 @@ describe Projects::DestroyService, services: true do shared_examples 'deleting the project' do it 'deletes the project' do - expect(Project.all).not_to include(project) + expect(Project.unscoped.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey expect(Dir.exist?(remove_path)).to be_falsey end end + shared_examples 'deleting the project with pipeline and build' do + context 'with pipeline and build' do # which has optimistic locking + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + before do + perform_enqueued_jobs do + destroy_project(project, user, {}) + end + end + + it_behaves_like 'deleting the project' + end + end + context 'Sidekiq inline' do before do # Run sidekiq immediatly to check that renamed repository will be removed @@ -35,30 +50,24 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end - context 'async delete of project with private issue visibility' do - let!(:async) { true } + context 'with async_execute' do + let(:async) { true } - before do - project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE) - # Run sidekiq immediately to check that renamed repository will be removed - Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + context 'async delete of project with private issue visibility' do + before do + project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE) + # Run sidekiq immediately to check that renamed repository will be removed + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end + + it_behaves_like 'deleting the project' end - it_behaves_like 'deleting the project' + it_behaves_like 'deleting the project with pipeline and build' end - context 'delete with pipeline' do # which has optimistic locking - let!(:pipeline) { create(:ci_pipeline, project: project) } - - before do - expect(project).to receive(:destroy!).and_call_original - - perform_enqueued_jobs do - destroy_project(project, user, {}) - end - end - - it_behaves_like 'deleting the project' + context 'with execute' do + it_behaves_like 'deleting the project with pipeline and build' end context 'container registry' do diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 1540b90163a..5c6fbea8d0e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -9,6 +9,8 @@ describe Projects::TransferService, services: true do before do allow_any_instance_of(Gitlab::UploadsTransfer). to receive(:move_project).and_return(true) + allow_any_instance_of(Gitlab::PagesTransfer). + to receive(:move_project).and_return(true) group.add_owner(user) @result = transfer_project(project, user, group) end @@ -81,4 +83,30 @@ describe Projects::TransferService, services: true do transfer_project(project, user, group) end end + + describe 'refreshing project authorizations' do + let(:group) { create(:group) } + let(:owner) { project.namespace.owner } + let(:group_member) { create(:user) } + + before do + group.add_user(owner, GroupMember::MASTER) + group.add_user(group_member, GroupMember::DEVELOPER) + end + + it 'refreshes the permissions of the old and new namespace' do + transfer_project(project, owner, group) + + expect(group_member.authorized_projects).to include(project) + expect(owner.authorized_projects).to include(project) + end + + it 'only schedules a single job for every user' do + expect(UserProjectAccessChangedService).to receive(:new). + with([owner.id, group_member.id]). + and_call_original + + transfer_project(project, owner, group) + end + end end diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb new file mode 100644 index 00000000000..8b329bc21c3 --- /dev/null +++ b/spec/services/projects/update_pages_configuration_service_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Projects::UpdatePagesConfigurationService, services: true do + let(:project) { create(:empty_project) } + subject { described_class.new(project) } + + describe "#update" do + let(:file) { Tempfile.new('pages-test') } + + after do + file.close + file.unlink + end + + it 'updates the .update file' do + # Access this reference to ensure scoping works + Projects::Settings # rubocop:disable Lint/Void + expect(subject).to receive(:pages_config_file).and_return(file.path) + expect(subject).to receive(:reload_daemon).and_call_original + + expect(subject.execute).to eq({ status: :success }) + end + end +end diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb new file mode 100644 index 00000000000..411b22a0fb8 --- /dev/null +++ b/spec/services/projects/update_pages_service_spec.rb @@ -0,0 +1,80 @@ +require "spec_helper" + +describe Projects::UpdatePagesService do + let(:project) { create :project } + let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha } + let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' } + let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } + + subject { described_class.new(project, build) } + + before do + project.remove_pages + end + + %w(tar.gz zip).each do |format| + context "for valid #{format}" do + let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") } + let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") } + let(:metadata) do + filename = Rails.root + "spec/fixtures/pages.#{format}.meta" + fixture_file_upload(filename) if File.exist?(filename) + end + + before do + build.update_attributes(artifacts_file: file) + build.update_attributes(artifacts_metadata: metadata) + end + + it 'succeeds' do + expect(project.pages_deployed?).to be_falsey + expect(execute).to eq(:success) + expect(project.pages_deployed?).to be_truthy + end + + it 'limits pages size' do + stub_application_setting(max_pages_size: 1) + expect(execute).not_to eq(:success) + end + + it 'removes pages after destroy' do + expect(PagesWorker).to receive(:perform_in) + expect(project.pages_deployed?).to be_falsey + expect(execute).to eq(:success) + expect(project.pages_deployed?).to be_truthy + project.destroy + expect(project.pages_deployed?).to be_falsey + end + + it 'fails if sha on branch is not latest' do + pipeline.update_attributes(sha: 'old_sha') + build.update_attributes(artifacts_file: file) + expect(execute).not_to eq(:success) + end + + it 'fails for empty file fails' do + build.update_attributes(artifacts_file: empty_file) + expect(execute).not_to eq(:success) + end + end + end + + it 'fails to remove project pages when no pages is deployed' do + expect(PagesWorker).not_to receive(:perform_in) + expect(project.pages_deployed?).to be_falsey + project.destroy + end + + it 'fails if no artifacts' do + expect(execute).not_to eq(:success) + end + + it 'fails for invalid archive' do + build.update_attributes(artifacts_file: invalid_file) + expect(execute).not_to eq(:success) + end + + def execute + subject.execute[:status] + end +end diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb new file mode 100644 index 00000000000..271c17dd8c0 --- /dev/null +++ b/spec/services/spam_service_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe SpamService, services: true do + describe '#check' do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:request) { double(:request, env: {}) } + + def check_spam(issue, request) + described_class.new(issue, request).check + end + + context 'when indicated as spam by akismet' do + before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) } + + it 'returns false when request is missing' do + expect(check_spam(issue, nil)).to be_falsey + end + + it 'returns false when issue is not public' do + issue = create(:issue, project: create(:project, :private)) + + expect(check_spam(issue, request)).to be_falsey + end + + it 'returns true' do + expect(check_spam(issue, request)).to be_truthy + end + + it 'creates a spam log' do + expect { check_spam(issue, request) }.to change { SpamLog.count }.from(0).to(1) + end + end + + context 'when not indicated as spam by akismet' do + before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + + it 'returns false' do + expect(check_spam(issue, request)).to be_falsey + end + + it 'does not create a spam log' do + expect { check_spam(issue, request) }.not_to change { SpamLog.count } + end + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 7913a180f9b..7f027ae02a2 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -418,6 +418,45 @@ describe SystemNoteService, services: true do to be_truthy end end + + context 'when noteable is an Issue' do + let(:issue) { create(:issue, project: project) } + + it 'is truthy when issue is closed' do + issue.close + + expect(described_class.cross_reference_disallowed?(issue, project.commit)). + to be_truthy + end + + it 'is falsey when issue is open' do + expect(described_class.cross_reference_disallowed?(issue, project.commit)). + to be_falsy + end + end + + context 'when noteable is a Merge Request' do + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + + it 'is truthy when merge request is closed' do + allow(merge_request).to receive(:closed?).and_return(:true) + + expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). + to be_truthy + end + + it 'is truthy when merge request is merged' do + allow(merge_request).to receive(:closed?).and_return(:true) + + expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). + to be_truthy + end + + it 'is falsey when merge request is open' do + expect(described_class.cross_reference_disallowed?(merge_request, project.commit)). + to be_falsy + end + end end describe '.cross_reference_exists?' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ab38dac65c5..5fda7c63cdb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -35,6 +35,7 @@ RSpec.configure do |config| config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature + config.include WaitForAjax, type: :feature config.include StubConfiguration config.include EmailHelpers, type: :mailer config.include TestEnv diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb index 93c0267d2db..4f0c745b7ee 100644 --- a/spec/support/services/issuable_create_service_shared_examples.rb +++ b/spec/support/services/issuable_create_service_shared_examples.rb @@ -31,8 +31,8 @@ shared_examples 'issuable create service' do context "when issuable feature is private" do before do - project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) - project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) end levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index bc751d20ce1..df8a47893f9 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -28,7 +28,7 @@ describe 'gitlab:app namespace rake task' do end def reenable_backup_sub_tasks - %w{db repo uploads builds artifacts lfs registry}.each do |subtask| + %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -71,6 +71,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke) + expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke) expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke) @@ -202,7 +203,7 @@ describe 'gitlab:app namespace rake task' do it 'sets correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') @@ -210,14 +211,15 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('repositories/') expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') + expect(tar_contents).to match('pages.tar.gz') expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).to match('registry.tar.gz') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) end it 'deletes temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}') ) expect(temp_dirs).to be_empty @@ -304,7 +306,7 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz} ) expect(tar_contents).to match('db/') @@ -312,6 +314,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('lfs.tar.gz') + expect(tar_contents).to match('pages.tar.gz') expect(tar_contents).to match('registry.tar.gz') expect(tar_contents).not_to match('repositories/') end @@ -327,6 +330,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke + expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke expect(Rake::Task['gitlab:shell:setup']).to receive :invoke diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 59cfb2c8e3a..d2609d21546 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do subject { described_class.new } it 'passes when the project has no push events' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) + project = create(:project_empty_repo, :wiki_disabled) project.events.destroy_all break_repo(project) @@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'fails if the wiki repository is broken' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) + project = create(:project_empty_repo, :wiki_enabled) project.create_wiki # Test sanity: everything should be fine before the wiki repo is broken @@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'skips wikis when disabled' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) + project = create(:project_empty_repo, :wiki_disabled) # Make sure the test would fail if the wiki repo was checked break_wiki(project) @@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'creates missing wikis' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) + project = create(:project_empty_repo, :wiki_enabled) FileUtils.rm_rf(wiki_path(project)) subject.perform(project.id) diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index d028d1251ad..a1a65c2d72e 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -44,3 +44,11 @@ captures/ # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +#Freeline +freeline.py +freeline/ +freeline_project_description.json diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore index 27ada0591ec..9ea395f15ee 100644 --- a/vendor/gitignore/CMake.gitignore +++ b/vendor/gitignore/CMake.gitignore @@ -1,6 +1,7 @@ CMakeCache.txt CMakeFiles CMakeScripts +Testing Makefile cmake_install.cmake install_manifest.txt diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore index 60571a0c383..bfea17cdc5b 100644 --- a/vendor/gitignore/CodeIgniter.gitignore +++ b/vendor/gitignore/CodeIgniter.gitignore @@ -9,3 +9,9 @@ user_guide_src/build/* user_guide_src/cilexer/build/* user_guide_src/cilexer/dist/* user_guide_src/cilexer/pycilexer.egg-info/* + +#codeigniter 3 +application/logs/* +!application/logs/index.html +!application/logs/.htaccess +/vendor/ diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index e375c744b6d..401fee15748 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -2,24 +2,24 @@ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: -.idea/workspace.xml -.idea/tasks.xml +.idea/**/workspace.xml +.idea/**/tasks.xml # Sensitive or high-churn files: -.idea/dataSources/ -.idea/dataSources.ids -.idea/dataSources.xml -.idea/dataSources.local.xml -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml # Gradle: -.idea/gradle.xml -.idea/libraries +.idea/**/gradle.xml +.idea/**/libraries # Mongo Explorer plugin: -.idea/mongoSettings.xml +.idea/**/mongoSettings.xml ## File-based project format: *.iws diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore index 32a5ad4c777..09dfde64b5f 100644 --- a/vendor/gitignore/Global/Matlab.gitignore +++ b/vendor/gitignore/Global/Matlab.gitignore @@ -17,3 +17,6 @@ slprj/ # Session info octave-workspace + +# Simulink autosave extension +.autosave diff --git a/vendor/gitignore/Global/Stata.gitignore b/vendor/gitignore/Global/Stata.gitignore new file mode 100644 index 00000000000..07997bb1201 --- /dev/null +++ b/vendor/gitignore/Global/Stata.gitignore @@ -0,0 +1,24 @@ +# .gitignore file for git projects containing Stata files +# Commercial statistical software: http://www.stata.com + +# Stata dataset and output files +*.dta +*.gph +*.log +*.smcl +*.stpr +*.stsem + +# Graphic export files from Stata +# Stata command graph export: http://www.stata.com/manuals14/g-2graphexport.pdf +# +# You may add graphic export files to your .gitignore. However you should be +# aware that this will exclude all image files from this main directory +# and subdirectories. +# *.ps +# *.eps +# *.wmf +# *.emf +# *.pdf +# *.png +# *.tif diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index 5e1047c9d78..a1338d68517 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -1,30 +1,14 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a +# Binaries for programs and plugins +*.exe +*.dll *.so +*.dylib -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe +# Test binary, build with `go test -c` *.test -*.prof # Output of the go coverage tool, specifically when used with LiteIDE *.out -# External packages folder -vendor/ +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index e44e0860405..dbb4a2dfa1a 100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore @@ -1,5 +1,8 @@ *.class +# Log file +*.log + # BlueJ files *.ctxt @@ -10,6 +13,9 @@ *.jar *.war *.ear +*.zip +*.tar.gz +*.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore index 93103fdbe77..53a74e74657 100644 --- a/vendor/gitignore/Joomla.gitignore +++ b/vendor/gitignore/Joomla.gitignore @@ -29,8 +29,6 @@ /administrator/components/com_search/* /administrator/components/com_templates/* /administrator/components/com_users/* -/administrator/components/com_weblinks/* -/administrator/components/index.html /administrator/help/* /administrator/includes/* /administrator/language/en-GB/en-GB.com_ajax.ini @@ -41,7 +39,6 @@ /administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini /administrator/language/en-GB/en-GB.com_postinstall.ini /administrator/language/en-GB/en-GB.com_postinstall.sys.ini -/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini /administrator/language/en-GB/en-GB.com_tags.ini /administrator/language/en-GB/en-GB.com_tags.sys.ini /administrator/language/en-GB/en-GB.mod_stats_admin.ini @@ -250,15 +247,10 @@ /administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini /administrator/language/en-GB/en-GB.plg_user_profile.ini /administrator/language/en-GB/en-GB.plg_user_profile.sys.ini -/administrator/language/en-GB/en-GB.tpl_bluestork.ini -/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini /administrator/language/en-GB/en-GB.tpl_hathor.ini /administrator/language/en-GB/en-GB.tpl_hathor.sys.ini /administrator/language/en-GB/en-GB.xml -/administrator/language/en-GB/index.html -/administrator/language/ru-RU/index.html /administrator/language/overrides/* -/administrator/language/index.html /administrator/logs/index.html /administrator/manifests/* /administrator/modules/mod_custom/* @@ -278,12 +270,9 @@ /administrator/modules/mod_unread/* /administrator/modules/mod_version/* /administrator/modules/mod_stats_admin/* -/administrator/modules/index.html -/administrator/templates/bluestork/* /administrator/templates/isis/* /administrator/templates/hathor/* /administrator/templates/system/* -/administrator/templates/index.html /administrator/index.php /cache/* /bin/* @@ -302,7 +291,6 @@ /components/com_newsfeeds/* /components/com_search/* /components/com_users/* -/components/com_weblinks/* /components/com_wrapper/* /components/index.html /images/banners/* @@ -403,7 +391,6 @@ /language/en-GB/en-GB.tpl_beez5.ini /language/en-GB/en-GB.tpl_beez5.sys.ini /language/en-GB/en-GB.xml -/language/en-GB/index.html /language/en-GB/install.xml /language/overrides/* /language/index.html @@ -428,8 +415,6 @@ /libraries/index.html /libraries/import.php /libraries/loader.php -/libraries/platform.php -/logs/* /media/cms/* /media/com_contenthistory/* /media/com_finder/* @@ -472,7 +457,6 @@ /modules/mod_tags_popular/* /modules/mod_tags_similar/* /modules/mod_users_latest/* -/modules/mod_weblinks/* /modules/mod_whosonline/* /modules/mod_wrapper/* /modules/index.html @@ -481,9 +465,7 @@ /plugins/authentication/joomla/* /plugins/authentication/ldap/* /plugins/authentication/cookie/* -/plugins/authentication/index.html /plugins/captcha/recaptcha/* -/plugins/captcha/index.html /plugins/content/emailcloak/* /plugins/content/example/* /plugins/content/finder/* @@ -494,27 +476,21 @@ /plugins/content/pagenavigation/* /plugins/content/vote/* /plugins/content/contact/* -/plugins/content/index.html /plugins/editors/codemirror/* /plugins/editors/none/* /plugins/editors/tinymce/* -/plugins/editors/index.html /plugins/editors-xtd/module/* /plugins/editors-xtd/article/* /plugins/editors-xtd/image/* /plugins/editors-xtd/pagebreak/* /plugins/editors-xtd/readmore/* -/plugins/editors-xtd/index.html /plugins/extension/example/* /plugins/extension/joomla/* -/plugins/extension/index.html -/plugins/finder/index.html /plugins/finder/categories/* /plugins/finder/contacts/* /plugins/finder/content/* /plugins/finder/newsfeeds/* /plugins/finder/tags/* -/plugins/finder/weblinks/* /plugins/installer/* /plugins/quickicon/extensionupdate/* /plugins/quickicon/joomlaupdate/* @@ -547,10 +523,7 @@ /plugins/user/profile/* /plugins/user/index.html /plugins/index.html -/templates/atomic/* /templates/beez3/* -/templates/beez_20/* -/templates/beez5/* /templates/protostar/* /templates/system/* /templates/index.html diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore index 606ed1c7b4d..208bc4fc591 100644 --- a/vendor/gitignore/KiCad.gitignore +++ b/vendor/gitignore/KiCad.gitignore @@ -13,7 +13,8 @@ _autosave-* *.net # Autorouter files (exported from Pcbnew) -.dsn +*.dsn +*.ses # Exported BOM files *.xml diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore index a2d1564060b..a4854bef534 100644 --- a/vendor/gitignore/Laravel.gitignore +++ b/vendor/gitignore/Laravel.gitignore @@ -1,5 +1,6 @@ vendor/ node_modules/ +npm-debug.log # Laravel 4 specific bootstrap/compiled.php @@ -7,10 +8,13 @@ app/storage/ # Laravel 5 & Lumen specific public/storage +public/hot storage/*.key .env.*.php .env.php .env +Homestead.yaml +Homestead.json # Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer .rocketeer/ diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore index 195c9b7a029..b282f5cf547 100644 --- a/vendor/gitignore/Magento.gitignore +++ b/vendor/gitignore/Magento.gitignore @@ -1,104 +1,16 @@ -.htaccess.sample -.modgit/ -.modman/ -app/code/community/Phoenix/Moneybookers/ -app/code/community/Cm/RedisSession/ -app/code/core/ -app/design/adminhtml/default/default/ -app/design/frontend/base/ -app/design/frontend/rwd/ -app/design/frontend/default/blank/ -app/design/frontend/default/default/ -app/design/frontend/default/iphone/ -app/design/frontend/default/modern/ -app/design/frontend/enterprise/default -app/design/install/ -app/etc/modules/Enterprise_* -app/etc/modules/Mage_*.xml -app/etc/modules/Phoenix_Moneybookers.xml -app/etc/modules/Cm_RedisSession.xml -app/etc/applied.patches.list -app/etc/config.xml -app/etc/enterprise.xml -app/etc/local.xml.additional -app/etc/local.xml.template -app/etc/local.xml -app/.htaccess -app/bootstrap.php -app/locale/en_US/ -app/Mage.php -/cron.php -cron.sh -dev/.htaccess -dev/tests/functional/ -downloader/ -errors/ -favicon.ico -/get.php -includes/ -/index.php -index.php.sample -/install.php -js/blank.html -js/calendar/ -js/enterprise/ -js/extjs/ -js/firebug/ -js/flash/ -js/index.php -js/jscolor/ -js/lib/ -js/mage/ -js/prototype/ -js/scriptaculous/ -js/spacer.gif -js/tiny_mce/ -js/varien/ -lib/3Dsecure/ -lib/Apache/ -lib/flex/ -lib/googlecheckout/ -lib/.htaccess -lib/LinLibertineFont/ -lib/Mage/ -lib/PEAR/ -lib/Pelago/ -lib/phpseclib/ -lib/Varien/ -lib/Zend/ -lib/Cm/ -lib/Credis/ -lib/Magento/ -LICENSE_AFL.txt -LICENSE.html -LICENSE.txt -LICENSE_EE* -/mage -media/ -/api.php -nbproject/ -pear -pear/ -php.ini.sample -pkginfo/ -RELEASE_NOTES.txt -shell/.htaccess -shell/abstract.php -shell/compiler.php -shell/indexer.php -shell/log.php -sitemap.xml -skin/adminhtml/default/default/ -skin/adminhtml/default/enterprise -skin/frontend/base/ -skin/frontend/rwd/ -skin/frontend/default/blank/ -skin/frontend/default/blue/ -skin/frontend/default/default/ -skin/frontend/default/french/ -skin/frontend/default/german/ -skin/frontend/default/iphone/ -skin/frontend/default/modern/ -skin/frontend/enterprise -skin/install/ -var/ +#--------------------------# +# Magento Default Files # +#--------------------------# + +/app/etc/local.xml +/media/* +!/media/.htaccess +!/media/customer/.htaccess +!/media/dhl/logo.jpg +!/media/downloadable/.htaccess +!/media/xmlconnect/custom/ok.gif +!/media/xmlconnect/original/ok.gif +!/media/xmlconnect/system/ok.gif +/var/* +!/var/.htaccess +!/var/package/*.xml diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index 9a439fcd988..38ac77e405e 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -21,6 +21,9 @@ coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt +# Bower dependency directory (https://bower.io/) +bower_components + # node-waf configuration .lock-wscript @@ -28,8 +31,11 @@ coverage build/Release # Dependency directories -node_modules -jspm_packages +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ # Optional npm cache directory .npm @@ -46,3 +52,6 @@ jspm_packages # Yarn Integrity file .yarn-integrity +# dotenv environment variables file +.env + diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore index 58c51ecaed4..af90c007a3f 100644 --- a/vendor/gitignore/Objective-C.gitignore +++ b/vendor/gitignore/Objective-C.gitignore @@ -19,7 +19,8 @@ xcuserdata/ ## Other *.moved-aside -*.xcuserstate +*.xccheckout +*.xcscmblueprint ## Obj-C/Swift specific *.hmap diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore index d41364ab18e..9bf1537f6ae 100644 --- a/vendor/gitignore/Perl.gitignore +++ b/vendor/gitignore/Perl.gitignore @@ -4,6 +4,7 @@ /META.json /MYMETA.* *.o +*.pm.tdy *.bs # Devel::Cover diff --git a/vendor/gitignore/PureScript.gitignore b/vendor/gitignore/PureScript.gitignore new file mode 100644 index 00000000000..361cf5277ba --- /dev/null +++ b/vendor/gitignore/PureScript.gitignore @@ -0,0 +1,8 @@ +# Dependencies +.psci_modules +bower_components +node_modules + +# Generated files +.psci +output diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index 9a05e2debe5..cf3102d6b00 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -80,7 +80,7 @@ celerybeat-schedule .env # virtualenv -.venv/ +.venv venv/ ENV/ diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore index a02d882cb88..006a7b247fe 100644 --- a/vendor/gitignore/Scala.gitignore +++ b/vendor/gitignore/Scala.gitignore @@ -13,6 +13,8 @@ project/boot/ project/plugins/project/ # Scala-IDE specific +.ensime +.ensime_cache/ .scala_dependencies .worksheet diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore index 2c22487b5e3..099d22ae2f4 100644 --- a/vendor/gitignore/Swift.gitignore +++ b/vendor/gitignore/Swift.gitignore @@ -19,7 +19,8 @@ xcuserdata/ ## Other *.moved-aside -*.xcuserstate +*.xccheckout +*.xcscmblueprint ## Obj-C/Swift specific *.hmap @@ -35,6 +36,7 @@ playground.xcworkspace # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ +# Package.pins .build/ # CocoaPods diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore index 1c10388911b..b829399ae85 100644 --- a/vendor/gitignore/Unity.gitignore +++ b/vendor/gitignore/Unity.gitignore @@ -5,6 +5,9 @@ /[Bb]uilds/ /Assets/AssetStoreTools* +# Visual Studio 2015 cache directory +/.vs/ + # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ .consulo/ @@ -18,6 +21,7 @@ ExportedObj/ *.pidb *.booproj *.svd +*.pdb # Unity3D generated meta files diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore index beec7b91f15..2f096001fec 100644 --- a/vendor/gitignore/UnrealEngine.gitignore +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -36,6 +36,7 @@ # These project files can be generated by the engine *.xcodeproj +*.xcworkspace *.sln *.suo *.opensdf @@ -56,6 +57,9 @@ Build/* # Don't ignore icon files in Build !Build/**/*.ico +# Built data for maps +*_BuiltData.uasset + # Configuration files generated by the Editor Saved/* diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index d9e876cfcdd..8054980d742 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -199,7 +199,6 @@ ClientBin/ *.jfm *.pfx *.publishsettings -node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components @@ -234,6 +233,10 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ # Visual Studio 6 build log *.plg @@ -271,4 +274,5 @@ __pycache__/ *.pyc # Cake - Uncomment if you are using it -# tools/ +# tools/** +# !tools/packages.config diff --git a/vendor/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore index 48e8d8f7be4..dad2b56bdda 100644 --- a/vendor/gitignore/Waf.gitignore +++ b/vendor/gitignore/Waf.gitignore @@ -1,4 +1,9 @@ -# for projects that use Waf for building: http://code.google.com/p/waf/ -.waf-* -.waf3-* -.lock-* +# For projects that use the Waf build system: https://waf.io/ +# Dot-hidden on Unix-like systems +.waf-*-*/ +.waf3-*-*/ +# Hidden directory on Windows (no dot) +waf-*-*/ +waf3-*-*/ +# Lockfile +.lock-waf_*_build diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml index 36dfc539b3b..7298ea73bab 100644 --- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml @@ -1,4 +1,4 @@ -# Explaination on the scripts: +# Explanation on the scripts: # https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md image: registry.gitlab.com/gitlab-examples/kubernetes-deploy |