diff options
author | Marcia Ramos <virtua.creative@gmail.com> | 2018-08-16 10:17:22 +0100 |
---|---|---|
committer | Marcia Ramos <virtua.creative@gmail.com> | 2018-08-16 10:17:22 +0100 |
commit | bfc0c602072bd5b9bf73e117d0808043adc9465e (patch) | |
tree | ea55f6e86fdfdcccca9c593d5441c9d2c3efd441 | |
parent | 1e23577417b3e4eb06b9aba1ec174dbe78b6b9be (diff) | |
parent | 0a666b2cb1020df4b8f7b71041b6bd109b49656a (diff) | |
download | gitlab-ce-docs-processes.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into docs-processesdocs-processes
440 files changed, 5539 insertions, 1760 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fcf47421b01..fd02d72b4c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -453,6 +453,7 @@ danger-review: - master variables: - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/ + - $CI_COMMIT_REF_NAME =~ /.*-stable(-ee)?-prepare-.*/ script: - git version - danger --fail-on-errors=true diff --git a/.gitlab/issue_templates/Acceptance_Testing.md b/.gitlab/issue_templates/Acceptance_Testing.md new file mode 100644 index 00000000000..f1fbb96ce61 --- /dev/null +++ b/.gitlab/issue_templates/Acceptance_Testing.md @@ -0,0 +1,100 @@ +## Details +- **Feature Toggle Name**: `FEATURE_NAME` +- **Required GitLab Version**: `vX.X` + +-------------------------------------------------------------------------------- + +## 1. Preparation + +- [ ] **Controllers and workers**: + 1. Please link to dashboards of the workers, and the controllers and actions that can be impacted + 2. ... + 3. ... + +## 2. Development Trial + +#### Check Dev Server Versions +- [ ] GitLab: https://dev.gitlab.org/help + +#### Enable on `dev.gitlab.org`: +- [ ] `/chatops feature set FEATURE_NAME true --dev` in [`#dev-gitlab`](https://gitlab.slack.com/messages/C6WQ87MU3) + +Then leave running while monitoring and performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) + +## 2. Staging Trial + +#### Check Staging Server Versions +- [ ] GitLab: https://staging.gitlab.com/help + +#### Enable on `staging.gitlab.com` +- [ ] `/chatops run feature set FEATURE_NAME true --staging` in [`#development`](https://gitlab.slack.com/messages/C02PF508L/) + +Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 4. Production Server Version Check + +- [ ] GitLab: https://gitlab.com/help + +## 5. Initial Impact Check + +- [ ] Enable for a subset of users, when using percentage gates: 1%. + +Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 6. Low Impact Check + +- [ ] Enable for a bigger subset of users, when using percentage gates: 10%. + +Then leave running while monitoring for at least **30 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 7. Mid Impact Trial + +- [ ] Enable for a big subset of users, when using percentage gates: 50%. + +Then leave running while monitoring for at least **12 hours** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 8. Full Impact Trial + +- [ ] Enable for all users: `/chatops run feature set FEATURE_NAME true + +Then leave running while monitoring for at least **1 week**. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) + +#### Success? + +- [ ] Remove the feature gate from the code, and close this issue with that MR. diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index c1f702e9385..64b54b171f7 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -12,7 +12,7 @@ Set the title to: `[Security] Description of the original issue` - [ ] Link to the original issue adding it to the [links section](#links) - [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org` - [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-` -- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]` +- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]` - [ ] Add a link to the MR to the [links section](#links) - [ ] Add a link to an EE MR if required - [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. @@ -22,13 +22,13 @@ Set the title to: `[Security] Description of the original issue` - [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases - [ ] At this point, it might be easy to squash the commits from the MR into one - - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation] + - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation] - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable) - [ ] Create each MR targetting the security branch `security-X-Y` - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR - [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. -[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script +[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script #### Documentation and final details @@ -68,4 +68,4 @@ Set the title to: `[Security] Description of the original issue` [security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md [RM list]: https://about.gitlab.com/release-managers/ -/label ~security +/label ~security diff --git a/.rubocop.yml b/.rubocop.yml index c8b1ce327e2..9858bbe0ddd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,6 +31,10 @@ Style/MutableConstant: - 'ee/db/post_migrate/**/*' - 'ee/db/geo/migrate/**/*' +# TODO: Move this to gitlab-styles +Style/SafeNavigation: + Enabled: false + Naming/FileName: ExpectMatchingDefinition: true Exclude: @@ -44,6 +48,8 @@ Naming/FileName: - 'qa/bin/*' - 'config/**/*' - 'lib/generators/**/*' + - 'locale/unfound_translations.rb' + - 'ee/locale/unfound_translations.rb' - 'ee/lib/generators/**/*' IgnoreExecutableScripts: true AllowedAcronyms: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8a1ca6747a8..54e3b8217d8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -706,12 +706,6 @@ Style/RescueModifier: Style/RescueStandardError: Enabled: false -# Offense count: 92 -# Cop supports --auto-correct. -# Configuration parameters: ConvertCodeThatCanStartToReturnNil. -Style/SafeNavigation: - Enabled: false - # Offense count: 8 # Cop supports --auto-correct. Style/SelfAssignment: diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 8104cabd36f..0e79152459e 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.1.0 +8.1.1 @@ -180,7 +180,7 @@ gem 'rufus-scheduler', '~> 3.4' gem 'httparty', '~> 0.13.3' # Colored output to console -gem 'rainbow', '~> 2.2' +gem 'rainbow', '~> 3.0' # Progress bar gem 'ruby-progressbar' diff --git a/Gemfile.lock b/Gemfile.lock index 1537cacaadd..b33dd75c278 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,7 +123,7 @@ GEM numerizer (~> 0.1.1) chunky_png (1.3.5) citrus (3.0.2) - coderay (1.1.1) + coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) commonmarker (0.17.8) @@ -494,7 +494,7 @@ GEM memoist (0.16.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) - method_source (0.8.2) + method_source (0.9.0) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) @@ -636,12 +636,11 @@ GEM unparser procto (0.0.3) prometheus-client-mmap (0.9.4) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.4.3) + byebug (>= 9.0, < 9.1) pry (~> 0.10) pry-rails (0.3.5) pry (>= 0.9.10) @@ -692,8 +691,7 @@ GEM activesupport (= 4.2.10) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.2.2) - rake + rainbow (3.0.0) raindrops (0.18.0) rake (12.3.1) rb-fsevent (0.10.2) @@ -811,7 +809,7 @@ GEM rubyzip (1.2.1) rufus-scheduler (3.4.0) et-orbi (~> 1.0) - rugged (0.27.2) + rugged (0.27.4) safe_yaml (1.0.4) sanitize (4.6.6) crass (~> 1.0.2) @@ -872,7 +870,6 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slack-notifier (1.5.1) - slop (3.6.0) spring (2.0.1) activesupport (>= 4.2) spring-commands-rspec (1.0.4) @@ -1136,7 +1133,7 @@ DEPENDENCIES rails (= 4.2.10) rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 4.0.9) - rainbow (~> 2.2) + rainbow (~> 3.0) raindrops (~> 0.18) rblineprof (~> 0.3.6) rbtrace (~> 0.4) @@ -1207,4 +1204,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.2 + 1.16.3 diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 39305927c0f..af70e2c1939 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -701,8 +701,7 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.2.2) - rake + rainbow (3.0.0) raindrops (0.18.0) rake (12.3.1) rb-fsevent (0.10.2) @@ -1147,7 +1146,7 @@ DEPENDENCIES rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) - rainbow (~> 2.2) + rainbow (~> 3.0) raindrops (~> 0.18) rblineprof (~> 0.3.6) rbtrace (~> 0.4) diff --git a/PROCESS.md b/PROCESS.md index eddc0dec3e5..583f36b820f 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -15,8 +15,9 @@ - [Between the 1st and the 7th](#between-the-1st-and-the-7th) - [On the 7th](#on-the-7th) - [After the 7th](#after-the-7th) -- [Regressions](#regressions) - - [How to manage a regression](#how-to-manage-a-regression) +- [Bugs](#bugs) + - [Regressions](#regressions) + - [Managing bugs](#managing-bugs) - [Release retrospective and kickoff](#release-retrospective-and-kickoff) - [Retrospective](#retrospective) - [Kickoff](#kickoff) @@ -185,7 +186,7 @@ For more information read the process for Once the stable branch is frozen, the only MRs that can be cherry-picked into the stable branch are: -* Fixes for [regressions](#regressions) +* Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section. * Fixes for security issues * Fixes or improvements to automated QA scenarios * [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release @@ -218,48 +219,59 @@ you can ask for an exception to be made. Check [this guide](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md) about how to open an exception request before opening one. -## Regressions +## Bugs -A regression for a particular monthly release is a bug that exists in that -release, but wasn't present in the release before. This includes bugs in -features that were only added in that monthly release. Every regression **must** -have the milestone of the release it was introduced in - if a regression doesn't -have a milestone, it might be 'just' a bug! +A ~bug is a defect, error, failure which causes the system to behave incorrectly or prevents it from fulfilling the product requirements. -For instance, if 10.5.0 adds a feature, and that feature doesn't work correctly, -then this is a regression in 10.5. If 10.5.1 then fixes that, but 10.5.3 somehow -reintroduces the bug, then this bug is still a regression in 10.5. +The level of impact of a ~bug can vary from blocking a whole functionality +or a feature usability bug. A bug should always be linked to a severity level. +Refer to our [severity levels](../CONTRIBUTING.md#severity-labels) -Because GitLab.com runs release candidates of new releases, a regression can be -reported in a release before its 'official' release date on the 22nd of the -month. When we say 'the most recent monthly release', this can refer to either -the version currently running on GitLab.com, or the most recent version -available in the package repositories. +Whether the bug is also a regression or not, the triage process should start as soon as possible. +Ensure that the Engineering Manager and/or the Product Manager for the relative area is involved to prioritize the work as needed. -### How to manage a regression +### Regressions -Regressions are very important, and they should be considered high priority -issues that should be solved as soon as possible, especially if they affect -users. Despite that, ~regression label itself does not imply when the issue -will be scheduled. +A ~regression implies that a previously **verified working functionality** no longer works. +Regressions are a subset of bugs. We use the ~regression label to imply that the defect caused the functionality to regress. +The label tells us that something worked before and it needs extra attention from Engineering and Product Managers to schedule/reschedule. -When a regression is found: -1. Create an issue describing the problem in the most detailed way possible -1. If possible, provide links to real examples and how to reproduce the problem +The regression label does not apply to ~bugs for new features for which functionality was **never verified as working**. +These, by definition, are not regressions. + +A regression should always have the `regression:xx.x` label on it to designate when it was introduced. + +Regressions should be considered high priority issues that should be solved as soon as possible, especially if they have severe impact on users. + +### Managing bugs + +**Prioritization:** We give higher priority to regressions on features that worked in the last recent monthly release and the current release candidates. +The two scenarios below can [bypass the exception request in the release process](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md#after-the-7th), where the affected regression version matches the current monthly release version. +* A regression which worked in the **Last monthly release** + * **Example:** In 11.0 we released a new `feature X` that is verified as working. Then in release 11.1 the feature no longer works, this is regression for 11.1. The issue should have the `regression:11.1` label. + * *Note:* When we say `the last recent monthly release`, this can refer to either the version currently running on GitLab.com, or the most recent version available in the package repositories. +* A regression which worked in the **Current release candidates** + * **Example:** In 11.1-RC3 we shipped a new feature which has been verified as working. Then in 11.1-RC5 the feature no longer works, this is regression for 11.1. The issue should have the `regression:11.1` label. + * *Note:* Because GitLab.com runs release candidates of new releases, a regression can be reported in a release before its 'official' release date on the 22nd of the month. + +When a bug is found: +1. Create an issue describing the problem in the most detailed way possible. +1. If possible, provide links to real examples and how to reproduce the problem. 1. Label the issue properly, using the [team label](../CONTRIBUTING.md#team-labels), the [subject label](../CONTRIBUTING.md#subject-labels) and any other label that may apply in the specific case -1. Add the ~bug and ~regression labels -1. Notify the respective Engineering Manager to evaluate the Severity of the regression and add a [Severity label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-severity-labels). The counterpart Product Manager is included to weigh-in on prioritization as needed to set the [Priority label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-priority-labels). -1. If the regression is either an ~S1, ~S2 or ~S3 severity, label the regression with the current milestone as it should be fixed in the current milestone. - 1. If the regression was introduced in an RC of the current release, label with ~Deliverable - 1. If the regression was introduced in the previous release, label with ~"Next Patch Release" -1. If the regression is an ~S4 severity, the regression may be scheduled for later milestones at the discretion of Engineering Manager and Product Manager. - -When a new issue is found, the fix should start as soon as possible. You can -ping the Engineering Manager or the Product Manager for the relative area to -make them aware of the issue earlier. They will analyze the priority and change -it if needed. +1. Notify the respective Engineering Manager to evaluate and apply the [Severity label](../CONTRIBUTING.md#bug-severity-labels) and [Priority label](../CONTRIBUTING.md#bug-priority-labels). +The counterpart Product Manager is included to weigh-in on prioritization as needed. +1. If the ~bug is **NOT** a regression: + 1. The Engineering Manager decides which milestone the bug will be fixed. The appropriate milestone is applied. +1. If the bug is a ~regression: + 1. Determine the release that the regression affects and add the corresponding `regression:xx.x` label. + 1. If the affected release version can't be determined, add the generic ~regression label for the time being. + 1. If the affected version `xx.x` in `regression:xx.x` is the **current release**, it's recommended to schedule the fix for the current milestone. + 1. This falls under regressions which worked in the last release and the current RCs. More detailed explanations in the **Prioritization** section above. + 1. If the affected version `xx.x` in `regression:xx.x` is older than the **current release** + 1. If the regression is an ~S1 severity, it's recommended to schedule the fix for the current milestone. We would like to fix the highest severity regression as soon as we can. + 1. If the regression is an ~S2, ~S3 or ~S4 severity, the regression may be scheduled for later milestones at the discretion of the Engineering Manager and Product Manager. ## Release retrospective and kickoff diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index a9102743bf9..109e60cbde2 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import Vue from 'vue'; import Flash from '../../flash'; -import { __ } from '../../locale'; +import { sprintf, __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; @@ -55,8 +55,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ return this.issue.labels && this.issue.labels.length; }, labelDropdownTitle() { - return this.hasLabels ? - `${this.issue.labels[0].title} ${this.issue.labels.length - 1}+ more` : 'Label'; + return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: this.issue.labels[0].title, + labelCount: this.issue.labels.length - 1 + }) : __('Label'); }, selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 050cbd8db48..ad473404c29 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */ /* global ListIssue */ +import { __ } from '~/locale'; import ListLabel from '~/vue_shared/models/label'; import ListAssignee from '~/vue_shared/models/assignee'; import queryData from '../utils/query_data'; @@ -30,7 +31,7 @@ class List { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; - this.title = obj.title; + this.title = obj.list_type === 'backlog' ? __('Open') : obj.title; this.type = obj.list_type; const typeInfo = this.getTypeInfo(this.type); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index 589eeee9695..742cf490ad2 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -8,6 +8,7 @@ import 'core-js/fn/object/assign'; import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; +import 'core-js/fn/string/includes'; import 'core-js/fn/symbol'; import 'core-js/es6/map'; import 'core-js/es6/weak-map'; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 7cc4e6a2c3a..b5b05df4d34 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -114,11 +114,15 @@ export default { this.adjustView(); }, methods: { - ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']), + ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']), fetchData() { - this.fetchDiffFiles().catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); + this.fetchDiffFiles() + .then(() => { + requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 }); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); if (!this.isNotesFetched) { eventHub.$emit('fetchNotesData'); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 7e7058d8d08..59e9ba08b8b 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -46,16 +46,25 @@ export default { showExpandMessage() { return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge; }, + showLoadingIcon() { + return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); + }, }, methods: { ...mapActions('diffs', ['loadCollapsedDiff']), handleToggle() { const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file; - if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) { + if ( + collapsed && + !highlightedDiffLines && + parallelDiffLines !== undefined && + !parallelDiffLines.length + ) { this.handleLoadCollapsedDiff(); } else { this.file.collapsed = !this.file.collapsed; + this.file.renderIt = true; } }, handleLoadCollapsedDiff() { @@ -65,6 +74,7 @@ export default { .then(() => { this.isLoadingCollapsedDiff = false; this.file.collapsed = false; + this.file.renderIt = true; }) .catch(() => { this.isLoadingCollapsedDiff = false; @@ -121,12 +131,12 @@ export default { </div> <diff-content - v-if="!isCollapsed" + v-if="!isCollapsed && file.renderIt" :class="{ hidden: isCollapsed || file.tooLarge }" :diff-file="file" /> <loading-icon - v-if="isLoadingCollapsedDiff" + v-else-if="showLoadingIcon" class="diff-content loading" /> <div diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 2fa8367f528..f68afa44837 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -25,3 +25,6 @@ export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded'; export const UNFOLD_COUNT = 20; export const COUNT_OF_AVATARS_IN_GUTTER = 3; export const LENGTH_OF_AVATAR_TOOLTIP = 17; + +export const LINES_TO_BE_RENDERED_DIRECTLY = 100; +export const MAX_LINES_TO_BE_RENDERED = 2000; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 27001142257..4ab6ceb249a 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -29,6 +29,27 @@ export const fetchDiffFiles = ({ state, commit }) => { .then(handleLocationHash); }; +export const startRenderDiffsQueue = ({ state, commit }) => { + const checkItem = () => { + const nextFile = state.diffFiles.find( + file => !file.renderIt && (!file.collapsed || !file.text), + ); + if (nextFile) { + requestAnimationFrame(() => { + commit(types.RENDER_FILE, nextFile); + }); + requestIdleCallback( + () => { + checkItem(); + }, + { timeout: 1000 }, + ); + } + }; + + checkItem(); +}; + export const setInlineDiffViewType = ({ commit }) => { commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 2c8e1a1466f..c999d637d50 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -8,3 +8,4 @@ export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; +export const RENDER_FILE = 'RENDER_FILE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index a98b2be89a3..0522e32c410 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import _ from 'underscore'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils'; +import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants'; import * as types from './mutation_types'; export default { @@ -15,8 +16,48 @@ export default { }, [types.SET_DIFF_DATA](state, data) { + const diffData = convertObjectPropsToCamelCase(data, { deep: true }); + let showingLines = 0; + const filesLength = diffData.diffFiles.length; + let i; + for (i = 0; i < filesLength; i += 1) { + const file = diffData.diffFiles[i]; + if (file.parallelDiffLines) { + const linesLength = file.parallelDiffLines.length; + let u = 0; + for (u = 0; u < linesLength; u += 1) { + const line = file.parallelDiffLines[u]; + if (line.left) delete line.left.text; + if (line.right) delete line.right.text; + } + } + + if (file.highlightedDiffLines) { + const linesLength = file.highlightedDiffLines.length; + let u; + for (u = 0; u < linesLength; u += 1) { + const line = file.highlightedDiffLines[u]; + delete line.text; + } + } + + if (file.highlightedDiffLines) { + showingLines += file.parallelDiffLines.length; + } + Object.assign(file, { + renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, + collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, + }); + } + Object.assign(state, { - ...convertObjectPropsToCamelCase(data, { deep: true }), + ...diffData, + }); + }, + + [types.RENDER_FILE](state, file) { + Object.assign(file, { + renderIt: true, }); }, diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 8c1861c56db..651169391fe 100644 --- a/app/assets/javascripts/emoji/support/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) { canvas.height = numTestEntries * fontSize; ctx.fillStyle = '#000000'; ctx.textBaseline = 'middle'; - ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; + ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; // Write each emoji to the canvas vertically let writeIndex = 0; testMapKeys.forEach(testKey => { diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 5611b37be7c..00ae5ea2c15 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -61,7 +61,7 @@ export default { <slot name="header"></slot> </header> <div - class="ide-tree-body" + class="ide-tree-body h-100" > <repo-file v-for="file in currentTree.tree" diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue index ff114e47741..aa5fce59dbf 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/button.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -1,7 +1,11 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, components: { Icon, }, @@ -26,6 +30,11 @@ export default { default: true, }, }, + computed: { + tooltipTitle() { + return this.showLabel ? '' : this.label; + }, + }, methods: { clicked() { this.$emit('click'); @@ -36,7 +45,9 @@ export default { <template> <button + v-tooltip :aria-label="label" + :title="tooltipTitle" type="button" class="btn-blank" @click.stop.prevent="clicked" diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index fef36eae7b1..39a1bd1f61b 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -2,6 +2,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import { Manager } from 'smooshpack'; +import { listen } from 'codesandbox-api'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Navigator from './navigator.vue'; import { packageJsonPath } from '../../constants'; @@ -16,6 +17,7 @@ export default { return { manager: {}, loading: false, + sandpackReady: false, }; }, computed: { @@ -81,6 +83,10 @@ export default { } this.manager = {}; + if (this.listener) { + this.listener(); + } + clearTimeout(this.timeout); this.timeout = null; }, @@ -96,17 +102,29 @@ export default { return this.loadFileContent(this.mainEntry) .then(() => this.$nextTick()) - .then(() => + .then(() => { this.initManager('#ide-preview', this.sandboxOpts, { fileResolver: { isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]), readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content), }, - }), - ); + }); + + this.listener = listen(e => { + switch (e.type) { + case 'done': + this.sandpackReady = true; + break; + default: + break; + } + }); + }); }, update() { - if (this.timeout) return; + if (!this.sandpackReady) return; + + clearTimeout(this.timeout); this.timeout = setTimeout(() => { if (_.isEmpty(this.manager)) { @@ -116,10 +134,7 @@ export default { } this.manager.updatePreview(this.sandboxOpts); - - clearTimeout(this.timeout); - this.timeout = null; - }, 500); + }, 250); }, initManager(el, opts, resolver) { this.manager = new Manager(el, opts, resolver); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index c6d7d218e81..3f6101e58f4 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -3,7 +3,6 @@ import VueRouter from 'vue-router'; import { join as joinPath } from 'path'; import flash from '~/flash'; import store from './stores'; -import { activityBarViews } from './constants'; Vue.use(VueRouter); @@ -74,98 +73,23 @@ router.beforeEach((to, from, next) => { projectId: to.params.project, }) .then(() => { - const fullProjectId = `${to.params.namespace}/${to.params.project}`; - + const basePath = to.params[0] || ''; + const projectId = `${to.params.namespace}/${to.params.project}`; const branchId = to.params.branchid; + const mergeRequestId = to.params.mrid; if (branchId) { - const basePath = to.params[0] || ''; - - store.dispatch('setCurrentBranchId', branchId); - - store.dispatch('getBranchData', { - projectId: fullProjectId, + store.dispatch('openBranch', { + projectId, branchId, + basePath, + }); + } else if (mergeRequestId) { + store.dispatch('openMergeRequest', { + projectId, + mergeRequestId, + targetProjectId: to.query.target_project, }); - - store - .dispatch('getFiles', { - projectId: fullProjectId, - branchId, - }) - .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(store.state.entries).find( - key => key === path && !store.state.entries[key].pending, - ); - const treeEntry = store.state.entries[treeEntryKey]; - - if (treeEntry) { - store.dispatch('handleTreeEntryAction', treeEntry); - } - } - }) - .catch(e => { - throw e; - }); - } else if (to.params.mrid) { - store - .dispatch('getMergeRequestData', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }) - .then(mr => { - store.dispatch('updateActivityBarView', activityBarViews.review); - - store.dispatch('getBranchData', { - projectId: fullProjectId, - branchId: mr.source_branch, - }); - - return store.dispatch('getFiles', { - projectId: fullProjectId, - branchId: mr.source_branch, - }); - }) - .then(() => - store.dispatch('getMergeRequestVersions', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }), - ) - .then(() => - store.dispatch('getMergeRequestChanges', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }), - ) - .then(mrChanges => { - mrChanges.changes.forEach((change, ind) => { - const changeTreeEntry = store.state.entries[change.new_path]; - - if (changeTreeEntry) { - store.dispatch('setFileMrChange', { - file: changeTreeEntry, - mrChange: change, - }); - - if (ind < 10) { - store.dispatch('getFileData', { - path: change.new_path, - makeFileActive: ind === 0, - }); - } - } - }); - }) - .catch(e => { - flash('Error while loading the merge request. Please try again.'); - throw e; - }); } }) .catch(e => { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 9e3f5da4676..c9795750d65 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -54,9 +54,6 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { commit(types.SET_FILE_ACTIVE, { path, active: true }); dispatch('scrollToTab'); - - commit(types.SET_CURRENT_PROJECT, file.projectId); - commit(types.SET_CURRENT_BRANCH, file.branchId); }; export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 1887b77b00b..187f8c75d07 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -1,6 +1,8 @@ -import { __ } from '../../../locale'; +import flash from '~/flash'; +import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; +import { activityBarViews } from '../../constants'; export const getMergeRequestData = ( { commit, dispatch, state }, @@ -104,3 +106,67 @@ export const getMergeRequestVersions = ( resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions); } }); + +export const openMergeRequest = ( + { dispatch, state }, + { projectId, targetProjectId, mergeRequestId } = {}, +) => + dispatch('getMergeRequestData', { + projectId, + targetProjectId, + mergeRequestId, + }) + .then(mr => { + dispatch('setCurrentBranchId', mr.source_branch); + + dispatch('getBranchData', { + projectId, + branchId: mr.source_branch, + }); + + return dispatch('getFiles', { + projectId, + branchId: mr.source_branch, + }); + }) + .then(() => + dispatch('getMergeRequestVersions', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(() => + dispatch('getMergeRequestChanges', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(mrChanges => { + if (mrChanges.changes.length) { + dispatch('updateActivityBarView', activityBarViews.review); + } + + mrChanges.changes.forEach((change, ind) => { + const changeTreeEntry = state.entries[change.new_path]; + + if (changeTreeEntry) { + dispatch('setFileMrChange', { + file: changeTreeEntry, + mrChange: change, + }); + + if (ind < 10) { + dispatch('getFileData', { + path: change.new_path, + makeFileActive: ind === 0, + }); + } + } + }); + }) + .catch(e => { + flash(__('Error while loading the merge request. Please try again.')); + throw e; + }); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 501e25d452b..543dc6c0461 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -124,3 +124,35 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { actionPayload: branchId, }); }; + +export const openBranch = ( + { dispatch, state }, + { projectId, branchId, basePath }, +) => { + dispatch('setCurrentBranchId', branchId); + + dispatch('getBranchData', { + projectId, + branchId, + }); + + return ( + dispatch('getFiles', { + projectId, + branchId, + }) + .then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; + + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } + } + }) + ); +}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js index 74aa98ef9f9..f90c2d77f2b 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/actions.js +++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js @@ -33,7 +33,4 @@ export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => { export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES); -export const openBranch = ({ rootState, dispatch }, id) => - dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true }); - export default () => {}; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 1eda5768709..56a8d9430c7 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -200,6 +200,7 @@ export default { }, [types.DELETE_ENTRY](state, path) { const entry = state.entries[path]; + const { tempFile = false } = entry; const parent = entry.parentPath ? state.entries[entry.parentPath] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; @@ -209,7 +210,11 @@ export default { parent.tree = parent.tree.filter(f => f.path !== entry.path); if (entry.type === 'blob') { - state.changedFiles = state.changedFiles.concat(entry); + if (tempFile) { + state.changedFiles = state.changedFiles.filter(f => f.path !== path); + } else { + state.changedFiles = state.changedFiles.concat(entry); + } } }, [types.RENAME_ENTRY](state, { path, name, entryPath = null }) { diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue new file mode 100644 index 00000000000..525c5eec91a --- /dev/null +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -0,0 +1,98 @@ +<script> + import TimeagoTooltiop from '~/vue_shared/components/time_ago_tooltip.vue'; + + export default { + components: { + TimeagoTooltiop, + }, + props: { + // @build.artifacts_expired? + haveArtifactsExpired: { + type: Boolean, + required: true, + }, + // @build.has_expiring_artifacts? + willArtifactsExpire: { + type: Boolean, + required: true, + }, + expireAt: { + type: String, + required: false, + default: null, + }, + keepArtifactsPath: { + type: String, + required: false, + default: null, + }, + downloadArtifactsPath: { + type: String, + required: false, + default: null, + }, + browseArtifactsPath: { + type: String, + required: false, + default: null, + }, + }, + }; +</script> +<template> + <div class="block"> + <div class="title"> + {{ s__('Job|Job artifacts') }} + </div> + + <p + v-if="haveArtifactsExpired" + class="js-artifacts-removed build-detail-row" + > + {{ s__('Job|The artifacts were removed') }} + </p> + <p + v-else-if="willArtifactsExpire" + class="js-artifacts-will-be-removed build-detail-row" + > + {{ s__('Job|The artifacts will be removed') }} + </p> + + <timeago-tooltiop + v-if="expireAt" + :time="expireAt" + /> + + <div + class="btn-group d-flex" + role="group" + > + <a + v-if="keepArtifactsPath" + :href="keepArtifactsPath" + class="js-keep-artifacts btn btn-sm btn-default" + data-method="post" + > + {{ s__('Job|Keep') }} + </a> + + <a + v-if="downloadArtifactsPath" + :href="downloadArtifactsPath" + class="js-download-artifacts btn btn-sm btn-default" + download + rel="nofollow" + > + {{ s__('Job|Download') }} + </a> + + <a + v-if="browseArtifactsPath" + :href="browseArtifactsPath" + class="js-browse-artifacts btn btn-sm btn-default" + > + {{ s__('Job|Browse') }} + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue new file mode 100644 index 00000000000..d688eebfa95 --- /dev/null +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -0,0 +1,48 @@ +<script> +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + components: { + TimeagoTooltip, + }, + props: { + erasedByUser: { + type: Boolean, + required: true, + }, + username: { + type: String, + required: false, + default: null, + }, + linkToUser: { + type: String, + required: false, + default: null, + }, + erasedAt: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="prepend-top-default js-build-erased"> + <div class="erased alert alert-warning"> + <template v-if="erasedByUser"> + {{ s__("Job|Job has been erased by") }} + <a :href="linkToUser"> + {{ username }} + </a> + </template> + <template v-else> + {{ s__("Job|Job has been erased") }} + </template> + + <timeago-tooltip + :time="erasedAt" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue new file mode 100644 index 00000000000..3c4749d996b --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -0,0 +1,33 @@ +<script> + export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, + }, + isReceivingBuildTrace: { + type: Boolean, + required: true, + }, + }, + }; +</script> +<template> + <pre class="build-trace"> + <code + class="bash" + v-html="trace" + > + </code> + + <div + v-if="isReceivingBuildTrace" + class="js-log-animation build-loader-animation" + > + <div class="dot"></div> + <div class="dot"></div> + <div class="dot"></div> + </div> + </pre> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index d2adf628050..36d4a3e2bc9 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -1,14 +1,16 @@ <script> -import detailRow from './sidebar_detail_row.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import timeagoMixin from '../../vue_shared/mixins/timeago'; -import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import Icon from '~/vue_shared/components/icon.vue'; +import DetailRow from './sidebar_detail_row.vue'; export default { name: 'SidebarDetailsBlock', components: { - detailRow, - loadingIcon, + DetailRow, + LoadingIcon, + Icon, }, mixins: [timeagoMixin], props: { @@ -20,16 +22,16 @@ export default { type: Boolean, required: true, }, - canUserRetry: { - type: Boolean, - required: false, - default: false, - }, runnerHelpUrl: { type: String, required: false, default: '', }, + terminalPath: { + type: String, + required: false, + default: null, + }, }, computed: { shouldRenderContent() { @@ -92,7 +94,7 @@ export default { {{ job.name }} </strong> <a - v-if="canUserRetry" + v-if="job.retry_path" :class="retryButtonClass" :href="job.retry_path" data-method="post" @@ -100,6 +102,16 @@ export default { > {{ __('Retry') }} </a> + <a + v-if="terminalPath" + :href="terminalPath" + class="js-terminal-link pull-right btn btn-primary + btn-inverted visible-md-block visible-lg-block" + target="_blank" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> <button :aria-label="__('Toggle Sidebar')" type="button" @@ -125,7 +137,7 @@ export default { {{ __('New issue') }} </a> <a - v-if="canUserRetry" + v-if="job.retry_path" :href="job.retry_path" class="js-retry-job btn btn-inverted-secondary" data-method="post" diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue new file mode 100644 index 00000000000..18883fea950 --- /dev/null +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -0,0 +1,63 @@ +<script> +/** + * Renders Stuck Runners block for job's view. + */ +export default { + props: { + hasNoRunnersForProject: { + type: Boolean, + required: true, + }, + tags: { + type: Array, + required: false, + default: () => [], + }, + runnersPath: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="bs-callout bs-callout-warning"> + <p + v-if="hasNoRunnersForProject" + class="js-stuck-no-runners" + > + {{ s__(`Job|This job is stuck, because the project + doesn't have any runners online assigned to it.`) }} + </p> + <p + v-else-if="tags.length" + class="js-stuck-with-tags" + > + {{ s__(`This job is stuck, because you don't have + any active runners online with any of these tags assigned to them:`) }} + <span + v-for="(tag, index) in tags" + :key="index" + class="badge badge-primary" + > + {{ tag }} + </span> + </p> + <p + v-else + class="js-stuck-no-active-runner" + > + {{ s__(`This job is stuck, because you don't + have any active runners that can run this job.`) }} + </p> + + {{ __("Go to") }} + <a + v-if="runnersPath" + :href="runnersPath" + class="js-runners-path" + > + {{ __("Runners page") }} + </a> + </div> +</template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 0db7b95636c..a84324f14b2 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -52,9 +52,9 @@ export default () => { return createElement('details-block', { props: { isLoading: this.mediator.state.isLoading, - canUserRetry: !!('canUserRetry' in detailsBlockDataset), job: this.mediator.store.state.job, runnerHelpUrl: dataset.runnerHelpUrl, + terminalPath: detailsBlockDataset.terminalPath, }, }); }, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index cb851ff6745..6499b919787 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; -import { __ } from './locale'; +import { sprintf, __ } from './locale'; import axios from './lib/utils/axios_utils'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; @@ -39,7 +39,7 @@ export default class LabelsSelect { showNo = $dropdown.data('showNo'); showAny = $dropdown.data('showAny'); showMenuAbove = $dropdown.data('showMenuAbove'); - defaultLabel = $dropdown.data('defaultLabel') || 'Label'; + defaultLabel = $dropdown.data('defaultLabel') || __('Label'); abilityName = $dropdown.data('abilityName'); $selectbox = $dropdown.closest('.selectbox'); $block = $selectbox.closest('.block'); @@ -267,7 +267,10 @@ export default class LabelsSelect { return selectedLabels; } else if (selectedLabels.length) { - return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; + return sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: selectedLabels[0], + labelCount: selectedLabels.length - 1 + }); } else { return defaultLabel; diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 48d75f5443b..47bd70537f1 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,6 +1,8 @@ import initSettingsPanels from '~/settings_panels'; +import projectSelect from '~/project_select'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); + projectSelect(); }); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index ffc84dc106b..78cf5406e43 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,3 +1,9 @@ import initForm from '../form'; +import MirrorRepos from './mirror_repos'; -document.addEventListener('DOMContentLoaded', initForm); +document.addEventListener('DOMContentLoaded', () => { + initForm(); + + const mirrorReposContainer = document.querySelector('.js-mirror-settings'); + if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init(); +}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js b/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js new file mode 100644 index 00000000000..4c56af20cc3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js @@ -0,0 +1,94 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import { __ } from '~/locale'; +import Flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; + +export default class MirrorRepos { + constructor(container) { + this.$container = $(container); + this.$form = $('.js-mirror-form', this.$container); + this.$urlInput = $('.js-mirror-url', this.$form); + this.$protectedBranchesInput = $('.js-mirror-protected', this.$form); + this.$table = $('.js-mirrors-table-body', this.$container); + this.mirrorEndpoint = this.$form.data('projectMirrorEndpoint'); + } + + init() { + this.initMirrorPush(); + this.registerUpdateListeners(); + } + + initMirrorPush() { + this.$passwordGroup = $('.js-password-group', this.$container); + this.$password = $('.js-password', this.$passwordGroup); + this.$authMethod = $('.js-auth-method', this.$form); + + this.$authMethod.on('change', () => this.togglePassword()); + this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl()); + } + + updateUrl() { + let val = this.$urlInput.val(); + + if (this.$password) { + const password = this.$password.val(); + if (password) val = val.replace('@', `:${password}@`); + } + + $('.js-mirror-url-hidden', this.$form).val(val); + } + + updateProtectedBranches() { + const val = this.$protectedBranchesInput.get(0).checked + ? this.$protectedBranchesInput.val() + : '0'; + $('.js-mirror-protected-hidden', this.$form).val(val); + } + + registerUpdateListeners() { + this.debouncedUpdateUrl = _.debounce(() => this.updateUrl(), 200); + this.$urlInput.on('input', () => this.debouncedUpdateUrl()); + this.$protectedBranchesInput.on('change', () => this.updateProtectedBranches()); + this.$table.on('click', '.js-delete-mirror', event => this.deleteMirror(event)); + } + + togglePassword() { + const isPassword = this.$authMethod.val() === 'password'; + + if (!isPassword) { + this.$password.val(''); + this.updateUrl(); + } + this.$passwordGroup.collapse(isPassword ? 'show' : 'hide'); + } + + deleteMirror(event, existingPayload) { + const $target = $(event.currentTarget); + let payload = existingPayload; + + if (!payload) { + payload = { + project: { + remote_mirrors_attributes: { + id: $target.data('mirrorId'), + enabled: 0, + }, + }, + }; + } + + return axios + .put(this.mirrorEndpoint, payload) + .then(() => this.removeRow($target)) + .catch(() => Flash(__('Failed to remove mirror.'))); + } + + /* eslint-disable class-methods-use-this */ + removeRow($target) { + const row = $target.closest('tr'); + $('.js-delete-mirror', row).tooltip('hide'); + row.remove(); + } + /* eslint-enable class-methods-use-this */ +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index bce7556bd40..6f3b32f8eea 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -14,6 +14,7 @@ export default function projectSelect() { this.orderBy = $(select).data('orderBy') || 'id'; this.withIssuesEnabled = $(select).data('withIssuesEnabled'); this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); + this.allowClear = $(select).data('allowClear') || false; placeholder = "Search for project"; if (this.includeGroups) { @@ -71,6 +72,13 @@ export default function projectSelect() { text: function (project) { return project.name_with_namespace || project.name; }, + + initSelection: function(el, callback) { + return Api.project(el.val()).then(({ data }) => callback(data)); + }, + + allowClear: this.allowClear, + dropdownCssClass: "ajax-project-dropdown" }); if (simpleFilter) return select; diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 140475b4dfa..7b37f4e9a97 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,10 +1,10 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; - import { componentNames } from '~/vue_shared/components/reports/issue_body'; - import ReportSection from '~/vue_shared/components/reports/report_section.vue'; - import SummaryRow from '~/vue_shared/components/reports/summary_row.vue'; - import IssuesList from '~/vue_shared/components/reports/issues_list.vue'; + import { componentNames } from './issue_body'; + import ReportSection from './report_section.vue'; + import SummaryRow from './summary_row.vue'; + import IssuesList from './issues_list.vue'; import Modal from './modal.vue'; import createStore from '../store'; import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js index 54dfb7b16bf..8b5af263d50 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issue_body.js +++ b/app/assets/javascripts/reports/components/issue_body.js @@ -1,4 +1,4 @@ -import TestIssueBody from '~/reports/components/test_issue_body.vue'; +import TestIssueBody from './test_issue_body.vue'; export const components = { TestIssueBody, diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index f8189117ac3..85811698a37 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,11 +1,10 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; - import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS, -} from '~/vue_shared/components/reports/constants'; +} from '../constants'; export default { name: 'IssueStatusIcon', diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index 2545e84f932..df42201b5de 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,10 +1,10 @@ <script> -import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue'; +import IssuesBlock from '~/reports/components/report_issues.vue'; import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL, -} from '~/vue_shared/components/reports/constants'; +} from '~/reports/constants'; /** * Renders block of issues diff --git a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue index 4f81cee2a38..4f81cee2a38 100644 --- a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue +++ b/app/assets/javascripts/reports/components/modal_open_name.vue diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/reports/components/report_issues.vue index 1f13e555b31..c553a374f66 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_issues.vue @@ -1,6 +1,6 @@ <script> -import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue'; -import { components, componentNames } from '~/vue_shared/components/reports/issue_body'; +import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; +import { components, componentNames } from '~/reports/components/issue_body'; export default { name: 'ReportIssues', diff --git a/app/assets/javascripts/vue_shared/components/reports/report_link.vue b/app/assets/javascripts/reports/components/report_link.vue index 74d68f9f439..74d68f9f439 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_link.vue +++ b/app/assets/javascripts/reports/components/report_link.vue diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index a6dbf21092b..dc609d6f90e 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -1,8 +1,8 @@ <script> import { __ } from '~/locale'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import Popover from '~/vue_shared/components/help_popover.vue'; import IssuesList from './issues_list.vue'; -import Popover from '../help_popover.vue'; const LOADING = 'LOADING'; const ERROR = 'ERROR'; diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 063beab58fc..4456d84c968 100644 --- a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -1,7 +1,7 @@ <script> import CiIcon from '~/vue_shared/components/ci_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; -import Popover from '../help_popover.vue'; +import Popover from '~/vue_shared/components/help_popover.vue'; /** * Renders the summary row for each report diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js index 807ecb1039e..c323dc543f3 100644 --- a/app/assets/javascripts/reports/constants.js +++ b/app/assets/javascripts/reports/constants.js @@ -11,6 +11,8 @@ export const SUCCESS = 'SUCCESS'; export const STATUS_FAILED = 'failed'; export const STATUS_SUCCESS = 'success'; +export const STATUS_NEUTRAL = 'neutral'; + export const ICON_WARNING = 'warning'; export const ICON_SUCCESS = 'success'; export const ICON_NOTFOUND = 'notfound'; diff --git a/app/assets/javascripts/vue_shared/components/reports/constants.js b/app/assets/javascripts/vue_shared/components/reports/constants.js deleted file mode 100644 index dbde648bfdb..00000000000 --- a/app/assets/javascripts/vue_shared/components/reports/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const STATUS_FAILED = 'failed'; -export const STATUS_SUCCESS = 'success'; -export const STATUS_NEUTRAL = 'neutral'; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index c20738a20c3..e8e707cf90c 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -14,7 +14,6 @@ $border-radius-base: 3px !default; $modal-body-bg: $white-light; $input-border: $border-color; -$input-border-focus: $focus-border-color; $padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; @@ -86,7 +85,7 @@ strong { } a { - color: $gl-link-color; + color: $blue-600; } hr { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c46b0b5db09..b1a20c06910 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,4 +1,5 @@ @import 'framework/variables'; +@import 'framework/variables_overrides'; @import 'framework/mixins'; @import 'bootstrap'; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 369556dc24e..4c7c399a3ca 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -103,6 +103,7 @@ display: flex; a { + width: 100%; display: flex; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 646cedd79ed..0dc7aa4ef68 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -350,7 +350,7 @@ &:focus { cursor: text; box-shadow: none; - border-color: lighten($dropdown-input-focus-border, 20%); + border-color: lighten($blue-300, 20%); color: $gray-darkest; background-color: $gray-light; } @@ -434,7 +434,7 @@ &:hover, &:active, &:focus { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } @@ -445,7 +445,7 @@ &:hover, &:active, &:focus { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index af17210f341..48a87ea8616 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -114,7 +114,11 @@ hr { .item-title { font-weight: $gl-font-weight-bold; } .author-link { - color: $gl-link-color; + color: $blue-600; +} + +.author-link:hover { + text-decoration: none; } .back-link { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index eebce8b9011..83bc3776178 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -615,7 +615,7 @@ &:focus { color: $dropdown-link-color; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: 0 0 4px $dropdown-input-focus-shadow; ~ .fa { diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 3cde0490371..a8ec1e1145a 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -2,7 +2,7 @@ gl-emoji { font-style: normal; display: inline-flex; vertical-align: middle; - font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1.5em; line-height: 0.9; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 00eac1688f2..54882633fea 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -286,19 +286,19 @@ span.idiff { .new-file { a { - color: $gl-text-green; + color: $green-600; } } .renamed-file { a { - color: $gl-text-orange; + color: $orange-600; } } .deleted-file { a { - color: $gl-text-red; + color: $red-500; } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 5d79610b21e..9b09ed0ed0a 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -205,7 +205,7 @@ &.focus, &.focus:hover { - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: 0 0 4px $search-input-focus-shadow-color; } @@ -294,7 +294,7 @@ &:hover, &:focus { color: $gl-text-color; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; outline: none; } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index e4bcb92876d..7a4c3914fb0 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -16,10 +16,10 @@ color: $gl-text-color; a { - color: $gl-link-color; + color: $blue-600; &:hover { - color: $gl-link-hover-color; + color: $blue-800; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index d7149d93622..a70eece8f68 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -170,7 +170,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } .input-group { @@ -201,7 +201,7 @@ label { } .gl-show-field-errors { - .form-control { + .form-control:not(textarea) { height: 34px; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 7290a174668..d8391b59a8c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -179,7 +179,7 @@ &:hover, &:focus { svg { - fill: $gl-link-color; + fill: $blue-600; } } } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index ffb40166c15..7d53a631cdf 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -79,7 +79,6 @@ body.modal-open { .modal { background-color: $black-transparent; - z-index: 2100; @include media-breakpoint-up(md) { .modal-dialog { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index b40dcf93969..88d2f0aaf85 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -153,7 +153,7 @@ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { - border-color: $input-border-focus; + border-color: $blue-300; } &.select2-active { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 473ca408c04..eccc814b747 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -180,7 +180,7 @@ } a > code { - color: $gl-link-color; + color: $blue-600; } dd { @@ -423,25 +423,25 @@ h4 { input, textarea { &::-webkit-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } // support firefox 19+ vendor prefix &::-moz-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; opacity: 1; // FF defaults to 0.54 } // scss-lint:disable PseudoElement // support Edge vendor prefix &::-ms-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } // scss-lint:disable PseudoElement // support IE vendor prefix &:-ms-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4db9efff6ee..866cb88ba5b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -177,7 +177,6 @@ $border-gray-dark: darken($white-normal, $darken-border-factor); * UI elements */ $border-color: #e5e5e5; -$focus-border-color: $blue-300; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -196,19 +195,10 @@ $gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85); $gl-text-color-disabled: #919191; -$gl-text-green: $green-600; -$gl-text-green-hover: $green-700; -$gl-text-red: $red-500; -$gl-text-orange: $orange-600; -$gl-link-color: $blue-600; -$gl-link-hover-color: $blue-800; $gl-grayish-blue: #7f8fa4; -$gl-gray: $gl-text-color; $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; -$gl-header-nav-hover-color: #434343; -$placeholder-text-color: $gl-text-color-tertiary; /* * Lists @@ -227,7 +217,7 @@ $list-warning-row-color: $orange-700; /* * Markdown */ -$md-link-color: $gl-link-color; +$md-link-color: $blue-600; $md-area-border: #ddd; /* @@ -374,7 +364,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, - 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* * Dropdowns @@ -392,8 +382,7 @@ $dropdown-divider-color: rgba(#000, 0.1); $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; -$dropdown-input-focus-border: $focus-border-color; -$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, 0.4); +$dropdown-input-focus-shadow: rgba($blue-300, 0.4); $dropdown-loading-bg: rgba(#fff, 0.6); $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); @@ -585,8 +574,8 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0); $common-gray: $gl-text-color; $common-gray-light: #bbb; $common-gray-dark: #444; -$common-red: $gl-text-red; -$common-green: $gl-text-green; +$common-red: $red-500; +$common-green: $green-600; /* * Editor @@ -834,19 +823,3 @@ Prometheus $prometheus-table-row-highlight-color: $theme-gray-100; $priority-label-empty-state-width: 114px; - -/* - * Override Bootstrap 4 variables - */ - -$secondary: $gray-light; -$input-disabled-bg: $gray-light; -$input-border-color: $theme-gray-200; -$input-color: $gl-text-color; -$font-family-sans-serif: $regular-font; -$font-family-monospace: $monospace-font; -$input-line-height: 20px; -$btn-line-height: 20px; -$table-accent-bg: $gray-light; -$card-border-color: $border-color; -$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss new file mode 100644 index 00000000000..b9c343fa2e9 --- /dev/null +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -0,0 +1,16 @@ +/* + * This file is only for overriding Bootstrap 4 variables. + * Please add any new variables to variables.scss + */ + +$secondary: $gray-light; +$input-disabled-bg: $gray-light; +$input-border-color: $theme-gray-200; +$input-color: $gl-text-color; +$font-family-sans-serif: $regular-font; +$font-family-monospace: $monospace-font; +$input-line-height: 20px; +$btn-line-height: 20px; +$table-accent-bg: $gray-light; +$card-border-color: $border-color; +$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index dbd3144b9b4..f2d296fb875 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -44,7 +44,7 @@ color: $gl-text-color-secondary; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2b8163b8c68..af91497d0ea 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1158,7 +1158,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } a { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index a68b47b1d02..91f470ca709 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -225,7 +225,7 @@ outline: 0; &:hover { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index e8158cd7f6b..1696d18584d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -221,7 +221,7 @@ color: $gl-text-color; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index bce83bf0dd0..10764e0f3df 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -279,7 +279,7 @@ } &.autodevops-link { - color: $gl-link-color; + color: $blue-600; } } @@ -321,7 +321,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; } .commit-row-message { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index e2c0a7a6225..bba9f38d3dd 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -360,7 +360,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; line-height: 1.3; vertical-align: top; font-weight: $gl-font-weight-normal; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 591e21243ed..47778110bae 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -511,13 +511,13 @@ padding: 0; background-color: transparent; border: 0; - color: $gl-link-color; + color: $blue-600; font-weight: $gl-font-weight-bold; &:hover, &:focus { outline: none; - color: $gl-link-hover-color; + color: $blue-800; } .caret-icon { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index ddd1f8cc98a..892da152b5f 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -36,10 +36,7 @@ line-height: 35px; padding-top: 7px; padding-bottom: 7px; - - .float-right { - height: 20px; - } + display: flex; } .editor-ref { @@ -60,20 +57,18 @@ .new-file-name { display: inline-block; - max-width: 450px; + max-width: 420px; float: left; @media(max-width: map-get($grid-breakpoints, lg)-1) { - width: 280px; - } - - @media(max-width: map-get($grid-breakpoints, md)-1) { width: 180px; } } .file-buttons { - font-size: 0; + display: flex; + flex: 1; + justify-content: flex-end; } .select2 { @@ -111,12 +106,10 @@ } -@include media-breakpoint-down(xs) { +@include media-breakpoint-down(sm) { .file-editor { .file-title { - .float-right { - height: auto; - } + display: block; } .new-file-name { @@ -144,6 +137,10 @@ } } } + + .editor-ref { + max-width: 250px; + } } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 8a074017344..179c0964567 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -479,10 +479,10 @@ .deploy-info-text-link { font-family: $monospace-font; - fill: $gl-link-color; + fill: $blue-600; &:hover { - fill: $gl-link-hover-color; + fill: $blue-800; } } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 49d8a5d959b..22fce893fd7 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -24,11 +24,11 @@ } .graph-additions { - color: $gl-text-green; + color: $green-600; } .graph-deletions { - color: $gl-text-red; + color: $red-500; } } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 1587aebfe1d..fa8a0f26b5d 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -419,6 +419,7 @@ .stats { position: relative; line-height: normal; + text-align: right; flex-shrink: 0; > span { @@ -464,7 +465,7 @@ } .last-updated { - position: absolute; + position: relative; right: 12px; min-width: 250px; text-align: right; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 8e78d9f65eb..d16a63d009a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -141,7 +141,7 @@ color: inherit; &:hover { - color: $gl-link-hover-color; + color: $blue-800; .avatar { border-color: rgba($avatar-border, .2); @@ -241,7 +241,7 @@ &:hover { text-decoration: underline; - color: $gl-link-hover-color; + color: $blue-800; } } } @@ -329,7 +329,7 @@ } .btn-secondary-hover-link:hover { - color: $gl-link-color; + color: $blue-600; } .sidebar-collapsed-icon { @@ -448,8 +448,8 @@ } .todo-undone { - color: $gl-link-color; - fill: $gl-link-color; + color: $blue-600; + fill: $blue-600; } .author { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 212e5979273..0f95fb911e1 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -157,7 +157,7 @@ ul.related-merge-requests > li { .issuable-email-modal-btn { padding: 0; - color: $gl-link-color; + color: $blue-600; background-color: transparent; border: 0; outline: 0; @@ -190,7 +190,7 @@ ul.related-merge-requests > li { .create-mr-dropdown-wrap { .ref::selection { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } .dropdown { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 2b40404971c..d32943fceec 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -114,7 +114,7 @@ } &:hover { - color: $gl-link-color; + color: $blue-600; &.remove-row { color: $gl-danger; @@ -253,7 +253,7 @@ text-align: right; padding: 0; position: relative; - top: -3px; + margin: 0; } .label-badge { @@ -274,6 +274,7 @@ .label-links { list-style: none; + margin: 0; padding: 0; white-space: nowrap; } @@ -342,10 +343,10 @@ &.remove-row { &:hover { - color: $gl-text-red; + color: $red-500; svg { - fill: $gl-text-red; + fill: $red-500; } } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 46437ce5841..1e92582d6d9 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -30,7 +30,7 @@ .milestone-progress { a { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index dcf590e7331..4f861d43f55 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -69,7 +69,7 @@ .comment-toolbar, .nav-links { - border-color: $focus-border-color; + border-color: $blue-300; } } @@ -306,7 +306,7 @@ &:hover, &:focus { - color: $gl-link-color; + color: $blue-600; outline: 0; } @@ -424,7 +424,7 @@ .uploading-error-icon, .uploading-error-message { - color: $gl-text-red; + color: $red-500; } .uploading-error-message { @@ -443,7 +443,7 @@ .attach-new-file, .button-attach-file, .retry-uploading-link { - color: $gl-link-color; + color: $blue-600; padding: 0; background: none; border: 0; @@ -452,5 +452,5 @@ } .markdown-selector { - color: $gl-link-color; + color: $blue-600; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index c369d89d63c..8d28daac750 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -210,7 +210,7 @@ ul.notes { } a { - color: $gl-link-color; + color: $blue-600; } p { @@ -253,14 +253,14 @@ ul.notes { overflow: hidden; .system-note-commit-list-toggler { - color: $gl-link-color; + color: $blue-600; padding: 10px 0 0; cursor: pointer; position: relative; z-index: 2; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: underline; } } @@ -390,7 +390,7 @@ ul.notes { color: inherit; &:hover { - color: $gl-link-color; + color: $blue-600; } &:focus, @@ -451,7 +451,7 @@ ul.notes { .discussion-headline-light { a { - color: $gl-link-color; + color: $blue-600; } } @@ -560,12 +560,12 @@ ul.notes { &:hover, &.is-active { .danger-highlight { - color: $gl-text-red; + color: $red-500; } .link-highlight { - color: $gl-link-color; - fill: $gl-link-color; + color: $blue-600; + fill: $blue-600; } .award-control-icon-neutral { @@ -597,13 +597,13 @@ ul.notes { transition: color 0.1s linear; &:hover { - color: $gl-link-color; + color: $blue-600; } &:focus { text-decoration: underline; outline: none; - color: $gl-link-color; + color: $blue-600; } .fa { @@ -673,7 +673,7 @@ ul.notes { } a { - color: $gl-link-color; + color: $blue-600; } } @@ -759,16 +759,16 @@ ul.notes { &:not(.is-disabled) { &:hover, &:focus { - color: $gl-text-green; + color: $green-600; } } &.is-active { - color: $gl-text-green; + color: $green-600; &:hover, &:focus { - color: $gl-text-green-hover; + color: $green-700; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b68c89c25d8..ad057ed3c83 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -175,7 +175,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; } .badge { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 944421604fe..fffb440027c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -388,7 +388,7 @@ line-height: $gl-btn-line-height; &:hover { - color: $gl-link-color; + color: $blue-600; } } } @@ -754,8 +754,19 @@ } .repository-languages-bar { - height: 6px; - margin-bottom: 8px; + height: 8px; + margin-bottom: $gl-padding-8; + background-color: $white-light; + border-radius: $border-radius-default; + + .progress-bar { + margin-right: 2px; + padding: 0 $gl-padding-4; + + &:last-child { + margin-right: 0; + } + } } pre.light-well { @@ -823,10 +834,6 @@ pre.light-well { .avatar-container { align-self: flex-start; - - > a { - width: 100%; - } } .project-details { @@ -954,7 +961,7 @@ pre.light-well { margin-left: 5px; &.is-done { - color: $gl-text-green; + color: $green-600; } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 60b280fd12e..5b3a468cd1c 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -23,7 +23,7 @@ $search-avatar-size: 16px; .search-text-input:hover, .form-control:hover, :not[readonly] { - border-color: lighten($dropdown-input-focus-border, 20%); + border-color: lighten($blue-300, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); } @@ -127,7 +127,7 @@ input[type='checkbox']:hover { &.search-active { form { @extend .form-control:focus; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: none; @include media-breakpoint-up(xl) { @@ -259,6 +259,6 @@ input[type='checkbox']:hover { &:hover, &:focus { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 839ac5ba59b..fb03970f64f 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -301,3 +301,17 @@ margin-bottom: 0; } } + +.mirror-error-badge { + background-color: $error-bg; + border-radius: $border-radius-default; + color: $white-light; +} + +.push-pull-table { + margin-top: 1em; + + .mirror-action-buttons { + padding-right: 0; + } +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 7a93c4dfa28..57d43beaf21 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -6,7 +6,7 @@ left: 0; top: 0; width: 100%; - z-index: 2000; + z-index: 1039; height: $performance-bar-height; background: $black; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 05ed3669a41..e5b38898a67 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar - before_action :limit_unauthenticated_session_times before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? @@ -27,6 +26,7 @@ class ApplicationController < ActionController::Base around_action :set_locale after_action :set_page_title_header, if: :json_request? + after_action :limit_unauthenticated_session_times protect_from_forgery with: :exception, prepend: true diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index ed20302487c..461f26561f1 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -5,14 +5,14 @@ class NotificationSettingsController < ApplicationController return render_404 unless can_read?(resource) @notification_setting = current_user.notification_settings_for(resource) - @saved = @notification_setting.update(notification_setting_params) + @saved = @notification_setting.update(notification_setting_params_for(resource)) render_response end def update @notification_setting = current_user.notification_settings.find(params[:id]) - @saved = @notification_setting.update(notification_setting_params) + @saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source)) render_response end @@ -42,8 +42,8 @@ class NotificationSettingsController < ApplicationController } end - def notification_setting_params - allowed_fields = NotificationSetting::EMAIL_EVENTS.dup + def notification_setting_params_for(source) + allowed_fields = NotificationSetting.email_events(source).dup allowed_fields << :level params.require(:notification_setting).permit(allowed_fields) end diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 07627ffb69f..a8f73ed5cb0 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -32,13 +32,8 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController end def target - case params[:type]&.downcase - when 'issue' - IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) - when 'mergerequest' - MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) - when 'commit' - @project.commit(params[:type_id]) - end + QuickActions::TargetService + .new(project, current_user) + .execute(params[:type], params[:type_id]) end end diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb new file mode 100644 index 00000000000..fad33f0eca2 --- /dev/null +++ b/app/finders/license_template_finder.rb @@ -0,0 +1,36 @@ +# LicenseTemplateFinder +# +# Used to find license templates, which may come from a variety of external +# sources +# +# Arguments: +# popular: boolean. When set to true, only "popular" licenses are shown. When +# false, all licenses except popular ones are shown. When nil (the +# default), *all* licenses will be shown. +class LicenseTemplateFinder + attr_reader :params + + def initialize(params = {}) + @params = params + end + + def execute + Licensee::License.all(featured: popular_only?).map do |license| + LicenseTemplate.new( + id: license.key, + name: license.name, + nickname: license.nickname, + category: (license.featured? ? :Popular : :Other), + content: license.content, + url: license.url, + meta: license.meta + ) + end + end + + private + + def popular_only? + params.fetch(:popular, nil) + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 2bdf2c2c120..1e05f07e676 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -254,6 +254,7 @@ module ApplicationSettingsHelper :usage_ping_enabled, :instance_statistics_visibility_private, :user_default_external, + :user_show_add_ssh_key_message, :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index d48dae8f06d..494f785e305 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,28 +1,10 @@ module AvatarsHelper def project_icon(project_id, options = {}) - project = - if project_id.respond_to?(:avatar_url) - project_id - else - Project.find_by_full_path(project_id) - end - - if project.avatar_url - image_tag project.avatar_url, options - else # generated icon - project_identicon(project, options) - end + source_icon(Project, project_id, options) end - def project_identicon(project, options = {}) - bg_key = (project.id % 7) + 1 - options[:class] ||= '' - options[:class] << ' identicon' - options[:class] << " bg#{bg_key}" - - content_tag(:div, class: options[:class]) do - project.name[0, 1].upcase - end + def group_icon(group_id, options = {}) + source_icon(Group, group_id, options) end # Takes both user and email and returns the avatar_icon by @@ -123,4 +105,32 @@ module AvatarsHelper mail_to(options[:user_email], avatar) end end + + private + + def source_icon(klass, source_id, options = {}) + source = + if source_id.respond_to?(:avatar_url) + source_id + else + klass.find_by_full_path(source_id) + end + + if source.avatar_url + image_tag source.avatar_url, options + else + source_identicon(source, options) + end + end + + def source_identicon(source, options = {}) + bg_key = (source.id % 7) + 1 + options[:class] ||= '' + options[:class] << ' identicon' + options[:class] << " bg#{bg_key}" + + content_tag(:div, class: options[:class].strip) do + source.name[0, 1].upcase + end + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7eb45ddd117..b61cbd5418a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -182,12 +182,14 @@ module BlobHelper def licenses_for_select return @licenses_for_select if defined?(@licenses_for_select) - licenses = Licensee::License.all + grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category) + categories = grouped_licenses.keys - @licenses_for_select = { - Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } }, - Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } } - } + @licenses_for_select = categories.each_with_object({}) do |category, hash| + hash[category] = grouped_licenses[category].map do |license| + { name: license.name, id: license.id } + end + end end def ref_project diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0171a880164..7adc882bc47 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -73,7 +73,11 @@ module ButtonHelper end def ssh_clone_button(project, append_link: true) - dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?) + if Gitlab::CurrentSettings.user_show_add_ssh_key_message? && + current_user.try(:require_ssh_key?) + dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") + end + append_url = project.ssh_url_to_repo if append_link dropdown_item_with_description('SSH', dropdown_description, href: append_url) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 3c5c8bbd71b..5b51d2f2425 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -33,11 +33,6 @@ module GroupsHelper .count end - def group_icon(group, options = {}) - img_path = group_icon_url(group, options) - image_tag img_path, options - end - def group_icon_url(group, options = {}) if group.is_a?(String) group = Group.find_by_full_path(group) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 41084ec686f..a8a10c98d69 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -62,6 +62,8 @@ module IconsHelper names = "key" when "two-factor" names = "key" + when "google_oauth2" + names = "google" end options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb new file mode 100644 index 00000000000..93ed22513ac --- /dev/null +++ b/app/helpers/mirror_helper.rb @@ -0,0 +1,5 @@ +module MirrorHelper + def mirrors_form_data_attributes + { project_mirror_endpoint: project_mirror_path(@project) } + end +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 3e42063224e..a185f2916d4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -83,21 +83,11 @@ module NotificationsHelper end def notification_event_name(event) - # All values from NotificationSetting::EMAIL_EVENTS + # All values from NotificationSetting.email_events case event when :success_pipeline s_('NotificationEvent|Successful pipeline') else - N_('NotificationEvent|New note') - N_('NotificationEvent|New issue') - N_('NotificationEvent|Reopen issue') - N_('NotificationEvent|Close issue') - N_('NotificationEvent|Reassign issue') - N_('NotificationEvent|New merge request') - N_('NotificationEvent|Close merge request') - N_('NotificationEvent|Reassign merge request') - N_('NotificationEvent|Merge merge request') - N_('NotificationEvent|Failed pipeline') s_(event.to_s.humanize) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index aaf9dff43ee..6b4079b4113 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -192,7 +192,10 @@ module ProjectsHelper end def show_no_ssh_key_message? - cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? + Gitlab::CurrentSettings.user_show_add_ssh_key_message? && + cookies[:hide_no_ssh_message].blank? && + !current_user.hide_no_ssh_key && + current_user.require_ssh_key? end def show_no_password_message? diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index fe5f68ba3d5..e032f568913 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbuseReportMailer < BaseMailer def notify(abuse_report_id) return unless deliverable? diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 654468bc7fe..5fd209c4761 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BaseMailer < ActionMailer::Base around_action :render_with_default_locale diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 962570a0efd..7aa75ee30e6 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to @@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer protected def subject_for(key) - subject = super - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [super] + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end end diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb index 76db31a4c45..45fc5a6c383 100644 --- a/app/mailers/email_rejection_mailer.rb +++ b/app/mailers/email_rejection_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailer < BaseMailer def rejection(reason, original_raw, can_retry = false) @reason = reason diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 392cc0bee03..c8b1ab5033a 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Issues def new_issue_email(recipient_id, issue_id, reason = nil) diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 75cf56a51f2..91dfdf58982 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Members extend ActiveSupport::Concern diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 70509e9066d..70f65d4e58d 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id, reason = nil) diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index d9a6fe2a41e..d3284e90568 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Notes def note_commit_email(recipient_id, note_id) diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb index 0027dfdc36b..ce449237ef6 100644 --- a/app/mailers/emails/pages_domains.rb +++ b/app/mailers/emails/pages_domains.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module PagesDomains def pages_domain_enabled_email(domain, recipient) diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index f9f45ab987b..31e183640ad 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Pipelines def pipeline_success_email(pipeline, recipients) @@ -39,10 +41,10 @@ module Emails end def pipeline_subject(status) - commit = @pipeline.short_sha - commit << " in #{@merge_request.to_reference}" if @merge_request + commit = [@pipeline.short_sha] + commit << "in #{@merge_request.to_reference}" if @merge_request - subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit) + subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit.join(' ')) end end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 4f5edeb9bda..40d7b9ccd7a 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Profile def new_user_email(user_id, token = nil) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 761d873c01c..d7e6c2ba7b2 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Projects def project_was_moved_email(project_id, user_id, old_path_with_namespace) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 0e1e39501f5..f4eeb85270e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Notify < BaseMailer include ActionDispatch::Routing::PolymorphicRoutes include GitlabRoutingHelper @@ -92,12 +94,14 @@ class Notify < BaseMailer # >> subject('Lorem ipsum', 'Dolor sit amet') # => "Lorem ipsum | Dolor sit amet" def subject(*extra) - subject = "" - subject << "#{@project.name} | " if @project - subject << "#{@group.name} | " if @group - subject << extra.join(' | ') if extra.present? - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [] + + subject << @project.name if @project + subject << @group.name if @group + subject.concat(extra) if extra.present? + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end # Return a string suitable for inclusion in the 'Message-Id' mail header. diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb index d6588efc486..3b9ef0d3ac0 100644 --- a/app/mailers/previews/devise_mailer_preview.rb +++ b/app/mailers/previews/devise_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailerPreview < ActionMailer::Preview def confirmation_instructions_for_signup DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {}) diff --git a/app/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb index 639e8471232..402066151ef 100644 --- a/app/mailers/previews/email_rejection_mailer_preview.rb +++ b/app/mailers/previews/email_rejection_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailerPreview < ActionMailer::Preview def rejection EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 3615cde8026..df470930e9e 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotifyPreview < ActionMailer::Preview def note_merge_request_email_for_individual_note note_email(:note_merge_request_email) do diff --git a/app/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb index 19d4eab1805..834d7594719 100644 --- a/app/mailers/previews/repository_check_mailer_preview.rb +++ b/app/mailers/previews/repository_check_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailerPreview < ActionMailer::Preview def notify RepositoryCheckMailer.notify(3).message diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 22a9f5da646..4bcf371cfc0 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailer < BaseMailer def notify(failed_count) @message = diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bbe7811841a..c77faa4b71d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -298,7 +298,8 @@ class ApplicationSetting < ActiveRecord::Base unique_ips_limit_time_window: 3600, usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], instance_statistics_visibility_private: false, - user_default_external: false + user_default_external: false, + user_show_add_ssh_key_message: true } end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9292929be98..3c69677baf0 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -226,7 +226,7 @@ module Ci end def cancelable? - active? + active? || created? end def retryable? diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index d7c5f29be96..17b7ee4f07e 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -33,7 +33,7 @@ module Ci where(file_type: types) end - delegate :exists?, :open, to: :file + delegate :filename, :exists?, :open, to: :file enum file_type: { archive: 1, diff --git a/app/models/commit.rb b/app/models/commit.rb index 8b9f4490ffa..27fbdc3e386 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -448,6 +448,10 @@ class Commit true end + def to_ability_name + model_name.singular + end + def touch # no-op but needs to be defined since #persisted? is defined end diff --git a/app/models/license_template.rb b/app/models/license_template.rb new file mode 100644 index 00000000000..0ad75b27827 --- /dev/null +++ b/app/models/license_template.rb @@ -0,0 +1,53 @@ +class LicenseTemplate + PROJECT_TEMPLATE_REGEX = + %r{[\<\{\[] + (project|description| + one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here + [\>\}\]]}xi.freeze + YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze + FULLNAME_TEMPLATE_REGEX = + %r{[\<\{\[] + (fullname|name\sof\s(author|copyright\sowner)) + [\>\}\]]}xi.freeze + + attr_reader :id, :name, :category, :nickname, :url, :meta + + alias_method :key, :id + + def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {}) + @id = id + @name = name + @category = category + @content = content + @nickname = nickname + @url = url + @meta = meta + end + + def popular? + category == :Popular + end + alias_method :featured?, :popular? + + # Returns the text of the license + def content + if @content.respond_to?(:call) + @content = @content.call + else + @content + end + end + + # Populate placeholders in the LicenseTemplate content + def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s) + # Ensure the string isn't shared with any other instance of LicenseTemplate + new_content = content.dup + new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present? + new_content.gsub!(PROJECT_TEMPLATE_REGEX, project_name) if project_name.present? + new_content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname.present? + + @content = new_content + + self + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b974309aeb6..0deb44d7916 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -10,6 +10,7 @@ class Namespace < ActiveRecord::Base include Storage::LegacyNamespace include Gitlab::SQL::Pattern include IgnorableColumn + include FeatureGate ignore_column :deleted_at @@ -124,7 +125,6 @@ class Namespace < ActiveRecord::Base def to_param full_path end - alias_method :flipper_id, :to_param def human_name owner_name diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 1df3a51a7fc..1600acfc575 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -45,6 +45,15 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ].freeze + # Update unfound_translations.rb when events are changed + def self.email_events(source = nil) + EMAIL_EVENTS + end + + def email_events + self.class.email_events(source) + end + EXCLUDED_PARTICIPATING_EVENTS = [ :success_pipeline ].freeze diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb index 400d6c407a7..0e667dac21e 100644 --- a/app/models/programming_language.rb +++ b/app/models/programming_language.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProgrammingLanguage < ActiveRecord::Base validates :name, presence: true validates :color, allow_blank: false, color: true diff --git a/app/models/project.rb b/app/models/project.rb index 36089995ed3..7735f23cb9e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -470,6 +470,24 @@ class Project < ActiveRecord::Base }x end + def reference_postfix + '>' + end + + def reference_postfix_escaped + '>' + end + + # Pattern used to extract `namespace/project>` project references from text. + # '>' or its escaped form ('>') are checked for because '>' is sometimes escaped + # when the reference comes from an external source. + def markdown_reference_pattern + %r{ + #{reference_pattern} + (#{reference_postfix}|#{reference_postfix_escaped}) + }x + end + def trending joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id') .reorder('trending_projects.id ASC') @@ -908,6 +926,10 @@ class Project < ActiveRecord::Base end end + def to_reference_with_postfix + "#{to_reference(full: true)}#{self.class.reference_postfix}" + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) if full || cross_namespace_reference?(from) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 4f289e6e215..35c19049c04 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 4234b8044e5..60575e45a90 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AssemblaService < Service prop_accessor :token, :subdomain validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index edc5c00d9c4..d502423726c 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BambooService < CiService include ReactiveService diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index e4e3a80976b..1a2bb6a171b 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BugzillaService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 35884c4560c..43edfde851c 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "addressable/uri" class BuildkiteService < CiService diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 0c526b53d72..f2295a95b60 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class is to be removed with 9.1 # We should also by then remove BuildsEmailService from database class BuildsEmailService < Service diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index cb4af73807b..1d7877a1fb5 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CampfireService < Service prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? @@ -82,7 +84,7 @@ class CampfireService < Service before = push[:before] after = push[:after] - message = "" + message = [] message << "[#{project.full_name}] " message << "#{push[:user_name]} " @@ -95,6 +97,6 @@ class CampfireService < Service message << "#{project.web_url}/compare/#{before}...#{after}" end - message + message.join end end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index f710fa85b5d..8c68ddc40f2 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'slack-notifier' module ChatMessage diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 3273f41dbd2..0cdcfcf0237 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class IssueMessage < BaseMessage attr_reader :title diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index f412b6833d9..58631e09538 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class MergeMessage < BaseMessage attr_reader :merge_request_iid diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index 7f9486132e6..741474fb27b 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class NoteMessage < BaseMessage attr_reader :note diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 96fd23aede3..62aec4351db 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class PipelineMessage < BaseMessage attr_reader :ref_type diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 8d599c5f116..82be33a12a1 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class PushMessage < BaseMessage attr_reader :after diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index d84b80f2de2..b605d289278 100644 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class WikiPageMessage < BaseMessage attr_reader :title diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index a60b4c7fd0d..c10ee07ccf4 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for Chat notifications services # This class is not meant to be used directly, but only to inherit from. class ChatNotificationService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 82979c8bd34..f0ef2d925ab 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for CI services # List methods you need to implement to get your CI service # working with GitLab Merge Requests diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 456c7f5cee2..b8f8072869c 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CustomIssueTrackerService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index 5b8320158fc..6dae4f3a4a6 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for deployment services # # These services integrate with a deployment solution like Kubernetes/OpenShift, diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index ab4e46da89f..158ae0bf255 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DroneCiService < CiService include ReactiveService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index b604d860a87..fb73d430fb1 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailsOnPushService < Service boolean_accessor :send_from_committer_email boolean_accessor :disable_diffs diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index a4b1ef09e93..d2835c6ac82 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ExternalWikiService < Service prop_accessor :external_wiki_url diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index da01ac1b7cf..2545df06f6b 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "flowdock-git-hook" # Flow dock depends on Grit to compute the number of commits between two given diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 8a6b0ed1a5f..67a92c441b1 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "gemnasium/gitlab_service" class GemnasiumService < Service diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 16e32a4139e..fa9abf58e62 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb index a8512c5f57c..272cd0f4e47 100644 --- a/app/models/project_services/hangouts_chat_service.rb +++ b/app/models/project_services/hangouts_chat_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'hangouts_chat' class HangoutsChatService < ChatNotificationService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index dce878e485f..66012f0da99 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class HipchatService < Service include ActionView::Helpers::SanitizeHelper @@ -108,7 +110,7 @@ class HipchatService < Service before = push[:before] after = push[:after] - message = "" + message = [] message << "#{push[:user_name]} " if Gitlab::Git.blank_ref?(before) @@ -132,7 +134,7 @@ class HipchatService < Service end end - message + message.join end def markdown(text, options = {}) @@ -165,11 +167,11 @@ class HipchatService < Service description = obj_attr[:description] issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" - message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>" + message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"] message << "<pre>#{markdown(description)}</pre>" - message + message.join end def create_merge_request_message(data) @@ -184,12 +186,11 @@ class HipchatService < Service merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" - message = "#{user_name} #{state} #{merge_request_link} in " \ - "#{project_link}: <b>#{title}</b>" + message = ["#{user_name} #{state} #{merge_request_link} in " \ + "#{project_link}: <b>#{title}</b>"] message << "<pre>#{markdown(description)}</pre>" - - message + message.join end def format_title(title) @@ -235,12 +236,11 @@ class HipchatService < Service end subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>" - message = "#{user_name} commented on #{subject_html} in #{project_link}: " + message = ["#{user_name} commented on #{subject_html} in #{project_link}: "] message << title message << "<pre>#{markdown(note, ref: commit_id)}</pre>" - - message + message.join end def create_pipeline_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 27bdf708c80..a783a314071 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'uri' class IrkerService < Service diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index df6dcd90985..c7520d766a8 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class IssueTrackerService < Service validate :one_issue_tracker, if: :activated?, on: :manual_change diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 82d438d5378..cc98b3f5a41 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JiraService < IssueTrackerService include Gitlab::Routing include ApplicationHelper diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 722642f6da7..bda1f67b8ff 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # NOTE: # We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 0362ed172c7..b8bc83b870e 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MattermostService < ChatNotificationService def title 'Mattermost notifications' diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 227d430083d..ca324f68d2d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MattermostSlashCommandsService < SlashCommandsService include TriggersHelper diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb index 99500caec0e..5b0e5fed092 100644 --- a/app/models/project_services/microsoft_teams_service.rb +++ b/app/models/project_services/microsoft_teams_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MicrosoftTeamsService < ChatNotificationService def title 'Microsoft Teams Notification' diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb index b89dc07a73e..6883976f0c8 100644 --- a/app/models/project_services/mock_ci_service.rb +++ b/app/models/project_services/mock_ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service class MockCiService < CiService ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb index 59a3811ce5d..7ab1687f8ba 100644 --- a/app/models/project_services/mock_deployment_service.rb +++ b/app/models/project_services/mock_deployment_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MockDeploymentService < DeploymentService def title 'Mock deployment' diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb index ed0318c6b27..bcf8f1df5da 100644 --- a/app/models/project_services/mock_monitoring_service.rb +++ b/app/models/project_services/mock_monitoring_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MockMonitoringService < MonitoringService def title 'Mock monitoring' diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index 9af68b4e821..1b530a8247b 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for monitoring services # # These services integrate with a deployment solution like Prometheus diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb index ba62a5b7ac0..003884bb7ac 100644 --- a/app/models/project_services/packagist_service.rb +++ b/app/models/project_services/packagist_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PackagistService < Service prop_accessor :username, :token, :server diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 4cf149ac044..6f39a5e6e83 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelinesEmailService < Service prop_accessor :recipients boolean_accessor :notify_only_broken_pipelines diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 3476e7d2283..617e502b639 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PivotaltrackerService < Service API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index df4254e0523..509e5b6089b 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PrometheusService < MonitoringService include PrometheusAdapter diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 8777a44b72f..4e48c348b45 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PushoverService < Service BASE_URI = 'https://api.pushover.net/1'.freeze @@ -79,7 +81,7 @@ class PushoverService < Service end if data[:total_commits_count] > 0 - message << "\nTotal commits count: #{data[:total_commits_count]}" + message = [message, "Total commits count: #{data[:total_commits_count]}"].join("\n") end pushover_data = { diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 3721093a6d1..a80be4b06da 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RedmineService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 71da0af75f6..482808255f9 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SlackService < ChatNotificationService def title 'Slack notifications' diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 1c3892a3f75..6c82e088231 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SlackSlashCommandsService < SlashCommandsService include TriggersHelper diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 37ea45109ae..e3ab60adefd 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. class SlashCommandsService < Service diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 802678147cf..eeeff5e802a 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TeamcityService < CiService include ReactiveService diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index e8d35ac326f..b0d5c64e931 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 7a2e9e5ec5d..b2a88229853 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess end diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb index 6b6ab3d8279..b06e55fb5dd 100644 --- a/app/models/protected_tag/create_access_level.rb +++ b/app/models/protected_tag/create_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedTag::CreateAccessLevel < ActiveRecord::Base include ProtectedTagAccess diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb index f467d4eafa3..b18142a2ac4 100644 --- a/app/models/repository_language.rb +++ b/app/models/repository_language.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryLanguage < ActiveRecord::Base belongs_to :project belongs_to :programming_language diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb index 9c9c3172fe6..daac1c57db9 100644 --- a/app/models/site_statistic.rb +++ b/app/models/site_statistic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SiteStatistic < ActiveRecord::Base # prevents the creation of multiple rows default_value_for :id, 1 diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index 26b4b78ac64..90710f73fd3 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Storage class HashedProject attr_accessor :project diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 27cb388c702..9f6f19acb41 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Storage class LegacyProject attr_accessor :project diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index c5c77bc8333..376ef673ca8 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -15,7 +15,7 @@ class SystemNoteMetadata < ActiveRecord::Base commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged duplicate locked unlocked - outdated + outdated tag ].freeze validates :note, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 37f2e8b680e..fb19de4b980 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -101,6 +101,10 @@ class User < ActiveRecord::Base has_many :groups, through: :group_members has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group + has_many :owned_or_maintainers_groups, + -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) }, + through: :group_members, + source: :group alias_attribute :masters_groups, :maintainers_groups # Projects @@ -982,15 +986,7 @@ class User < ActiveRecord::Base end def manageable_groups - union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql - - # Update this line to not use raw SQL when migrated to Rails 5.2. - # Either ActiveRecord or Arel constructions are fine. - # This was replaced with the raw SQL construction because of bugs in the arel gem. - # Bugs were fixed in arel 9.0.0 (Rails 5.2). - owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection - - Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants + Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants end def namespaces @@ -1244,11 +1240,6 @@ class User < ActiveRecord::Base !terms_accepted? end - def owned_or_maintainers_groups - union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups]) - Group.from("(#{union.to_sql}) namespaces") - end - # @deprecated alias_method :owned_or_masters_groups, :owned_or_maintainers_groups diff --git a/app/policies/commit_policy.rb b/app/policies/commit_policy.rb new file mode 100644 index 00000000000..67e9bc12804 --- /dev/null +++ b/app/policies/commit_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CommitPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f52a3bad77d..00c58f15013 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -251,6 +251,7 @@ class ProjectPolicy < BasePolicy enable :update_pages enable :read_cluster enable :create_cluster + enable :create_environment_terminal end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 02f6c5bdf81..880218e2727 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Ci class BuildRunnerPresenter < SimpleDelegator def artifacts diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index b18e9706db6..07a13c33b89 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -23,9 +23,8 @@ class EnvironmentEntity < Grape::Entity stop_project_environment_path(environment.project, environment) end - expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| - can?(request.current_user, :admin_environment, environment.project) && - terminal_project_environment_path(environment.project, environment) + expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment| + terminal_project_environment_path(environment.project, environment) end expose :folder_path do |environment| @@ -40,7 +39,13 @@ class EnvironmentEntity < Grape::Entity private + alias_method :environment, :object + def current_user request.current_user end + + def can_access_terminal? + can?(request.current_user, :create_environment_terminal, environment) + end end diff --git a/app/serializers/project_mirror_serializer.rb b/app/serializers/project_mirror_serializer.rb new file mode 100644 index 00000000000..6a9462aa7cb --- /dev/null +++ b/app/serializers/project_mirror_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProjectMirrorSerializer < BaseSerializer + entity ProjectMirrorEntity +end diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb index 5c1cbf37182..ec60055ba5b 100644 --- a/app/serializers/test_case_entity.rb +++ b/app/serializers/test_case_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestCaseEntity < Grape::Entity expose :status expose :name diff --git a/app/serializers/test_reports_comparer_entity.rb b/app/serializers/test_reports_comparer_entity.rb index b95d820d093..d7a3dd34fdc 100644 --- a/app/serializers/test_reports_comparer_entity.rb +++ b/app/serializers/test_reports_comparer_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestReportsComparerEntity < Grape::Entity expose :total_status, as: :status diff --git a/app/serializers/test_reports_comparer_serializer.rb b/app/serializers/test_reports_comparer_serializer.rb index a739858efb2..7fb8d28b09a 100644 --- a/app/serializers/test_reports_comparer_serializer.rb +++ b/app/serializers/test_reports_comparer_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestReportsComparerSerializer < BaseSerializer entity TestReportsComparerEntity end diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index a3965ba3930..9fa3a897ebe 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestSuiteComparerEntity < Grape::Entity expose :name expose :total_status, as: :status diff --git a/app/services/ci/enqueue_build_service.rb b/app/services/ci/enqueue_build_service.rb new file mode 100644 index 00000000000..8140651d980 --- /dev/null +++ b/app/services/ci/enqueue_build_service.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Ci + class EnqueueBuildService < BaseService + def execute(build) + build.enqueue + end + end +end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index cda9bbff3b4..cafee76a33c 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -37,7 +37,7 @@ module Ci def process_build(build, current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.action? ? build.actionize : build.enqueue + build.action? ? build.actionize : enqueue_build(build) true else build.skip @@ -93,5 +93,9 @@ module Ci .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) if latest_statuses.any? end + + def enqueue_build(build) + Ci::EnqueueBuildService.new(project, @user).execute(build) + end end end diff --git a/app/services/commits/tag_service.rb b/app/services/commits/tag_service.rb new file mode 100644 index 00000000000..7961ba4d3c4 --- /dev/null +++ b/app/services/commits/tag_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Commits + class TagService < BaseService + def execute(commit) + unless params[:tag_name] + return error('Missing parameter tag_name') + end + + tag_name = params[:tag_name] + message = params[:tag_message] + release_description = nil + + result = Tags::CreateService + .new(commit.project, current_user) + .execute(tag_name, commit.sha, message, release_description) + + if result[:status] == :success + tag = result[:tag] + SystemNoteService.tag_commit(commit, commit.project, current_user, tag.name) + end + + result + end + end +end diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index 7280449bb1c..4c14d834949 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -4,7 +4,8 @@ module Notes class QuickActionsService < BaseService UPDATE_SERVICES = { 'Issue' => Issues::UpdateService, - 'MergeRequest' => MergeRequests::UpdateService + 'MergeRequest' => MergeRequests::UpdateService, + 'Commit' => Commits::TagService }.freeze def self.noteable_update_service(note) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 4389fd89538..5c0e8a35cb0 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -130,7 +130,7 @@ module NotificationRecipientService end def add_project_watchers - add_recipients(project_watchers, :watch, nil) + add_recipients(project_watchers, :watch, nil) if project end def add_group_watchers @@ -220,6 +220,8 @@ module NotificationRecipientService end class Default < Base + MENTION_TYPE_ACTIONS = [:new_issue, :new_merge_request].freeze + attr_reader :target attr_reader :current_user attr_reader :action @@ -252,7 +254,7 @@ module NotificationRecipientService add_subscribed_users - if [:new_issue, :new_merge_request].include?(custom_action) + if self.class.mention_type_actions.include?(custom_action) # These will all be participants as well, but adding with the :mention # type ensures that users with the mention notification level will # receive them, too. @@ -279,10 +281,14 @@ module NotificationRecipientService end # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS + # Check NotificationSetting.email_events def custom_action @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym end + + def self.mention_type_actions + MENTION_TYPE_ACTIONS.dup + end end class NewNote < Base diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index a15ee4911ef..11b996ed4b6 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -16,7 +16,7 @@ class PreviewMarkdownService < BaseService private def explain_quick_actions(text) - return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) + return text, [] unless %w(Issue MergeRequest Commit).include?(commands_target_type) quick_actions_service = QuickActions::InterpretService.new(project, current_user) quick_actions_service.explain(text, find_commands_target) @@ -29,13 +29,9 @@ class PreviewMarkdownService < BaseService end def find_commands_target - if commands_target_id.present? - finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder - finder.new(current_user, project_id: project.id).find(commands_target_id) - else - collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests - collection.build - end + QuickActions::TargetService + .new(project, current_user) + .execute(commands_target_type, commands_target_id) end def commands_target_type diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 10eb2cea4a2..5286b92ab6b 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -47,15 +47,7 @@ module Projects end def commands(noteable, type) - noteable ||= - case type - when 'Issue' - @project.issues.build - when 'MergeRequest' - @project.merge_requests.build - end - - return [] unless noteable&.is_a?(Issuable) + return [] unless noteable QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb index 4b4108de231..3488b9ce47e 100644 --- a/app/services/projects/detect_repository_languages_service.rb +++ b/app/services/projects/detect_repository_languages_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects class DetectRepositoryLanguagesService < BaseService attr_reader :detected_repository_languages, :programming_languages diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index cdc8514c47c..8838ed06324 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -60,7 +60,8 @@ module QuickActions "Closes this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.open? && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end @@ -75,7 +76,8 @@ module QuickActions "Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.closed? && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end @@ -149,7 +151,8 @@ module QuickActions issuable.allows_multiple_assignees? ? '@user1 @user2' : '' end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.assignees.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -188,7 +191,8 @@ module QuickActions "Removes #{issuable.milestone.to_reference(format: :name)} milestone." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.milestone_id? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -231,7 +235,8 @@ module QuickActions end params '~label1 ~"label 2"' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.labels.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -257,7 +262,8 @@ module QuickActions end params '~label1 ~"label 2"' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.labels.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -295,7 +301,8 @@ module QuickActions desc 'Add a todo' explanation 'Adds a todo.' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && !TodoService.new.todo_exist?(issuable, current_user) end command :todo do @@ -317,7 +324,8 @@ module QuickActions "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && !issuable.subscribed?(current_user, project) end command :subscribe do @@ -329,7 +337,8 @@ module QuickActions "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.subscribed?(current_user, project) end command :unsubscribe do @@ -385,7 +394,8 @@ module QuickActions end params ':emoji:' condition do - issuable.persisted? + issuable.is_a?(Issuable) && + issuable.persisted? end parse_params do |emoji_param| match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern) @@ -574,6 +584,23 @@ module QuickActions @updates[:confidential] = true end + desc 'Tag this commit.' + explanation do |tag_name, message| + with_message = %{ with "#{message}"} if message.present? + "Tags this commit to #{tag_name}#{with_message}." + end + params 'v1.2.3 <message>' + parse_params do |tag_name_and_message| + tag_name_and_message.split(' ', 2) + end + condition do + issuable.is_a?(Commit) && current_user.can?(:push_code, project) + end + command :tag do |tag_name, message| + @updates[:tag_name] = tag_name + @updates[:tag_message] = message + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb new file mode 100644 index 00000000000..d8ba52c6e50 --- /dev/null +++ b/app/services/quick_actions/target_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QuickActions + class TargetService < BaseService + def execute(type, type_id) + case type&.downcase + when 'issue' + issue(type_id) + when 'mergerequest' + merge_request(type_id) + when 'commit' + commit(type_id) + end + end + + private + + def issue(type_id) + IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build + end + + def merge_request(type_id) + MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build + end + + def commit(type_id) + project.commit(type_id) + end + end +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 77494295f14..dda89830179 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -32,6 +32,21 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count)) end + # Called when a commit was tagged + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the tag + # tag_name - The created tag name + # + # Returns the created Note object + def tag_commit(noteable, project, author, tag_name) + link = url_helpers.project_tag_url(project, id: tag_name) + body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'tag')) + end + # Called when the assignee of a Noteable is changed or removed # # noteable - Noteable object diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 329722df747..35390f5082c 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -7,7 +7,7 @@ module Tags return error('Tag name invalid') unless valid_tag repository = project.repository - message&.strip! + message = message&.strip new_tag = nil diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb index dff5e1f30e5..aeb60e50c64 100644 --- a/app/services/todos/destroy/base_service.rb +++ b/app/services/todos/destroy/base_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class BaseService diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb index c5b66df057a..efec0f22da5 100644 --- a/app/services/todos/destroy/confidential_issue_service.rb +++ b/app/services/todos/destroy/confidential_issue_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class ConfidentialIssueService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb index 045f5ecaae7..4cb9d08713d 100644 --- a/app/services/todos/destroy/entity_leave_service.rb +++ b/app/services/todos/destroy/entity_leave_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class EntityLeaveService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb index d13fa7a6516..f67f1d40597 100644 --- a/app/services/todos/destroy/group_private_service.rb +++ b/app/services/todos/destroy/group_private_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class GroupPrivateService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/private_features_service.rb b/app/services/todos/destroy/private_features_service.rb index 4d8e2877bfb..7e204885b31 100644 --- a/app/services/todos/destroy/private_features_service.rb +++ b/app/services/todos/destroy/private_features_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class PrivateFeaturesService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb index 315a0c33398..ae8fab3ffca 100644 --- a/app/services/todos/destroy/project_private_service.rb +++ b/app/services/todos/destroy/project_private_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class ProjectPrivateService < ::Todos::Destroy::BaseService diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 7c8243a7a90..622cb11010e 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -29,5 +29,11 @@ = f.check_box :user_default_external, class: 'form-check-input' = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external + .form-group + = f.label :user_show_add_ssh_key_message, 'Prompt users to upload SSH keys', class: 'label-bold' + .form-check + = f.check_box :user_show_add_ssh_key_message, class: 'form-check-input' + = f.label :user_show_add_ssh_key_message, class: 'form-check-label' do + Inform users without uploaded SSH keys that they can't push over SSH until one is added = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 258d50ad676..6133a7646f4 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -325,6 +325,8 @@ .settings-content = render partial: 'repository_mirrors_form' += render_if_exists 'admin/application_settings/templates', expanded: expanded + %section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml index 8aaa6379730..b45d3e4823b 100644 --- a/app/views/admin/spam_logs/index.html.haml +++ b/app/views/admin/spam_logs/index.html.haml @@ -17,6 +17,6 @@ %th Primary Action %th = render @spam_logs - = paginate @spam_logs + = paginate @spam_logs, theme: 'gitlab' - else %h4 There are no Spam Logs diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index f730fd05176..029efadd75d 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -128,8 +128,8 @@ .col-md-6 - unless @user == current_user - unless @user.confirmed? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white Confirm user .card-body - if @user.unconfirmed_email.present? @@ -138,8 +138,8 @@ %br = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - if @user.blocked? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white This user is blocked .card-body %p A blocked user cannot: @@ -162,8 +162,8 @@ %br = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" - if @user.access_locked? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white This account has been locked .card-body %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account. diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 8560b72fe85..24f256d083b 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -2,11 +2,11 @@ .file-holder-bottom-radius.file-holder.file.append-bottom-default .js-file-title.file-title.clearfix{ data: { current_action: action } } - .editor-ref + .editor-ref.block-truncated = sprite_icon('fork', size: 12) = ref - %span.editor-file-name - - if current_action?(:edit) || current_action?(:update) + - if current_action?(:edit) || current_action?(:update) + %span.editor-file-name = text_field_tag 'file_path', (params[:file_path] || @path), class: 'form-control new-file-path js-file-path-name-input' @@ -16,7 +16,7 @@ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name js-file-path-name-input' - .float-right.file-buttons + .file-buttons = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml index 8a549d431ee..12cf40bb65f 100644 --- a/app/views/projects/forks/_fork_button.html.haml +++ b/app/views/projects/forks/_fork_button.html.haml @@ -5,7 +5,7 @@ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked = link_to project_path(forked_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_identicon(namespace, class: "avatar s100 identicon") + = project_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") @@ -18,7 +18,7 @@ class: ("disabled has-tooltip" unless can_create_project), title: (_('You have reached your project limit') unless can_create_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_identicon(namespace, class: "avatar s100 identicon") + = project_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 759efd4e9d4..86b2b8bf2f7 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -1,12 +1,7 @@ %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } .sidebar-container .blocks-container - - if can?(current_user, :create_build_terminal, @build) - .block - = link_to terminal_project_job_path(@project, @build), class: 'terminal-button pull-right btn visible-md-block visible-lg-block', title: 'Terminal' do - Terminal - - #js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } } + #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml index 64f0fde30cf..e051f9e6331 100644 --- a/app/views/projects/mirrors/_instructions.html.haml +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -1,10 +1,11 @@ .account-well.prepend-top-default.append-bottom-default %ul %li - The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>. - %li - Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>. - %li - The update action will time out after 10 minutes. For big repositories, use a clone/push combination. - %li - The Git LFS objects will <strong>not</strong> be synced. + = _('The repository must be accessible over <code>http://</code>, + <code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe + %li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe + %li= _("The update action will time out after #{import_will_timeout_message(Gitlab.config.gitlab_shell.git_timeout)} minutes. For big repositories, use a clone/push combination.") + %li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe + %li + = _('This user will be the author of all events in the activity feed that are the result of an update, + like new branches being created or new commits being pushed to existing branches.') diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml new file mode 100644 index 00000000000..53387b3a50c --- /dev/null +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -0,0 +1,63 @@ +- expanded = Rails.env.test? +- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') + +%section.settings.project-mirror-settings.js-mirror-settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4= _('Mirroring repositories') + %button.btn.js-settings-toggle + = expanded ? _('Collapse') : _('Expand') + %p + = _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.') + = link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank' + + .settings-content + = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f| + .panel.panel-default + .panel-heading + %h3.panel-title= _('Mirror a repository') + .panel-body + %div= form_errors(@project) + + .form-group.has-feedback + = label_tag :url, _('Git repository URL'), class: 'label-light' + = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+" + + = render 'projects/mirrors/instructions' + + = render 'projects/mirrors/mirror_repos_form', f: f + + .form-check.append-bottom-10 + = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' + = label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label' + = link_to icon('question-circle'), help_page_path('user/project/protected_branches') + + .panel-footer + = f.submit _('Mirror repository'), class: 'btn btn-create', name: :update_remote_mirror + + .panel.panel-default + .table-responsive + %table.table.push-pull-table + %thead + %tr + %th + = _('Mirrored repositories') + = render_if_exists 'projects/mirrors/mirrored_repositories_count' + %th= _('Direction') + %th= _('Last update') + %th + %th + %tbody.js-mirrors-table-body + = render_if_exists 'projects/mirrors/table_pull_row' + - @project.remote_mirrors.each_with_index do |mirror, index| + - if mirror.enabled + %tr + %td= mirror.safe_url + %td= _('Push') + %td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td + - if mirror.last_error.present? + .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') + %td.mirror-action-buttons + .btn-group.mirror-actions-group.pull-right{ role: 'group' } + = render 'shared/remote_mirror_update_button', remote_mirror: mirror + %button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o') diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml new file mode 100644 index 00000000000..93994cb30ac --- /dev/null +++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml @@ -0,0 +1,18 @@ +- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') + +.form-group + = label_tag :mirror_direction, _('Mirror direction'), class: 'label-light' + = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true + += f.fields_for :remote_mirrors, @project.remote_mirrors.build do |rm_f| + = rm_f.hidden_field :enabled, value: '1' + = rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+" + = rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden' + +.form-group + = label_tag :auth_method, _('Authentication method'), class: 'label-bold' + = select_tag :auth_method, options_for_select([[_('None'), 'none'], [_('Password'), 'password']], 'none'), { class: "form-control js-auth-method" } + +.form-group.js-password-group.collapse + = label_tag :password, _('Password'), class: 'label-bold' + = text_field_tag :password, '', class: 'form-control js-password' diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml deleted file mode 100644 index 08375e09816..00000000000 --- a/app/views/projects/mirrors/_push.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- expanded = Rails.env.test? -%section.settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - Push to a remote repository - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Set up the remote repository that you want to update with the content of the current repository - every time someone pushes to it. - = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank' - .settings-content - = form_for @project, url: project_mirror_path(@project) do |f| - %div - = form_errors(@project) - = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror - - if @remote_mirror.last_error.present? - .panel.panel-danger - .panel-heading - - if @remote_mirror.last_update_at - The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}. - - else - The remote repository failed to update. - - - if @remote_mirror.last_successful_update_at - Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. - .panel-body - %pre - :preserve - #{h(@remote_mirror.last_error.strip)} - = f.fields_for :remote_mirrors, @remote_mirror do |rm_form| - .form-group - = rm_form.check_box :enabled, class: "float-left" - .prepend-left-20 - = rm_form.label :enabled, "Remote mirror repository", class: "label-bold append-bottom-0" - %p.light.append-bottom-0 - Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it. - .form-group.has-feedback - = rm_form.label :url, "Git repository URL", class: "label-bold" - = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git' - - = render "projects/mirrors/instructions" - - .form-group - = rm_form.check_box :only_protected_branches, class: 'float-left' - .prepend-left-20 - = rm_form.label :only_protected_branches, class: 'label-bold' - = link_to icon('question-circle'), help_page_path('user/project/protected_branches') - - = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror' diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml index de77701a373..8318d5898a1 100644 --- a/app/views/projects/mirrors/_show.html.haml +++ b/app/views/projects/mirrors/_show.html.haml @@ -1,3 +1 @@ -- if can?(current_user, :admin_remote_mirror, @project) - = render 'projects/mirrors/push' - += render 'projects/mirrors/mirror_repos' diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index cd9177c0f9e..988dabef3a0 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,6 +1,6 @@ - unless @project.pages_deployed? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white Configure pages .card-body %p diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 434aed2f603..9134257b631 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -17,7 +17,7 @@ %h5.prepend-top-0 = _("Git strategy for pipelines") %p - = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code") + = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } @@ -47,7 +47,7 @@ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted - = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>") + = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' %hr diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index fdcd126e7a3..a8d4d4af93a 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,4 +1,6 @@ - project = find_project_for_result_blob(blob) +- return unless project + - file_name, blob = parse_search_result(blob) - blob_link = project_blob_path(project, tree_join(blob.ref, file_name)) diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml index 34de1c0695f..f32cff18fa8 100644 --- a/app/views/shared/_remote_mirror_update_button.html.haml +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -1,13 +1,6 @@ -- if @project.has_remote_mirror? - .append-bottom-default - - if remote_mirror.update_in_progress? - %span.btn.disabled - = icon("refresh spin") - Updating… - - else - = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do - = icon("refresh") - Update Now - - if @remote_mirror.last_successful_update_at - %p.inline.prepend-left-10 - Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. +- if remote_mirror.update_in_progress? + %button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') } + = icon("refresh spin") +- else + = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do + = icon("refresh") diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index ee6354b1c28..ee97f0172da 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -2,8 +2,8 @@ - primary = local_assigns.fetch(:primary, false) - panel_class = primary ? 'bg-primary text-white' : '' -.card{ class: panel_class } - .card-header +.card + .card-header{ class: panel_class } .title = title - if show_counter diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index 1f6e8f98bbb..43a87fd8397 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -19,7 +19,7 @@ - paragraph = _('Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.') % { notification_link: notification_link.html_safe } #{ paragraph.html_safe } .col-lg-8 - - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| + - notification_setting.email_events.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" .form-group .form-check{ class: ("prepend-top-0" if index == 0) } diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml index 7bcc54e7459..9d230d12be2 100644 --- a/app/views/shared/plugins/_index.html.haml +++ b/app/views/shared/plugins/_index.html.haml @@ -19,5 +19,5 @@ .monospace = File.basename(file) - else - %p.card.bg-light.text-center - No plugins found. + .card.bg-light.text-center + .nothing-here-block No plugins found. diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 0337680d79b..fa93307be31 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -1,56 +1,56 @@ = form_for runner, url: runner_form_url do |f| = form_errors(runner) .form-group.row - = label :active, "Active", class: 'col-form-label col-sm-2' + = label :active, _("Active"), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :active, { class: 'form-check-input' } - %span.light Paused Runners don't accept new jobs + %label.light{ for: :runner_active }= _("Paused Runners don't accept new jobs") .form-group.row - = label :protected, "Protected", class: 'col-form-label col-sm-2' + = label :protected, _("Protected"), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected' - %span.light This runner will only run on pipelines triggered on protected branches + %label.light{ for: :runner_access_level }= _('This runner will only run on pipelines triggered on protected branches') .form-group.row - = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2' + = label :run_untagged, _('Run untagged jobs'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :run_untagged, { class: 'form-check-input' } - %span.light Indicates whether this runner can pick jobs without tags + %label.light{ for: :runner_run_untagged }= _('Indicates whether this runner can pick jobs without tags') - unless runner.group_type? .form-group.row = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :locked, { class: 'form-check-input' } - %span.light= _('When a runner is locked, it cannot be assigned to other projects') + %label.light{ for: :runner_locked }= _('When a runner is locked, it cannot be assigned to other projects') .form-group.row = label_tag :token, class: 'col-form-label col-sm-2' do - Token + = _('Token') .col-sm-10 = f.text_field :token, class: 'form-control', readonly: true .form-group.row = label_tag :ip_address, class: 'col-form-label col-sm-2' do - IP Address + = _('IP Address') .col-sm-10 = f.text_field :ip_address, class: 'form-control', readonly: true .form-group.row = label_tag :description, class: 'col-form-label col-sm-2' do - Description + = _('Description') .col-sm-10 = f.text_field :description, class: 'form-control' .form-group.row = label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do - Maximum job timeout + = _('Maximum job timeout') .col-sm-10 = f.text_field :maximum_timeout_human_readable, class: 'form-control' - .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout + .form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout') .form-group.row = label_tag :tag_list, class: 'col-form-label col-sm-2' do - Tags + = _('Tags') .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas. + .form-text.text-muted= _('You can setup jobs to only use Runners with specific tags. Separate tags with commas.') .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb index 537b8fd5963..cfbbbffc8a6 100644 --- a/app/workers/detect_repository_languages_worker.rb +++ b/app/workers/detect_repository_languages_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DetectRepositoryLanguagesWorker include ApplicationWorker include ExceptionBacktrace diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb index 9d640c14963..481fde8c83d 100644 --- a/app/workers/todos_destroyer/confidential_issue_worker.rb +++ b/app/workers/todos_destroyer/confidential_issue_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class ConfidentialIssueWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/entity_leave_worker.rb b/app/workers/todos_destroyer/entity_leave_worker.rb index e62d9876f4a..7db3f6c84b4 100644 --- a/app/workers/todos_destroyer/entity_leave_worker.rb +++ b/app/workers/todos_destroyer/entity_leave_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class EntityLeaveWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/group_private_worker.rb b/app/workers/todos_destroyer/group_private_worker.rb index 3e47eec7461..21ec4abe478 100644 --- a/app/workers/todos_destroyer/group_private_worker.rb +++ b/app/workers/todos_destroyer/group_private_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class GroupPrivateWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/private_features_worker.rb b/app/workers/todos_destroyer/private_features_worker.rb index f457d5e0471..1e68f0fd5ae 100644 --- a/app/workers/todos_destroyer/private_features_worker.rb +++ b/app/workers/todos_destroyer/private_features_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class PrivateFeaturesWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/project_private_worker.rb b/app/workers/todos_destroyer/project_private_worker.rb index 7a853c36370..064e001530c 100644 --- a/app/workers/todos_destroyer/project_private_worker.rb +++ b/app/workers/todos_destroyer/project_private_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class ProjectPrivateWorker include ApplicationWorker diff --git a/bin/secpick b/bin/secpick index 5029fe57cfe..5e30c8e72c5 100755 --- a/bin/secpick +++ b/bin/secpick @@ -35,7 +35,9 @@ parser.parse! abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil) abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/ -branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze +branch = "#{options[:branch]}-#{options[:version]}" +branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-") +branch = branch.freeze stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" diff --git a/changelogs/unreleased/25990-web-terminal-improvements.yml b/changelogs/unreleased/25990-web-terminal-improvements.yml new file mode 100644 index 00000000000..99a4a82ea66 --- /dev/null +++ b/changelogs/unreleased/25990-web-terminal-improvements.yml @@ -0,0 +1,5 @@ +--- +title: Make terminal button more visible +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/2747-protected-environments-backend-ce.yml b/changelogs/unreleased/2747-protected-environments-backend-ce.yml new file mode 100644 index 00000000000..dcec74a33a7 --- /dev/null +++ b/changelogs/unreleased/2747-protected-environments-backend-ce.yml @@ -0,0 +1,5 @@ +--- +title: CE Port of Protected Environments backend +merge_request: 20859 +author: +type: other diff --git a/changelogs/unreleased/28930-add-project-reference-filter.yml b/changelogs/unreleased/28930-add-project-reference-filter.yml new file mode 100644 index 00000000000..c7679c5fe76 --- /dev/null +++ b/changelogs/unreleased/28930-add-project-reference-filter.yml @@ -0,0 +1,5 @@ +--- +title: Add the ability to reference projects in comments and other markdown text. +merge_request: 20285 +author: Reuben Pereira +type: added diff --git a/changelogs/unreleased/41996-copy-to-clipboard-tooltip-appears-under-modal.yml b/changelogs/unreleased/41996-copy-to-clipboard-tooltip-appears-under-modal.yml new file mode 100644 index 00000000000..e452a91d8b7 --- /dev/null +++ b/changelogs/unreleased/41996-copy-to-clipboard-tooltip-appears-under-modal.yml @@ -0,0 +1,5 @@ +--- +title: Solve tooltip appears under modal +merge_request: 21017 +author: +type: fixed diff --git a/changelogs/unreleased/45663-tag-quick-action-on-commit-comments.yml b/changelogs/unreleased/45663-tag-quick-action-on-commit-comments.yml new file mode 100644 index 00000000000..6d664511e6e --- /dev/null +++ b/changelogs/unreleased/45663-tag-quick-action-on-commit-comments.yml @@ -0,0 +1,5 @@ +--- +title: "`/tag` quick action on Commit comments" +merge_request: 20694 +author: Peter Leitzen +type: added diff --git a/changelogs/unreleased/47752-buttons-on-new-file-page-wrap-outside-of-container-for-long-branch-names.yml b/changelogs/unreleased/47752-buttons-on-new-file-page-wrap-outside-of-container-for-long-branch-names.yml new file mode 100644 index 00000000000..81ca632947d --- /dev/null +++ b/changelogs/unreleased/47752-buttons-on-new-file-page-wrap-outside-of-container-for-long-branch-names.yml @@ -0,0 +1,5 @@ +--- +title: Fix buttons on the new file page wrapping outside of the container +merge_request: 21015 +author: +type: fixed diff --git a/changelogs/unreleased/48320-cancel-a-created-job.yml b/changelogs/unreleased/48320-cancel-a-created-job.yml new file mode 100644 index 00000000000..3e7a9e9ae52 --- /dev/null +++ b/changelogs/unreleased/48320-cancel-a-created-job.yml @@ -0,0 +1,5 @@ +--- +title: Allows to cancel a Created job +merge_request: 20635 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/48942-rename-backlog-list-to-open-issue-boards.yml b/changelogs/unreleased/48942-rename-backlog-list-to-open-issue-boards.yml new file mode 100644 index 00000000000..26851fc2dec --- /dev/null +++ b/changelogs/unreleased/48942-rename-backlog-list-to-open-issue-boards.yml @@ -0,0 +1,5 @@ +--- +title: Change 'Backlog' list title to 'Open' in Issue Boards +merge_request: 21131 +author: +type: changed diff --git a/changelogs/unreleased/48967-disable-statement-timeout.yml b/changelogs/unreleased/48967-disable-statement-timeout.yml new file mode 100644 index 00000000000..2da800ed41e --- /dev/null +++ b/changelogs/unreleased/48967-disable-statement-timeout.yml @@ -0,0 +1,5 @@ +--- +title: disable_statement_timeout no longer leak to other migrations +merge_request: 20503 +author: +type: fixed diff --git a/changelogs/unreleased/49905-fix-checkboxes-runners.yml b/changelogs/unreleased/49905-fix-checkboxes-runners.yml new file mode 100644 index 00000000000..af40e5348b8 --- /dev/null +++ b/changelogs/unreleased/49905-fix-checkboxes-runners.yml @@ -0,0 +1,5 @@ +--- +title: Fix checkboxes on runner admin settings - The labels are now clickable +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml b/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml new file mode 100644 index 00000000000..82423092792 --- /dev/null +++ b/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml @@ -0,0 +1,5 @@ +--- +title: Add ability to suppress the global "You won't be able to use SSH" message +merge_request: 21027 +author: Ævar Arnfjörð Bjarmason +type: added diff --git a/changelogs/unreleased/50047-spam-logs-pagination.yml b/changelogs/unreleased/50047-spam-logs-pagination.yml new file mode 100644 index 00000000000..ca5f432cd8c --- /dev/null +++ b/changelogs/unreleased/50047-spam-logs-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Add gitlab theme to spam logs pagination +merge_request: 21145 +author: +type: fixed diff --git a/changelogs/unreleased/50063-add-missing-i18n-strings-to-issue-boards.yml b/changelogs/unreleased/50063-add-missing-i18n-strings-to-issue-boards.yml new file mode 100644 index 00000000000..ca17a41d611 --- /dev/null +++ b/changelogs/unreleased/50063-add-missing-i18n-strings-to-issue-boards.yml @@ -0,0 +1,5 @@ +--- +title: Added missing i18n strings to issue boards lables dropdown +merge_request: 21081 +author: +type: other diff --git a/changelogs/unreleased/50101-aritfacts-block.yml b/changelogs/unreleased/50101-aritfacts-block.yml new file mode 100644 index 00000000000..435e9d9d486 --- /dev/null +++ b/changelogs/unreleased/50101-aritfacts-block.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vue component for artifacts block on job page +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-erased-block.yml b/changelogs/unreleased/50101-erased-block.yml new file mode 100644 index 00000000000..5a5c9bc0fc4 --- /dev/null +++ b/changelogs/unreleased/50101-erased-block.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for erased block on job view +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-job-log-component.yml b/changelogs/unreleased/50101-job-log-component.yml new file mode 100644 index 00000000000..0759e7cfbd9 --- /dev/null +++ b/changelogs/unreleased/50101-job-log-component.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for job log trace +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-stuck-component.yml b/changelogs/unreleased/50101-stuck-component.yml new file mode 100644 index 00000000000..bfe4009a2b3 --- /dev/null +++ b/changelogs/unreleased/50101-stuck-component.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vvue component for warning block about stuck runners +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50126-blocked-user-card.yml b/changelogs/unreleased/50126-blocked-user-card.yml new file mode 100644 index 00000000000..a42d62e5530 --- /dev/null +++ b/changelogs/unreleased/50126-blocked-user-card.yml @@ -0,0 +1,5 @@ +--- +title: Fix blocked user card style +merge_request: 21095 +author: +type: fixed diff --git a/changelogs/unreleased/50180-fa-icon-google-audit.yml b/changelogs/unreleased/50180-fa-icon-google-audit.yml new file mode 100644 index 00000000000..fb1771a7570 --- /dev/null +++ b/changelogs/unreleased/50180-fa-icon-google-audit.yml @@ -0,0 +1,5 @@ +--- +title: Show google icon in audit log +merge_request: 21207 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/50243-auto-devops-behind-a-proxy.yml b/changelogs/unreleased/50243-auto-devops-behind-a-proxy.yml new file mode 100644 index 00000000000..0f6208d0c7a --- /dev/null +++ b/changelogs/unreleased/50243-auto-devops-behind-a-proxy.yml @@ -0,0 +1,6 @@ +--- +title: Vendor Auto-DevOps.gitlab-ci.yml with new proxy env vars passed through to + docker +merge_request: 21159 +author: kinolaev +type: added diff --git a/changelogs/unreleased/50257-fix-auto-devops-glibc-pubkey-url.yml b/changelogs/unreleased/50257-fix-auto-devops-glibc-pubkey-url.yml new file mode 100644 index 00000000000..e081dfe6093 --- /dev/null +++ b/changelogs/unreleased/50257-fix-auto-devops-glibc-pubkey-url.yml @@ -0,0 +1,5 @@ +--- +title: 'Auto-DevOps.gitlab-ci.yml: Update glibc package signing key URL' +merge_request: 21182 +author: sgerrand +type: fixed diff --git a/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml b/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml new file mode 100644 index 00000000000..eb20e34c466 --- /dev/null +++ b/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken JavaScript in IE11 +merge_request: 21214 +author: +type: fixed diff --git a/changelogs/unreleased/add_google_noto_color_emoji_font.yml b/changelogs/unreleased/add_google_noto_color_emoji_font.yml new file mode 100644 index 00000000000..9ba46262767 --- /dev/null +++ b/changelogs/unreleased/add_google_noto_color_emoji_font.yml @@ -0,0 +1,5 @@ +--- +title: Add Noto Color Emoji font support +merge_request: 19036 +author: Alexander Popov +type: changed diff --git a/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml b/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml new file mode 100644 index 00000000000..a1625193189 --- /dev/null +++ b/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml @@ -0,0 +1,5 @@ +--- +title: 'Auto-DevOps.gitlab-ci.yml: update glibc package to 2.28' +merge_request: 21191 +author: sgerrand +type: fixed diff --git a/changelogs/unreleased/bvl-add-czech.yml b/changelogs/unreleased/bvl-add-czech.yml new file mode 100644 index 00000000000..49e0e4a74b7 --- /dev/null +++ b/changelogs/unreleased/bvl-add-czech.yml @@ -0,0 +1,5 @@ +--- +title: Add Czech as an available language. +merge_request: 21201 +author: +type: added diff --git a/changelogs/unreleased/bvl-merge-base-api.yml b/changelogs/unreleased/bvl-merge-base-api.yml new file mode 100644 index 00000000000..78fb3ce0897 --- /dev/null +++ b/changelogs/unreleased/bvl-merge-base-api.yml @@ -0,0 +1,5 @@ +--- +title: Get the merge base of two refs through the API +merge_request: 20929 +author: +type: added diff --git a/changelogs/unreleased/ce-5666-optimize_querying_manageable_groups.yml b/changelogs/unreleased/ce-5666-optimize_querying_manageable_groups.yml new file mode 100644 index 00000000000..0c6a1071cdd --- /dev/null +++ b/changelogs/unreleased/ce-5666-optimize_querying_manageable_groups.yml @@ -0,0 +1,5 @@ +--- +title: Optimize querying User#manageable_groups +merge_request: 21050 +author: +type: performance diff --git a/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml b/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml new file mode 100644 index 00000000000..1453d39934b --- /dev/null +++ b/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml @@ -0,0 +1,5 @@ +--- +title: Expose all artifacts sizes in jobs api +merge_request: 20821 +author: Peter Marko +type: added diff --git a/changelogs/unreleased/feat-add-default-avatar-to-group.yml b/changelogs/unreleased/feat-add-default-avatar-to-group.yml new file mode 100644 index 00000000000..56d8f2ccd6d --- /dev/null +++ b/changelogs/unreleased/feat-add-default-avatar-to-group.yml @@ -0,0 +1,5 @@ +--- +title: Add default avatar to group +merge_request: 17271 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/fix-labels-list-item-height-with-no-description.yml b/changelogs/unreleased/fix-labels-list-item-height-with-no-description.yml new file mode 100644 index 00000000000..d215d034917 --- /dev/null +++ b/changelogs/unreleased/fix-labels-list-item-height-with-no-description.yml @@ -0,0 +1,5 @@ +--- +title: Fix label list item container height when there is no label description +merge_request: 21106 +author: +type: fixed diff --git a/changelogs/unreleased/fix-pipeline-fixture-seeder.yml b/changelogs/unreleased/fix-pipeline-fixture-seeder.yml new file mode 100644 index 00000000000..02b83062e07 --- /dev/null +++ b/changelogs/unreleased/fix-pipeline-fixture-seeder.yml @@ -0,0 +1,5 @@ +--- +title: Fix pipeline fixture seeder +merge_request: 21088 +author: +type: fixed diff --git a/changelogs/unreleased/fl-reduce-ee-conflicts-reports-code.yml b/changelogs/unreleased/fl-reduce-ee-conflicts-reports-code.yml new file mode 100644 index 00000000000..068681dfe19 --- /dev/null +++ b/changelogs/unreleased/fl-reduce-ee-conflicts-reports-code.yml @@ -0,0 +1,5 @@ +--- +title: Reduce differences between CE and EE code base in reports components +merge_request: +author: +type: other diff --git a/changelogs/unreleased/frozen-string-enable-app-mailers.yml b/changelogs/unreleased/frozen-string-enable-app-mailers.yml new file mode 100644 index 00000000000..2cd247ca76c --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-mailers.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen in app/mailers/**/*.rb +merge_request: 21147 +author: gfyoung +type: performance diff --git a/changelogs/unreleased/frozen-string-enable-app-models-even-more-still.yml b/changelogs/unreleased/frozen-string-enable-app-models-even-more-still.yml new file mode 100644 index 00000000000..a77f3baeed3 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-models-even-more-still.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string in rest of app/models/**/*.rb +merge_request: gfyoung +author: +type: performance diff --git a/changelogs/unreleased/frozen-string-enable-app-vestigial.yml b/changelogs/unreleased/frozen-string-enable-app-vestigial.yml new file mode 100644 index 00000000000..8cb7bd43784 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-vestigial.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string in vestigial app files +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/gitaly-install-path.yml b/changelogs/unreleased/gitaly-install-path.yml new file mode 100644 index 00000000000..4b24cd81dc7 --- /dev/null +++ b/changelogs/unreleased/gitaly-install-path.yml @@ -0,0 +1,5 @@ +--- +title: Remove storage path dependency of gitaly install task +merge_request: 21101 +author: +type: changed diff --git a/changelogs/unreleased/ide-delete-new-files-state.yml b/changelogs/unreleased/ide-delete-new-files-state.yml new file mode 100644 index 00000000000..500115d19d0 --- /dev/null +++ b/changelogs/unreleased/ide-delete-new-files-state.yml @@ -0,0 +1,5 @@ +--- +title: Fixed IDE deleting new files creating wrong state +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ide-header-buttons-tooltip.yml b/changelogs/unreleased/ide-header-buttons-tooltip.yml new file mode 100644 index 00000000000..4c8f6fd554f --- /dev/null +++ b/changelogs/unreleased/ide-header-buttons-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Added tooltips to tree list header +merge_request: 21138 +author: +type: added diff --git a/changelogs/unreleased/ide-open-empty-merge-request.yml b/changelogs/unreleased/ide-open-empty-merge-request.yml new file mode 100644 index 00000000000..05f2de5d31c --- /dev/null +++ b/changelogs/unreleased/ide-open-empty-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix empty merge requests not opening in the Web IDE +merge_request: 21102 +author: +type: fixed diff --git a/changelogs/unreleased/mk-bump-rainbow-gem.yml b/changelogs/unreleased/mk-bump-rainbow-gem.yml new file mode 100644 index 00000000000..31c003fb4d9 --- /dev/null +++ b/changelogs/unreleased/mk-bump-rainbow-gem.yml @@ -0,0 +1,5 @@ +--- +title: Fix bin/secpick error and security branch prefixing +merge_request: 21210 +author: +type: fixed diff --git a/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml new file mode 100644 index 00000000000..bcf3d2c8e16 --- /dev/null +++ b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml @@ -0,0 +1,6 @@ +--- +title: Combines emoji award spec files into single user_interacts_with_awards_in_issue_spec.rb + file +merge_request: 21126 +author: Nate Geslin +type: other diff --git a/changelogs/unreleased/rails5-fix-duplicate-gpg-signature.yml b/changelogs/unreleased/rails5-fix-duplicate-gpg-signature.yml new file mode 100644 index 00000000000..e31768773b1 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-duplicate-gpg-signature.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix specs duplicate key value violates unique constraint 'index_gpg_signatures_on_commit_sha' +merge_request: 21119 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/sh-bump-rugged-0-27-4.yml b/changelogs/unreleased/sh-bump-rugged-0-27-4.yml new file mode 100644 index 00000000000..50373cb81ad --- /dev/null +++ b/changelogs/unreleased/sh-bump-rugged-0-27-4.yml @@ -0,0 +1,5 @@ +--- +title: Bump rugged to 0.27.4 for security fixes +merge_request: +author: +type: security diff --git a/changelogs/unreleased/tz-mr-incremental-rendering.yml b/changelogs/unreleased/tz-mr-incremental-rendering.yml new file mode 100644 index 00000000000..a35fa200363 --- /dev/null +++ b/changelogs/unreleased/tz-mr-incremental-rendering.yml @@ -0,0 +1,4 @@ +title: Incremental rendering with Vue on merge request page +merge_request: 21063 +author: +type: performance diff --git a/changelogs/unreleased/visual-improvements-language-bar.yml b/changelogs/unreleased/visual-improvements-language-bar.yml new file mode 100644 index 00000000000..23cae22b962 --- /dev/null +++ b/changelogs/unreleased/visual-improvements-language-bar.yml @@ -0,0 +1,5 @@ +--- +title: Improve visuals of language bar on projects +merge_request: 21006 +author: +type: changed diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index d3a63aa2a78..5535c4a14e5 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -41,7 +41,7 @@ class Gitlab::Seeder::Pipelines when: 'manual', status: :skipped }, # notify stage - { name: 'slack', stage: 'notify', when: 'manual', status: :created }, + { name: 'slack', stage: 'notify', when: 'manual', status: :success }, ] EXTERNAL_JOBS = [ { name: 'jenkins', stage: 'test', status: :success, @@ -54,16 +54,10 @@ class Gitlab::Seeder::Pipelines def seed! pipelines.each do |pipeline| - begin - BUILDS.each { |opts| build_create!(pipeline, opts) } - EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) } - print '.' - rescue ActiveRecord::RecordInvalid - print 'F' - ensure - pipeline.update_duration - pipeline.update_status - end + BUILDS.each { |opts| build_create!(pipeline, opts) } + EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) } + pipeline.update_duration + pipeline.update_status end end @@ -87,7 +81,9 @@ class Gitlab::Seeder::Pipelines branch = merge_request.source_branch merge_request.commits.last(4).map do |commit| - create_pipeline!(project, branch, commit) + create_pipeline!(project, branch, commit).tap do |pipeline| + merge_request.update!(head_pipeline_id: pipeline.id) + end end end @@ -98,7 +94,7 @@ class Gitlab::Seeder::Pipelines def create_pipeline!(project, ref, commit) - project.pipelines.create(sha: commit.id, ref: ref, source: :push) + project.pipelines.create!(sha: commit.id, ref: ref, source: :push) end def build_create!(pipeline, opts = {}) @@ -111,24 +107,39 @@ class Gitlab::Seeder::Pipelines # block directly to `Ci::Build#create!`. setup_artifacts(build) + setup_test_reports(build) setup_build_log(build) build.project.environments. find_or_create_by(name: build.expanded_environment_name) - build.save + build.save! end end def setup_artifacts(build) - return unless %w[build test].include?(build.stage) + return unless build.stage == "build" artifacts_cache_file(artifacts_archive_path) do |file| - build.job_artifacts.build(project: build.project, file_type: :archive, file: file) + build.job_artifacts.build(project: build.project, file_type: :archive, file_format: :zip, file: file) end artifacts_cache_file(artifacts_metadata_path) do |file| - build.job_artifacts.build(project: build.project, file_type: :metadata, file: file) + build.job_artifacts.build(project: build.project, file_type: :metadata, file_format: :gzip, file: file) + end + end + + def setup_test_reports(build) + return unless build.stage == "test" && build.name == "rspec:osx" + + if build.ref == build.project.default_branch + artifacts_cache_file(test_reports_pass_path) do |file| + build.job_artifacts.build(project: build.project, file_type: :junit, file_format: :gzip, file: file) + end + else + artifacts_cache_file(test_reports_failed_path) do |file| + build.job_artifacts.build(project: build.project, file_type: :junit, file_format: :gzip, file: file) + end end end @@ -171,13 +182,21 @@ class Gitlab::Seeder::Pipelines Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' end + def test_reports_pass_path + Rails.root + 'spec/fixtures/junit/junit_ant.xml.gz' + end + + def test_reports_failed_path + Rails.root + 'spec/fixtures/junit/junit.xml.gz' + end + def artifacts_cache_file(file_path) - cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_") + file = Tempfile.new("artifacts") + file.close - FileUtils.copy(file_path, cache_path) - File.open(cache_path) do |file| - yield file - end + FileUtils.copy(file_path, file.path) + + yield(UploadedFile.new(file.path, filename: File.basename(file_path))) end end diff --git a/db/fixtures/development/23_spam_logs.rb b/db/fixtures/development/23_spam_logs.rb new file mode 100644 index 00000000000..81cc13e6b2d --- /dev/null +++ b/db/fixtures/development/23_spam_logs.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Db + module Fixtures + module Development + class SpamLog + def self.seed + Gitlab::Seeder.quiet do + (::SpamLog.default_per_page + 3).times do |i| + ::SpamLog.create( + user: self.random_user, + user_agent: FFaker::Lorem.sentence, + source_ip: FFaker::Internet.ip_v4_address, + title: FFaker::Lorem.sentence, + description: FFaker::Lorem.paragraph, + via_api: FFaker::Boolean.random, + submitted_as_ham: FFaker::Boolean.random, + recaptcha_verified: FFaker::Boolean.random) + print '.' + end + end + end + + def self.random_user + User.find(User.pluck(:id).sample) + end + end + end + end +end + +Db::Fixtures::Development::SpamLog.seed diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb index 461e7fb3a9b..2bf549d7ecd 100644 --- a/db/migrate/20160317092222_add_moved_to_to_issue.rb +++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb @@ -1,5 +1,5 @@ class AddMovedToToIssue < ActiveRecord::Migration def change - add_reference :issues, :moved_to, references: :issues + add_reference :issues, :moved_to, references: :issues # rubocop:disable Migration/AddReference end end diff --git a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb index 1199073ed3a..12352d98a62 100644 --- a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb +++ b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb @@ -106,11 +106,11 @@ class ProjectForeignKeysWithCascadingDeletes < ActiveRecord::Migration # Disables statement timeouts for the current connection. This is # necessary as removing of orphaned data might otherwise exceed the # statement timeout. - disable_statement_timeout + disable_statement_timeout do + remove_orphans(*queue.pop) until queue.empty? - remove_orphans(*queue.pop) until queue.empty? - - steal_from_queues(queues - [queue]) + steal_from_queues(queues - [queue]) + end end end end diff --git a/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb index db60c2087b9..a770ff63b4e 100644 --- a/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb +++ b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb @@ -25,8 +25,9 @@ class AddLowerPathIndexToRedirectRoutes < ActiveRecord::Migration # trivial to write a query that checks for an index. BUT there is a # convenient `IF EXISTS` parameter for `DROP INDEX`. if supports_drop_index_concurrently? - disable_statement_timeout - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + disable_statement_timeout do + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + end else execute "DROP INDEX IF EXISTS #{INDEX_NAME};" end diff --git a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb index d1a039ed551..130b24fe6f0 100644 --- a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb +++ b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb @@ -8,25 +8,25 @@ class AddIndexOnNamespacesLowerName < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - - if Gitlab::Database.version.to_f >= 9.5 - # Allow us to hot-patch the index manually ahead of the migration - execute "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{INDEX_NAME} ON namespaces (lower(name));" - else - execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON namespaces (lower(name));" + disable_statement_timeout do + if Gitlab::Database.version.to_f >= 9.5 + # Allow us to hot-patch the index manually ahead of the migration + execute "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{INDEX_NAME} ON namespaces (lower(name));" + else + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON namespaces (lower(name));" + end end end def down return unless Gitlab::Database.postgresql? - disable_statement_timeout - - if Gitlab::Database.version.to_f >= 9.2 - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" - else - execute "DROP INDEX IF EXISTS #{INDEX_NAME};" + disable_statement_timeout do + if Gitlab::Database.version.to_f >= 9.2 + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME};" + end end end end diff --git a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb index ab9971be074..53f82a31203 100644 --- a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb +++ b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb @@ -18,51 +18,51 @@ class ReworkRedirectRoutesIndexes < ActiveRecord::Migration OLD_INDEX_NAME_PATH_LOWER = "index_on_redirect_routes_lower_path" def up - disable_statement_timeout - - # this is a plain btree on a single boolean column. It'll never be - # selective enough to be valuable. This class is called by - # setup_postgresql.rake so it needs to be able to handle this - # index not existing. - if index_exists?(:redirect_routes, :permanent) - remove_concurrent_index(:redirect_routes, :permanent) - end + disable_statement_timeout do + # this is a plain btree on a single boolean column. It'll never be + # selective enough to be valuable. This class is called by + # setup_postgresql.rake so it needs to be able to handle this + # index not existing. + if index_exists?(:redirect_routes, :permanent) + remove_concurrent_index(:redirect_routes, :permanent) + end - # If we're on MySQL then the existing index on path is ok. But on - # Postgres we need to clean things up: - return unless Gitlab::Database.postgresql? + # If we're on MySQL then the existing index on path is ok. But on + # Postgres we need to clean things up: + break unless Gitlab::Database.postgresql? - if_not_exists = Gitlab::Database.version.to_f >= 9.5 ? "IF NOT EXISTS" : "" + if_not_exists = Gitlab::Database.version.to_f >= 9.5 ? "IF NOT EXISTS" : "" - # Unique index on lower(path) across both types of redirect_routes: - execute("CREATE UNIQUE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_UNIQUE} ON redirect_routes (lower(path) varchar_pattern_ops);") + # Unique index on lower(path) across both types of redirect_routes: + execute("CREATE UNIQUE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_UNIQUE} ON redirect_routes (lower(path) varchar_pattern_ops);") - # Make two indexes on path -- one for permanent and one for temporary routes: - execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") - execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") + # Make two indexes on path -- one for permanent and one for temporary routes: + execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") + execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") - # Remove the old indexes: + # Remove the old indexes: - # This one needed to be on lower(path) but wasn't so it's replaced with the two above - execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_TPOPS};" + # This one needed to be on lower(path) but wasn't so it's replaced with the two above + execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_TPOPS};" - # This one isn't needed because we only ever do = and LIKE on this - # column so the varchar_pattern_ops index is sufficient - execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_LOWER};" + # This one isn't needed because we only ever do = and LIKE on this + # column so the varchar_pattern_ops index is sufficient + execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_LOWER};" + end end def down - disable_statement_timeout + disable_statement_timeout do + add_concurrent_index(:redirect_routes, :permanent) - add_concurrent_index(:redirect_routes, :permanent) + break unless Gitlab::Database.postgresql? - return unless Gitlab::Database.postgresql? + execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_TPOPS} ON redirect_routes (path varchar_pattern_ops);") + execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_LOWER} ON redirect_routes (LOWER(path));") - execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_TPOPS} ON redirect_routes (path varchar_pattern_ops);") - execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_LOWER} ON redirect_routes (LOWER(path));") - - execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_UNIQUE};") - execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};") - execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};") + execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_UNIQUE};") + execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};") + execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};") + end end end diff --git a/db/migrate/20180403035759_create_project_ci_cd_settings.rb b/db/migrate/20180403035759_create_project_ci_cd_settings.rb index 06856af6204..173e662cffc 100644 --- a/db/migrate/20180403035759_create_project_ci_cd_settings.rb +++ b/db/migrate/20180403035759_create_project_ci_cd_settings.rb @@ -13,16 +13,16 @@ class CreateProjectCiCdSettings < ActiveRecord::Migration end end - disable_statement_timeout + disable_statement_timeout do + # This particular INSERT will take between 10 and 20 seconds. + execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' - # This particular INSERT will take between 10 and 20 seconds. - execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' + # We add the index and foreign key separately so the above INSERT statement + # takes as little time as possible. + add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true) - # We add the index and foreign key separately so the above INSERT statement - # takes as little time as possible. - add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true) - - add_foreign_key_with_retry + add_foreign_key_with_retry + end end def down diff --git a/db/migrate/20180420010616_cleanup_build_stage_migration.rb b/db/migrate/20180420010616_cleanup_build_stage_migration.rb index 24777294101..5e9fe756efd 100644 --- a/db/migrate/20180420010616_cleanup_build_stage_migration.rb +++ b/db/migrate/20180420010616_cleanup_build_stage_migration.rb @@ -14,48 +14,50 @@ class CleanupBuildStageMigration < ActiveRecord::Migration end def up - disable_statement_timeout - - ## - # We steal from the background migrations queue to catch up with the - # scheduled migrations set. - # - Gitlab::BackgroundMigration.steal('MigrateBuildStage') - - ## - # We add temporary index, to make iteration over batches more performant. - # Conditional here is to avoid the need of doing that in a separate - # migration file to make this operation idempotent. - # - unless index_exists_by_name?(:ci_builds, TMP_INDEX) - add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', name: TMP_INDEX) - end - - ## - # We check if there are remaining rows that should be migrated (for example - # if Sidekiq / Redis fails / is restarted, what could result in not all - # background migrations being executed correctly. - # - # We migrate remaining rows synchronously in a blocking way, to make sure - # that when this migration is done we are confident that all rows are - # already migrated. - # - Build.where('stage_id IS NULL').each_batch(of: 50) do |batch| - range = batch.pluck('MIN(id)', 'MAX(id)').first - - Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range) + disable_statement_timeout do + ## + # We steal from the background migrations queue to catch up with the + # scheduled migrations set. + # + Gitlab::BackgroundMigration.steal('MigrateBuildStage') + + ## + # We add temporary index, to make iteration over batches more performant. + # Conditional here is to avoid the need of doing that in a separate + # migration file to make this operation idempotent. + # + unless index_exists_by_name?(:ci_builds, TMP_INDEX) + add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', name: TMP_INDEX) + end + + ## + # We check if there are remaining rows that should be migrated (for example + # if Sidekiq / Redis fails / is restarted, what could result in not all + # background migrations being executed correctly. + # + # We migrate remaining rows synchronously in a blocking way, to make sure + # that when this migration is done we are confident that all rows are + # already migrated. + # + Build.where('stage_id IS NULL').each_batch(of: 50) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range) + end + + ## + # We remove temporary index, because it is not required during standard + # operations and runtime. + # + remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) end - - ## - # We remove temporary index, because it is not required during standard - # operations and runtime. - # - remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) end def down if index_exists_by_name?(:ci_builds, TMP_INDEX) - remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) + disable_statement_timeout do + remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) + end end end end diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb index d6f25d3d4ab..74f3673bb03 100644 --- a/db/migrate/20180504195842_project_name_lower_index.rb +++ b/db/migrate/20180504195842_project_name_lower_index.rb @@ -13,20 +13,20 @@ class ProjectNameLowerIndex < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - - execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))" + disable_statement_timeout do + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))" + end end def down return unless Gitlab::Database.postgresql? - disable_statement_timeout - - if supports_drop_index_concurrently? - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}" - else - execute "DROP INDEX IF EXISTS #{INDEX_NAME}" + disable_statement_timeout do + if supports_drop_index_concurrently? + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME}" + end end end end diff --git a/db/migrate/20180702124358_remove_orphaned_routes.rb b/db/migrate/20180702124358_remove_orphaned_routes.rb index 6f6e289ba87..4068e479b6c 100644 --- a/db/migrate/20180702124358_remove_orphaned_routes.rb +++ b/db/migrate/20180702124358_remove_orphaned_routes.rb @@ -28,16 +28,16 @@ class RemoveOrphanedRoutes < ActiveRecord::Migration # which is pretty close to our 15 second statement timeout. To ensure a # smooth deployment procedure we disable the statement timeouts for this # migration, just in case. - disable_statement_timeout - - # On GitLab.com there are around 4000 orphaned project routes, and around - # 150 orphaned namespace routes. - [ - Route.orphaned_project_routes, - Route.orphaned_namespace_routes - ].each do |relation| - relation.each_batch(of: 1_000) do |batch| - batch.delete_all + disable_statement_timeout do + # On GitLab.com there are around 4000 orphaned project routes, and around + # 150 orphaned namespace routes. + [ + Route.orphaned_project_routes, + Route.orphaned_namespace_routes + ].each do |relation| + relation.each_batch(of: 1_000) do |batch| + batch.delete_all + end end end end diff --git a/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb new file mode 100644 index 00000000000..e3019af2cc9 --- /dev/null +++ b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddUserShowAddSshKeyMessageToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :user_show_add_ssh_key_message, :boolean, default: true, allow_null: false + end + + def down + remove_column :application_settings, :user_show_add_ssh_key_message + end +end diff --git a/db/optional_migrations/composite_primary_keys.rb b/db/optional_migrations/composite_primary_keys.rb index d45705021b0..b330da13d43 100644 --- a/db/optional_migrations/composite_primary_keys.rb +++ b/db/optional_migrations/composite_primary_keys.rb @@ -29,18 +29,20 @@ class CompositePrimaryKeysMigration < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - TABLES.each do |index| - add_primary_key(index) + disable_statement_timeout do + TABLES.each do |index| + add_primary_key(index) + end end end def down return unless Gitlab::Database.postgresql? - disable_statement_timeout - TABLES.each do |index| - remove_primary_key(index) + disable_statement_timeout do + TABLES.each do |index| + remove_primary_key(index) + end end end diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb index bba37e32c01..845c6f0557f 100644 --- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb +++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb @@ -8,9 +8,9 @@ class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration DOWNTIME = false def up - disable_statement_timeout - - update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1) + disable_statement_timeout do + update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1) + end end def down diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb index b0b58ab3011..079f0e7511f 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -7,12 +7,12 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - if Gitlab::Database.mysql? up_mysql else - up_postgres + disable_statement_timeout do + up_postgres + end end end diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb index 81e9d050668..5df3ab71648 100644 --- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb +++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb @@ -7,20 +7,20 @@ class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - pipelines = Arel::Table.new(:ci_pipelines) merge_requests = Arel::Table.new(:merge_requests) - head_id = pipelines - .project(Arel::Nodes::NamedFunction.new('max', [pipelines[:id]])) - .from(pipelines) - .where(pipelines[:ref].eq(merge_requests[:source_branch])) - .where(pipelines[:project_id].eq(merge_requests[:source_project_id])) + disable_statement_timeout do + head_id = pipelines + .project(Arel::Nodes::NamedFunction.new('max', [pipelines[:id]])) + .from(pipelines) + .where(pipelines[:ref].eq(merge_requests[:source_branch])) + .where(pipelines[:project_id].eq(merge_requests[:source_project_id])) - sub_query = Arel::Nodes::SqlLiteral.new(Arel::Nodes::Grouping.new(head_id).to_sql) + sub_query = Arel::Nodes::SqlLiteral.new(Arel::Nodes::Grouping.new(head_id).to_sql) - update_column_in_batches(:merge_requests, :head_pipeline_id, sub_query) + update_column_in_batches(:merge_requests, :head_pipeline_id, sub_query) + end end def down diff --git a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb index 2125cc046e5..c996ddbec84 100644 --- a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb +++ b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb @@ -87,16 +87,16 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration ].freeze def up - disable_statement_timeout - - TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) } - PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) } - GROUP_ROUTES.each { |route| rename_child_paths(route) } + disable_statement_timeout do + TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) } + PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) } + GROUP_ROUTES.each { |route| rename_child_paths(route) } + end end def down - disable_statement_timeout - - revert_renames + disable_statement_timeout do + revert_renames + end end end diff --git a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb index afd4db183c2..736aff77f02 100644 --- a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb +++ b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb @@ -6,17 +6,17 @@ class MigratePipelineStages < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - - execute <<-SQL.strip_heredoc - INSERT INTO ci_stages (project_id, pipeline_id, name) - SELECT project_id, commit_id, stage FROM ci_builds - WHERE stage IS NOT NULL - AND stage_id IS NULL - AND EXISTS (SELECT 1 FROM projects WHERE projects.id = ci_builds.project_id) - AND EXISTS (SELECT 1 FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id) - GROUP BY project_id, commit_id, stage - ORDER BY MAX(stage_idx) - SQL + disable_statement_timeout do + execute <<-SQL.strip_heredoc + INSERT INTO ci_stages (project_id, pipeline_id, name) + SELECT project_id, commit_id, stage FROM ci_builds + WHERE stage IS NOT NULL + AND stage_id IS NULL + AND EXISTS (SELECT 1 FROM projects WHERE projects.id = ci_builds.project_id) + AND EXISTS (SELECT 1 FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id) + GROUP BY project_id, commit_id, stage + ORDER BY MAX(stage_idx) + SQL + end end end diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb index 31a73bb3b27..a7bfba0ab2b 100644 --- a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb +++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb @@ -7,22 +7,22 @@ class MigrateBuildStageReferenceAgain < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - stage_id = Arel.sql <<-SQL.strip_heredoc (SELECT id FROM ci_stages WHERE ci_stages.pipeline_id = ci_builds.commit_id AND ci_stages.name = ci_builds.stage) SQL - update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| - query.where(table[:stage_id].eq(nil)) + disable_statement_timeout do + update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| + query.where(table[:stage_id].eq(nil)) + end end end def down - disable_statement_timeout - - update_column_in_batches(:ci_builds, :stage_id, nil) + disable_statement_timeout do + update_column_in_batches(:ci_builds, :stage_id, nil) + end end end diff --git a/db/post_migrate/20170711145558_migrate_stages_statuses.rb b/db/post_migrate/20170711145558_migrate_stages_statuses.rb index 65755c0e824..265f7317b9b 100644 --- a/db/post_migrate/20170711145558_migrate_stages_statuses.rb +++ b/db/post_migrate/20170711145558_migrate_stages_statuses.rb @@ -26,9 +26,9 @@ class MigrateStagesStatuses < ActiveRecord::Migration end def down - disable_statement_timeout - - # rubocop:disable Migration/UpdateLargeTable - update_column_in_batches(:ci_stages, :status, nil) + disable_statement_timeout do + # rubocop:disable Migration/UpdateLargeTable + update_column_in_batches(:ci_stages, :status, nil) + end end end diff --git a/db/post_migrate/20171207150343_remove_soft_removed_objects.rb b/db/post_migrate/20171207150343_remove_soft_removed_objects.rb index 3e2dedfdd6a..3109b6dbf8e 100644 --- a/db/post_migrate/20171207150343_remove_soft_removed_objects.rb +++ b/db/post_migrate/20171207150343_remove_soft_removed_objects.rb @@ -78,12 +78,12 @@ class RemoveSoftRemovedObjects < ActiveRecord::Migration MODELS = [Issue, MergeRequest, CiPipelineSchedule, CiTrigger].freeze def up - disable_statement_timeout - - remove_personal_routes - remove_personal_namespaces - remove_group_namespaces - remove_simple_soft_removed_rows + disable_statement_timeout do + remove_personal_routes + remove_personal_namespaces + remove_group_namespaces + remove_simple_soft_removed_rows + end end def down diff --git a/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb b/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb index 61ea85eb2a7..269f1287f91 100644 --- a/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb +++ b/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb @@ -38,29 +38,29 @@ class RemoveRedundantPipelineStages < ActiveRecord::Migration end def remove_redundant_pipeline_stages! - disable_statement_timeout - - redundant_stages_ids = <<~SQL - SELECT id FROM ci_stages WHERE (pipeline_id, name) IN ( - SELECT pipeline_id, name FROM ci_stages - GROUP BY pipeline_id, name HAVING COUNT(*) > 1 - ) - SQL - - execute <<~SQL - UPDATE ci_builds SET stage_id = NULL WHERE stage_id IN (#{redundant_stages_ids}) - SQL - - if Gitlab::Database.postgresql? - execute <<~SQL - DELETE FROM ci_stages WHERE id IN (#{redundant_stages_ids}) + disable_statement_timeout do + redundant_stages_ids = <<~SQL + SELECT id FROM ci_stages WHERE (pipeline_id, name) IN ( + SELECT pipeline_id, name FROM ci_stages + GROUP BY pipeline_id, name HAVING COUNT(*) > 1 + ) SQL - else # We can't modify a table we are selecting from on MySQL + execute <<~SQL - DELETE a FROM ci_stages AS a, ci_stages AS b - WHERE a.pipeline_id = b.pipeline_id AND a.name = b.name - AND a.id <> b.id + UPDATE ci_builds SET stage_id = NULL WHERE stage_id IN (#{redundant_stages_ids}) SQL + + if Gitlab::Database.postgresql? + execute <<~SQL + DELETE FROM ci_stages WHERE id IN (#{redundant_stages_ids}) + SQL + else # We can't modify a table we are selecting from on MySQL + execute <<~SQL + DELETE a FROM ci_stages AS a, ci_stages AS b + WHERE a.pipeline_id = b.pipeline_id AND a.name = b.name + AND a.id <> b.id + SQL + end end end end diff --git a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb index db5165dbe70..aa19732ca1c 100644 --- a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb +++ b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb @@ -15,10 +15,10 @@ class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration # ReworkRedirectRoutesIndexes: # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16211 if Gitlab::Database.postgresql? - disable_statement_timeout - - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};" - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};" + disable_statement_timeout do + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};" + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};" + end end remove_column(:redirect_routes, :permanent) @@ -28,10 +28,10 @@ class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration add_column(:redirect_routes, :permanent, :boolean) if Gitlab::Database.postgresql? - disable_statement_timeout - - execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") - execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") + disable_statement_timeout do + execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") + execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") + end end end end diff --git a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb index d6fb4f06695..ca9212fae27 100644 --- a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb +++ b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb @@ -20,10 +20,10 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - - unless index_exists_by_name?(:redirect_routes, INDEX_NAME) - execute("CREATE UNIQUE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (lower(path) varchar_pattern_ops);") + disable_statement_timeout do + unless index_exists_by_name?(:redirect_routes, INDEX_NAME) + execute("CREATE UNIQUE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (lower(path) varchar_pattern_ops);") + end end end diff --git a/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb b/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb index e19387bce1e..c32123454f9 100644 --- a/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb +++ b/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb @@ -17,13 +17,13 @@ class RescheduleBuildsStagesMigration < ActiveRecord::Migration end def up - disable_statement_timeout - - Build.where('stage_id IS NULL').tap do |relation| - queue_background_migration_jobs_by_range_at_intervals(relation, - MIGRATION, - 5.minutes, - batch_size: BATCH_SIZE) + disable_statement_timeout do + Build.where('stage_id IS NULL').tap do |relation| + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + 5.minutes, + batch_size: BATCH_SIZE) + end end end diff --git a/db/post_migrate/20180420080616_schedule_stages_index_migration.rb b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb index 1d0daad002f..eb82f098639 100644 --- a/db/post_migrate/20180420080616_schedule_stages_index_migration.rb +++ b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb @@ -13,13 +13,13 @@ class ScheduleStagesIndexMigration < ActiveRecord::Migration end def up - disable_statement_timeout - - Stage.all.tap do |relation| - queue_background_migration_jobs_by_range_at_intervals(relation, - MIGRATION, - 5.minutes, - batch_size: BATCH_SIZE) + disable_statement_timeout do + Stage.all.tap do |relation| + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + 5.minutes, + batch_size: BATCH_SIZE) + end end end diff --git a/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb index 73c23dffca0..5418f442e79 100644 --- a/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb +++ b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb @@ -12,32 +12,34 @@ class CleanupStagesPositionMigration < ActiveRecord::Migration end def up - disable_statement_timeout + disable_statement_timeout do + Gitlab::BackgroundMigration.steal('MigrateStageIndex') - Gitlab::BackgroundMigration.steal('MigrateStageIndex') - - unless index_exists_by_name?(:ci_stages, TMP_INDEX_NAME) - add_concurrent_index(:ci_stages, :id, where: 'position IS NULL', name: TMP_INDEX_NAME) - end + unless index_exists_by_name?(:ci_stages, TMP_INDEX_NAME) + add_concurrent_index(:ci_stages, :id, where: 'position IS NULL', name: TMP_INDEX_NAME) + end - migratable = <<~SQL - position IS NULL AND EXISTS ( - SELECT 1 FROM ci_builds WHERE stage_id = ci_stages.id AND stage_idx IS NOT NULL - ) - SQL + migratable = <<~SQL + position IS NULL AND EXISTS ( + SELECT 1 FROM ci_builds WHERE stage_id = ci_stages.id AND stage_idx IS NOT NULL + ) + SQL - Stages.where(migratable).each_batch(of: 1000) do |batch| - batch.pluck(:id).each do |stage| - Gitlab::BackgroundMigration::MigrateStageIndex.new.perform(stage, stage) + Stages.where(migratable).each_batch(of: 1000) do |batch| + batch.pluck(:id).each do |stage| + Gitlab::BackgroundMigration::MigrateStageIndex.new.perform(stage, stage) + end end - end - remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + end end def down if index_exists_by_name?(:ci_stages, TMP_INDEX_NAME) - remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + disable_statement_timeout do + remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + end end end end diff --git a/db/schema.rb b/db/schema.rb index f1d8f4df3b7..1288a98745c 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: 20180807153545) do +ActiveRecord::Schema.define(version: 20180808162000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -170,6 +170,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do t.boolean "hide_third_party_offers", default: false, null: false t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false + t.boolean "user_show_add_ssh_key_message", default: true, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/README.md b/doc/README.md index a814c787f94..4248f62c08c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -133,6 +133,7 @@ scales to run your tests faster. - [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab. - [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request. - [Pipeline Graphs](ci/pipelines.md#pipeline-graphs) +- [JUnit test reports](ci/junit_test_reports.md) ### Package diff --git a/doc/administration/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md index 8968afba01b..9edccd25ced 100644 --- a/doc/administration/operations/ssh_certificates.md +++ b/doc/administration/operations/ssh_certificates.md @@ -163,3 +163,20 @@ Such a restriction can currently be hacked in by e.g. providing a custom `AuthorizedKeysCommand` which checks if the discovered key-ID returned from `gitlab-shell-authorized-keys-check` is a deploy key or not (all non-deploy keys should be refused). + +## Disabling the global warning about users lacking SSH keys + +By default GitLab will show a "You won't be able to pull or push +project code via SSH" warning to users who have not uploaded an SSH +key to their profile. + +This is counterproductive when using SSH certificates, since users +aren't expected to upload their own keys. + +To disable this warning globally, go to "Application settings -> +Account and limit settings" and disable the "Show user add SSH key +message" setting. + +This setting was added specifically for use with SSH certificates, but +can be turned off without using them if you'd like to hide the warning +for some other reason. diff --git a/doc/api/boards.md b/doc/api/boards.md index 246de50323e..5f006f4f012 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -144,7 +144,7 @@ Example response: ## List board lists Get a list of the board's lists. -Does not include `backlog` and `closed` lists +Does not include `open` and `closed` lists ``` GET /projects/:id/boards/:board_id/lists diff --git a/doc/api/events.md b/doc/api/events.md index f4d26c4de1c..1b6c4d437dd 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -255,7 +255,7 @@ Example response: Get a list of visible events for a particular project. ``` -GET /:project_id/events +GET /projects/:project_id/events ``` Parameters: diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md index 45a8544d6b1..373904e50c4 100644 --- a/doc/api/group_boards.md +++ b/doc/api/group_boards.md @@ -119,7 +119,7 @@ Example response: ## List board lists Get a list of the board's lists. -Does not include `backlog` and `closed` lists +Does not include `open` and `closed` lists ``` GET /groups/:id/boards/:board_id/lists diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 9a950097675..4bf65a8fafd 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -33,7 +33,6 @@ Example of response }, "coverage": null, "created_at": "2015-12-24T15:51:21.727Z", - "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "artifacts_expire_at": "2016-01-23T17:54:24.921Z", "id": 6, @@ -45,6 +44,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:24.729Z", @@ -82,6 +82,12 @@ Example of response "filename": "artifacts.zip", "size": 1000 }, + "artifacts": [ + {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"}, + {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"}, + {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"}, + {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"} + ], "finished_at": "2015-12-24T17:54:27.895Z", "artifacts_expire_at": "2016-01-23T17:54:27.895Z", "id": 7, @@ -93,6 +99,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:27.722Z", @@ -151,7 +158,6 @@ Example of response }, "coverage": null, "created_at": "2015-12-24T15:51:21.727Z", - "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "artifacts_expire_at": "2016-01-23T17:54:24.921Z", "id": 6, @@ -163,6 +169,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:24.729Z", @@ -200,6 +207,12 @@ Example of response "filename": "artifacts.zip", "size": 1000 }, + "artifacts": [ + {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"}, + {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"}, + {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"}, + {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"} + ], "finished_at": "2015-12-24T17:54:27.895Z", "artifacts_expire_at": "2016-01-23T17:54:27.895Z", "id": 7, @@ -211,6 +224,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:27.722Z", @@ -267,7 +281,6 @@ Example of response }, "coverage": null, "created_at": "2015-12-24T15:51:21.880Z", - "artifacts_file": null, "finished_at": "2015-12-24T17:54:31.198Z", "artifacts_expire_at": "2016-01-23T17:54:31.198Z", "id": 8, @@ -279,6 +292,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:30.733Z", @@ -458,11 +472,11 @@ Example of response }, "coverage": null, "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, "finished_at": "2016-01-11T10:14:09.526Z", "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": null, @@ -505,11 +519,11 @@ Example of response }, "coverage": null, "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, "finished_at": null, "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": null, @@ -559,6 +573,7 @@ Example of response "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "created_at": "2016-01-11T10:13:33.506Z", @@ -610,6 +625,7 @@ Example response: "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "created_at": "2016-01-11T10:13:33.506Z", @@ -654,11 +670,11 @@ Example of response }, "coverage": null, "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, "finished_at": null, "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": null, diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index 682b90361bd..165b9a11c7a 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -15,7 +15,7 @@ mention custom ``` -If the `custom` level is used, specific email events can be controlled. Notification email events are defined in the `NotificationSetting::EMAIL_EVENTS` model variable. Currently, these events are recognized: +If the `custom` level is used, specific email events can be controlled. Available events are returned by `NotificationSetting.email_events`. Currently, these events are recognized: ``` new_note diff --git a/doc/api/projects.md b/doc/api/projects.md index f360b49c293..bda4164ee92 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -258,8 +258,7 @@ GET /projects?custom_attributes[key]=value&custom_attributes[other_key]=other_va ## List user projects -Get a list of visible projects for the given user. When accessed without -authentication, only public projects are returned. +Get a list of visible projects owned by the given user. When accessed without authentication, only public projects are returned. ``` GET /users/:user_id/projects diff --git a/doc/api/repositories.md b/doc/api/repositories.md index cb816bbd712..a4fdeca162e 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -204,3 +204,39 @@ Response: "deletions": 244 }] ``` + +## Merge Base + +Get the common ancestor for 2 refs (commit SHAs, branch names or tags). + +``` +GET /projects/:id/repository/merge_base +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `refs` | array | yes | The refs to find the common ancestor of, for now only 2 refs are supported | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257dcb821665ab5110318fc58a007bd104ed&refs[]=0031876facac3f2b2702a0e53a26e89939a42209" +``` + +Example response: + +```json +{ + "id": "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863", + "short_id": "1a0b36b3", + "title": "Initial commit", + "created_at": "2014-02-27T08:03:18.000Z", + "parent_ids": [], + "message": "Initial commit\n", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "authored_date": "2014-02-27T08:03:18.000Z", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T08:03:18.000Z" +} +``` diff --git a/doc/api/settings.md b/doc/api/settings.md index 68fc56b1fa3..b480d62e16a 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -56,7 +56,8 @@ Example response: "enforce_terms": true, "terms": "Hello world!", "performance_bar_allowed_group_id": 42, - "instance_statistics_visibility_private": false + "instance_statistics_visibility_private": false, + "user_show_add_ssh_key_message": true } ``` @@ -161,6 +162,8 @@ PUT /application/settings | `enforce_terms` | boolean | no | Enforce application ToS to all users | | `terms` | text | yes (if `enforce_terms` is true) | Markdown content for the ToS | | `instance_statistics_visibility_private` | boolean | no | When set to `true` Instance statistics will only be available to admins | +| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push ++project code via SSH" warning shown to users with no uploaded SSH key | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal @@ -206,6 +209,7 @@ Example response: "enforce_terms": true, "terms": "Hello world!", "performance_bar_allowed_group_id": 42, - "instance_statistics_visibility_private": false + "instance_statistics_visibility_private": false, + "user_show_add_ssh_key_message": true } ``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index dd424470b67..7b8db6cfa8f 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -34,6 +34,7 @@ Example response: "push_events":true, "tag_push_events":false, "merge_requests_events": true, + "repository_update_events": true, "enable_ssl_verification":true } ] @@ -56,6 +57,7 @@ POST /hooks | `push_events` | boolean | no | When true, the hook will fire on push events | | `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed | | `merge_requests_events` | boolean | no | Trigger hook on merge requests events | +| `repository_update_events` | boolean | no | Trigger hook on repository update events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | Example request: @@ -75,6 +77,7 @@ Example response: "push_events":true, "tag_push_events":false, "merge_requests_events": true, + "repository_update_events": true, "enable_ssl_verification":true } ] @@ -127,4 +130,4 @@ Example request: ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2 -``` +```
\ No newline at end of file diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 811f4d1f07a..8eb96ae10b2 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -43,6 +43,10 @@ There's also a collection of repositories with [example projects](https://gitlab - [Using `dpl` as deployment tool](deployment/README.md) - [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) +## Test Reports + +[Collect test reports in Verify stage](../junit_test_reports.md). + ## Code Quality analysis **(Starter)** [Analyze your project's Code Quality](code_quality.md). diff --git a/doc/ci/img/junit_test_report.png b/doc/ci/img/junit_test_report.png Binary files differnew file mode 100644 index 00000000000..ad098eb457f --- /dev/null +++ b/doc/ci/img/junit_test_report.png diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md new file mode 100644 index 00000000000..5ae8ecaafa6 --- /dev/null +++ b/doc/ci/junit_test_reports.md @@ -0,0 +1,102 @@ +# JUnit test reports + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45318) in GitLab 11.2. +Requires GitLab Runner 11.2 and above. + +## Overview + +It is very common that a [CI/CD pipeline](pipelines.md) contains a +test job that will verify your code. +If the tests fail, the pipeline fails and users get notified. The person that +works on the merge request will have to check the job logs and see where the +tests failed so that they can fix them. + +You can configure your job to use JUnit test reports, and GitLab will display a +report on the merge request so that it's easier and faster to identify the +failure without having to check the entire log. + +## Use cases + +Consider the following workflow: + +1. Your `master` branch is rock solid, your project is using GitLab CI/CD and + your pipelines indicate that there isn't anything broken. +1. Someone from you team submits a merge request, a test fails and the pipeline + gets the known red icon. To investigate more, you have to go through the job + logs to figure out the cause of the failed test, which usually contain + thousands of lines. +1. You configure the JUnit test reports and immediately GitLab collects and + exposes them in the merge request. No more searching in the job logs. +1. Your development and debugging workflow becomes easier, faster and efficient. + +## How it works + +First, GitLab Runner uploads all JUnit XML files as artifacts to GitLab. Then, +when you visit a merge request, GitLab starts comparing the head and base branch's +JUnit test reports, where: + +- The base branch is the target branch (usually `master`). +- The head branch is the source branch (the latest pipeline in each merge request). + +The reports panel has a summary showing how many tests failed and how many were fixed. +If no comparison can be done because data for the base branch is not available, +the panel will just show the list of failed tests for head. + +There are three types of results: + +1. **Newly failed tests:** Test cases which passed on base branch and failed on head branch +1. **Existing failures:** Test cases which failed on base branch and failed on head branch +1. **Resolved failures:** Test cases which failed on base branch and passed on head branch + +Each entry in the panel will show the test name and its type from the list +above. Clicking on the test name will open a modal window with details of its +execution time and the error output. + +![Test Reports Widget](img/junit_test_report.png) + +## How to set it up + +NOTE: **Note:** +For a list of supported languages on JUnit tests, check the +[Wikipedia article](https://en.wikipedia.org/wiki/JUnit#Ports). + +To enable the JUnit reports in merge requests, you need to add +[`artifacts:reports:junit`](yaml/README.md#artifacts-reports-junit) +in `.gitlab-ci.yml`, and specify the path(s) of the generated test reports. + +In the following examples, the job in the `test` stage runs and GitLab +collects the JUnit test report from each job. After each job is executed, the +XML reports are stored in GitLab as artifacts and their results are shown in the +merge request widget. + +### Ruby example + +Use the following job in `.gitlab-ci.yml`: + +```yaml +## Use https://github.com/sj26/rspec_junit_formatter to generate a JUnit report with rspec +ruby: + stage: test + script: + - bundle install + - rspec spec/lib/ --format RspecJunitFormatter --out rspec.xml + artifacts: + reports: + junit: rspec.xml +``` + +### Go example + +Use the following job in `.gitlab-ci.yml`: + +```yaml +## Use https://github.com/jstemmer/go-junit-report to generate a JUnit report with go +golang: + stage: test + script: + - go get -u github.com/jstemmer/go-junit-report + - go test -v 2>&1 | go-junit-report > report.xml + artifacts: + reports: + junit: report.xml +``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 95d705d3a3d..ef740ab1c5e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1092,6 +1092,52 @@ job: expire_in: 1 week ``` +### `artifacts:reports` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in +GitLab 11.2. Requires GitLab Runner 11.2 and above. + +The `reports` keyword is used for collecting test reports from jobs and +exposing them in GitLab's UI (merge requests, pipeline views). Read how to use +this with [JUnit reports](#artifacts-reports-junit). + +NOTE: **Note:** +The test reports are collected regardless of the job results (success or failure). +You can use [`artifacts:expire_in`](#artifacts-expire_in) to set up an expiration +date for their artifacts. + +#### `artifacts:reports:junit` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in +GitLab 11.2. Requires GitLab Runner 11.2 and above. + +The `junit` report collects [JUnit XML files](https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.1.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html) +as artifacts. Although JUnit was originally developed in Java, there are many +[third party ports](https://en.wikipedia.org/wiki/JUnit#Ports) for other +languages like Javascript, Python, Ruby, etc. + +Below is an example of collecting a JUnit XML file from Ruby's RSpec test tool: + +```yaml +rspec: + stage: test + script: + - bundle install + - rspec --format RspecJunitFormatter --out rspec.xml + artifacts: + reports: + junit: rspec.xml +``` + +The collected JUnit reports will be uploaded to GitLab as an artifact and will +be automatically [shown in merge requests](../junit_test_reports.md). + +NOTE: **Note:** +In case the JUnit tool you use exports to multiple XML files, you can specify +multiple test report paths within a single job +(`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`) and they will be automatically +concatenated into a single file. + ## `dependencies` > Introduced in GitLab 8.6 and GitLab Runner v1.1.1. diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md index d4cce79b067..57ae318e821 100644 --- a/doc/development/contributing/design.md +++ b/doc/development/contributing/design.md @@ -18,23 +18,16 @@ To better understand the priority by which UX tackles issues, see the [UX sectio Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue. -The UX team has a special type label called ~"design artifact". This label indicates that the final output -for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. -Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is -needed until the solution has been decided. +There is a special type label called ~"product discovery". It represents a discovery issue intended for UX, PM, FE, and BE to discuss the problem and potential solutions. The final output for this issue could be a doc of requirements, a design artifact, or even a prototype. The solution will be developed in a subsequent milestone. -~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. +~"product discovery" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. -To prevent the misunderstanding that a feature will be be delivered in the -assigned milestone, when only UX design is planned for that milestone, the -Product Manager should create a separate issue for the ~"design artifact", -assign the ~UX, ~"design artifact" and ~"Deliverable" labels, add a milestone -and use a title that makes it clear that the scheduled issue is design only -(e.g. `Design exploration for XYZ`). +The initial issue should be about the problem we are solving. If a separate [product discovery issue](#product-discovery-issues) is needed for additional research and design work, it will be created by a PM or UX person. Assign the ~UX, ~"product discovery" and ~"Deliverable" labels, add a milestone and use a title that makes it clear that the scheduled issue is product discovery +(e.g. `Product discovery for XYZ`). -When the ~"design artifact" issue has been completed, the UXer removes the ~UX +When the ~"product discovery" issue has been completed, the UXer removes the ~UX label, adds the ~"UX ready" label and closes the issue. This indicates the -design artifact is complete. The UXer will also copy the designs to related +UX work for the issue is complete. The UXer will also copy any designs to related issues for implementation in an upcoming milestone. ## Style guides diff --git a/doc/development/diffs.md b/doc/development/diffs.md index 55fc16e0b33..d06339480b1 100644 --- a/doc/development/diffs.md +++ b/doc/development/diffs.md @@ -1,6 +1,6 @@ -# Working with Merge Request diffs +# Working with diffs -Currently we rely on different sources to present merge request diffs, these include: +Currently we rely on different sources to present diffs, these include: - Rugged gem - Gitaly service @@ -11,6 +11,8 @@ We're constantly moving Rugged calls to Gitaly and the progress can be followed ## Architecture overview +### Merge request diffs + When refreshing a Merge Request (pushing to a source branch, force-pushing to target branch, or if the target branch now contains any commits from the MR) we fetch the comparison information using `Gitlab::Git::Compare`, which fetches `base` and `head` data using Gitaly and diff between them through `Gitlab::Git::Diff.between` (which uses _Gitaly_ if it's enabled, otherwise _Rugged_). @@ -32,6 +34,17 @@ In order to present diffs information on the Merge Request diffs page, we: 3. If the diff file is cacheable (text-based), it's cached on Redis using `Gitlab::Diff::FileCollection::MergeRequestDiff` +### Note diffs + +When commenting on a diff (any comparison), we persist a truncated diff version +on `NoteDiffFile` (which is associated with the actual `DiffNote`). So instead +of hitting the repository every time we need the diff of the file, we: + +1. Check whether we have the `NoteDiffFile#diff` persisted and use it +2. Otherwise, if it's a current MR revision, use the persisted +`MergeRequestDiffFile#diff` +3. In the last scenario, go the the repository and fetch the diff + ## Diff limits As explained above, we limit single diff files and the size of the whole diff. There are scenarios where we collapse the diff file, diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 5d1f657015c..09ea8c05be6 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -20,7 +20,40 @@ dynamic (querying the DB etc.). Once defined in `lib/feature.rb`, you will be able to activate a feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature) +For GitLab.com, team members have access to feature flags through chatops. Only +percentage gates are supported at this time. Setting a feature to be used 50% of +the time, you should execute `/chatops run feature set my_feature_flag 50`. + ## Feature flags for user applications GitLab does not yet support the use of feature flags in deployed user applications. -You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779).
\ No newline at end of file +You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779). + +## Developing with feature flags + +In general, it's better to have a group- or user-based gate, and you should prefer +it over the use of percentage gates. This would make debugging easier, as you +filter for example logs and errors based on actors too. Futhermore, this allows +for enabling for the `gitlab-org` group first, while the rest of the users +aren't impacted. + +```ruby +# Good +Feature.enabled?(:feature_flag, project) + +# Avoid, if possible +Feature.enabled?(:feature_flag) +``` + +To use feature gates based on actors, the model needs to respond to +`flipper_id`. For example, to enable for the Foo model: + +```ruby +class Foo < ActiveRecord::Base + include FeatureGate +end +``` + +Features that are developed and are intended to be merged behind a feature flag +should not include a changelog entry. The entry should be added in the merge +request removing the feature flags. diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index a211effdfa7..6f31e5b82e5 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -182,6 +182,34 @@ class MyMigration < ActiveRecord::Migration end ``` +## Adding foreign-key constraints + +When adding a foreign-key constraint to either an existing or new +column remember to also add a index on the column. + +This is _required_ if the foreign-key constraint specifies +`ON DELETE CASCADE` or `ON DELETE SET NULL` behavior. On a cascading +delete, the [corresponding record needs to be retrieved using an +index](https://www.cybertec-postgresql.com/en/postgresql-indexes-and-foreign-keys/) +(otherwise, we'd need to scan the whole table) for subsequent update or +deletion. + +Here's an example where we add a new column with a foreign key +constraint. Note it includes `index: true` to create an index for it. + +```ruby +class Migration < ActiveRecord::Migration + + def change + add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade } + end +end +``` + +When adding a foreign-key constraint to an existing column, we +have to employ `add_concurrent_foreign_key` and `add_concurrent_index` +instead of `add_reference`. + ## Adding Columns With Default Values When adding columns with default values you must use the method diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md index e1e13474b75..53dfe6774e9 100644 --- a/doc/development/new_fe_guide/development/testing.md +++ b/doc/development/new_fe_guide/development/testing.md @@ -133,3 +133,27 @@ afterEach(() => { vm.$destroy(); }); ``` +## Testing with older browsers + +Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps: + + +### Browserstack + +[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers. +You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access. +You can find the credentials on 1Password, under `frontendteam@gitlab.com`. + +### Firefox + +#### macOS +You can download any older version of Firefox from the releases FTP server, https://ftp.mozilla.org/pub/firefox/releases/ + +1. From the website, select a version, in this case `50.0.1`. +2. Go to the mac folder. +3. Select your preferred language, you will find the dmg package inside, download it. +4. Drag and drop the application to any other folder but the `Applications` folder. +5. Rename the application to something like `Firefox_Old`. +6. Move the application to the `Applications` folder. +7. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version. +8. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version. diff --git a/doc/install/installation.md b/doc/install/installation.md index ea01d88d85f..a310f12b29e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -153,7 +153,7 @@ page](https://golang.org/dl). # Remove former Go installation folder sudo rm -rf /usr/local/go - + curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz @@ -494,11 +494,11 @@ Make GitLab start on boot: ### Install Gitaly # Fetch Gitaly source with Git and compile with Go - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production + sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,https://example.com/gitaly.git]" RAILS_ENV=production + sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production Next, make sure gitaly configured: diff --git a/doc/user/markdown.md b/doc/user/markdown.md index bd199b55a61..6856544ae1b 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -234,6 +234,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: + Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: :zap: You can use emoji anywhere GFM is supported. :v: @@ -244,6 +246,8 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: +Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + ### Special GitLab References GFM recognizes special references. @@ -259,6 +263,7 @@ GFM will recognize the following: | `@user_name` | specific user | | `@group_name` | specific group | | `@all` | entire team | +| `namespace/project>` | project | | `#12345` | issue | | `!123` | merge request | | `$123` | snippet | diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex 50e051e25a0..925b969eebe 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 8e486318980..77fa517b5b1 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1122,7 +1122,6 @@ X-Gitlab-Event: Build Hook }, "repository": { "name": "gitlab_test", - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", "description": "Atque in sunt eos similique dolores voluptatem.", "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 49b49271cff..0e847be79c2 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -119,10 +119,10 @@ Issue Board, that is, create or delete lists and drag issues from one list to an ## Issue Board terminology - **Issue Board** - Each board represents a unique view for your issues. It can have multiple lists with each list consisting of issues represented by cards. -- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Backlog' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee. On the top of that list you can see the number of issues that belong to it. +- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Open' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee. On the top of that list you can see the number of issues that belong to it. - **Label list**: a list based on a label. It shows all opened issues with that label. - **Assignee list**: a list which includes all issues assigned to a user. - - **Backlog** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list. + - **Open** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list. - **Closed** (default): shows all closed issues. Always appears as the rightmost list. - **Card** - A box in the list that represents an individual issue. The information you can see on a card consists of the issue number, the issue title, the assignee, and the labels associated with the issue. You can drag cards from one list to another to change their label or assignee from that of the source list to that of the destination list. @@ -353,9 +353,9 @@ To remove an assignee list, just as with a label list, click the trash icon. When dragging issues between lists, different behavior occurs depending on the source list and the target list. -| | To Backlog | To Closed | To label `B` list | To assignee `Bob` list | +| | To Open | To Closed | To label `B` list | To assignee `Bob` list | | --- | --- | --- | --- | --- | -| From Backlog | - | Issue closed | `B` added | `Bob` assigned | +| From Open | - | Issue closed | `B` added | `Bob` assigned | | From Closed | Issue reopened | - | Issue reopened<br/>`B` added | Issue reopened<br/>`Bob` assigned | | From label `A` list | `A` removed | Issue closed | `A` removed<br/>`B` added | `Bob` assigned | | From assignee `Alice` list | `Alice` unassigned | Issue closed | `B` added | `Alice` unassigned<br/>`Bob` assigned | diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 86ecf33ed31..43ca498d006 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -43,8 +43,7 @@ A. Consider you are a software developer working in a team: 1. You checkout a new branch, and submit your changes through a merge request 1. You gather feedback from your team -1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]** -1. You build and test your changes with GitLab CI/CD +1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD 1. You request the approval from your manager 1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter) 1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#manual-actions) for GitLab CI/CD diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 0ef8eddad20..8fdfd2a6f4d 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -1,7 +1,7 @@ # GitLab quick actions -Quick actions are textual shortcuts for common actions on issues or merge -requests that are usually done by clicking buttons or dropdowns in GitLab's UI. +Quick actions are textual shortcuts for common actions on issues, merge requests +or commits that are usually done by clicking buttons or dropdowns in GitLab's UI. You can enter these commands while creating a new issue or merge request, and in comments. Each command should be on a separate line in order to be properly detected and executed. The commands are removed from the issue, merge request or @@ -39,7 +39,8 @@ do. | `/board_move ~column` | Move issue to column on the board | | `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | | `/move path/to/project` | Moves issue to another project | +| `/tag v1.2.3 <message>` | Tags a commit with a given tag name and optional message | | `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | | `/shrug` | Append the comment with `¯\_(ツ)_/¯` | | <code>/copy_metadata #issue | !merge_request</code> | Copy labels and milestone from other issue or merge request | -| `/confidential` | Makes the issue confidential |
\ No newline at end of file +| `/confidential` | Makes the issue confidential | diff --git a/doc/user/project/web_ide/img/admin_clientside_evaluation.png b/doc/user/project/web_ide/img/admin_clientside_evaluation.png Binary files differnew file mode 100644 index 00000000000..a930490398b --- /dev/null +++ b/doc/user/project/web_ide/img/admin_clientside_evaluation.png diff --git a/doc/user/project/web_ide/img/clientside_evaluation.png b/doc/user/project/web_ide/img/clientside_evaluation.png Binary files differnew file mode 100644 index 00000000000..bd04d3d644b --- /dev/null +++ b/doc/user/project/web_ide/img/clientside_evaluation.png diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 511ac2d7e79..16969b2c527 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -72,5 +72,39 @@ leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list of branches. You will need to commit or discard all your changes before switching to a different branch. +## Client Side Evaluation + +> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19764) [GitLab Core][ce] 11.2. + +The Web IDE can be used to preview JavaScript projects right in the browser. +This feature uses CodeSandbox to compile and bundle the JavaScript used to +preview the web application. On public projects, an `Open in CodeSandbox` +button is visible which will transfer the contents of the project into a +CodeSandbox project to share with others. +**Note** this button is not visible on private or internal projects. + +![Web IDE Client Side Evaluation](img/clientside_evaluation.png) + +### Enabling Client Side Evaluation + +The Client Side Evaluation feature needs to be enabled in the GitLab instances +admin settings. Client Side Evaluation is enabled for all projects on +GitLab.com + +![Admin Client Side Evaluation setting](img/admin_clientside_evaluation.png) + +Once it has been enabled in application settings, projects with a +`package.json` file and a `main` entry point can be previewed inside of the Web +IDE. An example `package.json` is below. + +```json +{ + "main": "index.js", + "dependencies": { + "vue": "latest" + } +} +``` + [ce]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/ diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 27f28e1df93..b6393fdef19 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -856,7 +856,7 @@ module API class NotificationSetting < Grape::Entity expose :level expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do - ::NotificationSetting::EMAIL_EVENTS.each do |event| + ::NotificationSetting.email_events.each do |event| expose event end end @@ -1080,6 +1080,10 @@ module API expose :filename, :size end + class JobArtifact < Grape::Entity + expose :file_type, :size, :filename, :file_format + end + class JobBasic < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at @@ -1094,7 +1098,9 @@ module API end class Job < JobBasic + # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } + expose :job_artifacts, as: :artifacts, using: JobArtifact expose :runner, with: Runner expose :artifacts_expire_at end @@ -1153,7 +1159,7 @@ module API class License < Grape::Entity expose :key, :name, :nickname - expose :featured, as: :popular + expose :popular?, as: :popular expose :url, as: :html_url expose(:source_url) { |license| license.meta['source'] } expose(:description) { |license| license.meta['description'] } diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 10c6e565f09..fc8c52085ab 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -38,7 +38,7 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project) + builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, pipeline: :project) present paginate(builds), with: Entities::Job end @@ -54,7 +54,7 @@ module API pipeline = user_project.pipelines.find(params[:pipeline_id]) builds = pipeline.builds builds = filter_builds(builds, params[:scope]) - builds = builds.preload(:job_artifacts_archive, project: [:namespace]) + builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace]) present paginate(builds), with: Entities::Job end diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index 0266bf2f717..bf0d6b9e434 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -23,7 +23,7 @@ module API params do optional :level, type: String, desc: 'The global notification level' optional :notification_email, type: String, desc: 'The email address to send notifications' - NotificationSetting::EMAIL_EVENTS.each do |event| + NotificationSetting.email_events.each do |event| optional event, type: Boolean, desc: 'Enable/disable this notification' end end @@ -50,7 +50,9 @@ module API end end - %w[group project].each do |source_type| + [Group, Project].each do |source_class| + source_type = source_class.name.underscore + params do requires :id, type: String, desc: "The #{source_type} ID" end @@ -73,7 +75,7 @@ module API end params do optional :level, type: String, desc: "The #{source_type} notification level" - NotificationSetting::EMAIL_EVENTS.each do |event| + NotificationSetting.email_events(source_class).each do |event| optional event, type: Boolean, desc: 'Enable/disable this notification' end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 33a9646ac3b..79736107bbb 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -123,6 +123,39 @@ module API not_found! end end + + desc 'Get the common ancestor between commits' do + success Entities::Commit + end + params do + # For now we just support 2 refs passed, but `merge-base` supports + # multiple defining this as an Array instead of 2 separate params will + # make sure we don't need to deprecate this API in favor of one + # supporting multiple commits when this functionality gets added to + # Gitaly + requires :refs, type: Array[String] + end + get ':id/repository/merge_base' do + refs = params[:refs] + + unless refs.size == 2 + render_api_error!('Provide exactly 2 refs', 400) + end + + merge_base = Gitlab::Git::MergeBase.new(user_project.repository, refs) + + if merge_base.unknown_refs.any? + ref_noun = 'ref'.pluralize(merge_base.unknown_refs.size) + message = "Could not find #{ref_noun}: #{merge_base.unknown_refs.join(', ')}" + render_api_error!(message, 400) + end + + if merge_base.commit + present merge_base.commit, with: Entities::Commit + else + not_found!("Merge Base") + end + end end end end diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 41862768a3f..927baaea652 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -16,31 +16,8 @@ module API gitlab_version: 8.15 } }.freeze - PROJECT_TEMPLATE_REGEX = - %r{[\<\{\[] - (project|description| - one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]}xi.freeze - YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze - FULLNAME_TEMPLATE_REGEX = - %r{[\<\{\[] - (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]}xi.freeze helpers do - def parsed_license_template - # We create a fresh Licensee::License object since we'll modify its - # content in place below. - template = Licensee::License.new(params[:name]) - - template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s) - template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present? - - fullname = params[:fullname].presence || current_user.try(:name) - template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname - template - end - def render_response(template_type, template) not_found!(template_type.to_s.singularize) unless template present template, with: Entities::Template @@ -56,11 +33,12 @@ module API use :pagination end get "templates/licenses" do - options = { - featured: declared(params)[:popular].present? ? true : nil - } - licences = ::Kaminari.paginate_array(Licensee::License.all(options)) - present paginate(licences), with: Entities::License + popular = declared(params)[:popular] + popular = to_boolean(popular) if popular.present? + + templates = LicenseTemplateFinder.new(popular: popular).execute + + present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License end desc 'Get the text for a specific license' do @@ -71,9 +49,15 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do - not_found!('License') unless Licensee::License.find(declared(params)[:name]) + templates = LicenseTemplateFinder.new.execute + template = templates.find { |template| template.key == params[:name] } + + not_found!('License') unless template.present? - template = parsed_license_template + template.resolve!( + project_name: params[:project].presence, + fullname: params[:fullname].presence || current_user&.name + ) present template, with: ::API::Entities::License end diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb new file mode 100644 index 00000000000..83cf45097ed --- /dev/null +++ b/lib/banzai/filter/project_reference_filter.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML filter that replaces project references with links. + class ProjectReferenceFilter < ReferenceFilter + self.reference_type = :project + + # Public: Find `namespace/project>` project references in text + # + # ProjectReferenceFilter.references_in(text) do |match, project| + # "<a href=...>#{project}></a>" + # end + # + # text - String text to search. + # + # Yields the String match, and the String project name. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Project.markdown_reference_pattern) do |match| + yield match, "#{$~[:namespace]}/#{$~[:project]}" + end + end + + def call + ref_pattern = Project.markdown_reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + nodes.each do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + project_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, inner_html| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + project_link_filter(link, link_content: inner_html) + end + end + end + end + end + + doc + end + + # Replace `namespace/project>` project references in text with links to the referenced + # project page. + # + # text - String text to replace references in. + # link_content - Original content of the link being replaced. + # + # Returns a String with `namespace/project>` references replaced with links. All links + # have `gfm` and `gfm-project` class names attached for styling. + def project_link_filter(text, link_content: nil) + self.class.references_in(text) do |match, project_path| + cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do + if project = projects_hash[project_path.downcase] + link_to_project(project, link_content: link_content) || match + else + match + end + end + end + end + + # Returns a Hash containing all Project objects for the project + # references in the current document. + # + # The keys of this Hash are the project paths, the values the + # corresponding Project objects. + def projects_hash + @projects ||= Project.eager_load(:route, namespace: [:route]) + .where_full_path_in(projects) + .index_by(&:full_path) + .transform_keys(&:downcase) + end + + # Returns all projects referenced in the current document. + def projects + refs = Set.new + + nodes.each do |node| + node.to_html.scan(Project.markdown_reference_pattern) do + refs << "#{$~[:namespace]}/#{$~[:project]}" + end + end + + refs.to_a + end + + private + + def urls + Gitlab::Routing.url_helpers + end + + def link_class + reference_class(:project) + end + + def link_to_project(project, link_content: nil) + url = urls.project_url(project, only_path: context[:only_path]) + data = data_attribute(project: project.id) + content = link_content || project.to_reference_with_postfix + + link_tag(url, data, content, project.name) + end + + def link_tag(url, data, link_content, title) + %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5dab80dd3eb..e9be05e174e 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -36,6 +36,7 @@ module Banzai def self.reference_filters [ Filter::UserReferenceFilter, + Filter::ProjectReferenceFilter, Filter::IssueReferenceFilter, Filter::ExternalIssueReferenceFilter, Filter::MergeRequestReferenceFilter, diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb new file mode 100644 index 00000000000..b4e3a55b4f1 --- /dev/null +++ b/lib/banzai/reference_parser/project_parser.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class ProjectParser < BaseParser + include Gitlab::Utils::StrongMemoize + + self.reference_type = :project + + def references_relation + Project + end + + private + + # Returns an Array of Project ids that can be read by the given user. + # + # user - The User for which to check the projects + def readable_project_ids_for(user) + @project_ids_by_user ||= {} + @project_ids_by_user[user] ||= + Project.public_or_visible_to_user(user).where("projects.id IN (?)", @projects_for_nodes.values.map(&:id)).pluck(:id) + end + + def can_read_reference?(user, ref_project, node) + readable_project_ids_for(user).include?(ref_project.try(:id)) + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index f39b3b6eb5b..7f012312819 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -58,7 +58,6 @@ module Gitlab if Database.postgresql? options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout end if index_exists?(table_name, column_name, options) @@ -66,7 +65,9 @@ module Gitlab return end - add_index(table_name, column_name, options) + disable_statement_timeout do + add_index(table_name, column_name, options) + end end # Removes an existed index, concurrently when supported @@ -87,7 +88,6 @@ module Gitlab if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout end unless index_exists?(table_name, column_name, options) @@ -95,7 +95,9 @@ module Gitlab return end - remove_index(table_name, options.merge({ column: column_name })) + disable_statement_timeout do + remove_index(table_name, options.merge({ column: column_name })) + end end # Removes an existing index, concurrently when supported @@ -116,7 +118,6 @@ module Gitlab if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout end unless index_exists_by_name?(table_name, index_name) @@ -124,7 +125,9 @@ module Gitlab return end - remove_index(table_name, options.merge({ name: index_name })) + disable_statement_timeout do + remove_index(table_name, options.merge({ name: index_name })) + end end # Only available on Postgresql >= 9.2 @@ -171,8 +174,6 @@ module Gitlab on_delete = 'SET NULL' if on_delete == :nullify end - disable_statement_timeout - key_name = concurrent_foreign_key_name(source, column) unless foreign_key_exists?(source, target, column: column) @@ -199,7 +200,9 @@ module Gitlab # while running. # # Note this is a no-op in case the constraint is VALID already - execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") + disable_statement_timeout do + execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") + end end def foreign_key_exists?(source, target = nil, column: nil) @@ -224,8 +227,48 @@ module Gitlab # Long-running migrations may take more than the timeout allowed by # the database. Disable the session's statement timeout to ensure # migrations don't get killed prematurely. (PostgreSQL only) + # + # There are two possible ways to disable the statement timeout: + # + # - Per transaction (this is the preferred and default mode) + # - Per connection (requires a cleanup after the execution) + # + # When using a per connection disable statement, code must be inside + # a block so we can automatically execute `RESET ALL` after block finishes + # otherwise the statement will still be disabled until connection is dropped + # or `RESET ALL` is executed def disable_statement_timeout - execute('SET statement_timeout TO 0') if Database.postgresql? + # bypass disabled_statement logic when not using postgres, but still execute block when one is given + unless Database.postgresql? + if block_given? + yield + end + + return + end + + if block_given? + begin + execute('SET statement_timeout TO 0') + + yield + ensure + execute('RESET ALL') + end + else + unless transaction_open? + raise <<~ERROR + Cannot call disable_statement_timeout() without a transaction open or outside of a transaction block. + If you don't want to use a transaction wrap your code in a block call: + + disable_statement_timeout { # code that requires disabled statement here } + + This will make sure statement_timeout is disabled before and reset after the block execution is finished. + ERROR + end + + execute('SET LOCAL statement_timeout TO 0') + end end def true_value @@ -367,30 +410,30 @@ module Gitlab 'in the body of your migration class' end - disable_statement_timeout - - transaction do - if limit - add_column(table, column, type, default: nil, limit: limit) - else - add_column(table, column, type, default: nil) + disable_statement_timeout do + transaction do + if limit + add_column(table, column, type, default: nil, limit: limit) + else + add_column(table, column, type, default: nil) + end + + # Changing the default before the update ensures any newly inserted + # rows already use the proper default value. + change_column_default(table, column, default) end - # Changing the default before the update ensures any newly inserted - # rows already use the proper default value. - change_column_default(table, column, default) - end - - begin - update_column_in_batches(table, column, default, &block) + begin + update_column_in_batches(table, column, default, &block) - change_column_null(table, column, false) unless allow_null - # We want to rescue _all_ exceptions here, even those that don't inherit - # from StandardError. - rescue Exception => error # rubocop: disable all - remove_column(table, column) + change_column_null(table, column, false) unless allow_null + # We want to rescue _all_ exceptions here, even those that don't inherit + # from StandardError. + rescue Exception => error # rubocop: disable all + remove_column(table, column) - raise error + raise error + end end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 55236a1122f..2913a3e416d 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -10,9 +10,11 @@ module Gitlab TAG_REF_PREFIX = "refs/tags/".freeze BRANCH_REF_PREFIX = "refs/heads/".freeze - CommandError = Class.new(StandardError) - CommitError = Class.new(StandardError) - OSError = Class.new(StandardError) + BaseError = Class.new(StandardError) + CommandError = Class.new(BaseError) + CommitError = Class.new(BaseError) + OSError = Class.new(BaseError) + UnknownRef = Class.new(BaseError) class << self include Gitlab::EncodingHelper diff --git a/lib/gitlab/git/merge_base.rb b/lib/gitlab/git/merge_base.rb new file mode 100644 index 00000000000..b27f7038c26 --- /dev/null +++ b/lib/gitlab/git/merge_base.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class MergeBase + include Gitlab::Utils::StrongMemoize + + def initialize(repository, refs) + @repository, @refs = repository, refs + end + + # Returns the SHA of the first common ancestor + def sha + if unknown_refs.any? + raise UnknownRef, "Can't find merge base for unknown refs: #{unknown_refs.inspect}" + end + + strong_memoize(:sha) do + @repository.merge_base(*commits_for_refs) + end + end + + # Returns the merge base as a Gitlab::Git::Commit + def commit + return unless sha + + @commit ||= @repository.commit_by(oid: sha) + end + + # Returns the refs passed on initialization that aren't found in + # the repository, and thus cannot be used to find a merge base. + def unknown_refs + @unknown_refs ||= Hash[@refs.zip(commits_for_refs)] + .select { |ref, commit| commit.nil? }.keys + end + + private + + def commits_for_refs + @commits_for_refs ||= @repository.commits_by(oids: @refs) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 3e11355435b..e9c901f8592 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -366,18 +366,9 @@ module Gitlab end end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233 def new_commits(newrev) - gitaly_migrate(:new_commits) do |is_enabled| - if is_enabled - gitaly_ref_client.list_new_commits(newrev) - else - refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - rev_list(including: newrev, excluding: :all).split("\n").map(&:strip) - end - - Gitlab::Git::Commit.batch_by_oid(self, refs) - end + wrapped_gitaly_errors do + gitaly_ref_client.list_new_commits(newrev) end end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 65eb5cc18cf..752a91fbb60 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -2,34 +2,7 @@ module Gitlab module Git module RepositoryMirroring def remote_branches(remote_name) - gitaly_migrate(:ref_find_all_remote_branches) do |is_enabled| - if is_enabled - gitaly_ref_client.remote_branches(remote_name) - else - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - rugged_remote_branches(remote_name) - end - end - end - end - - private - - def rugged_remote_branches(remote_name) - branches = [] - - rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref| - name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '') - - begin - target_commit = Gitlab::Git::Commit.find(self, ref.target.oid) - branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit) - rescue Rugged::ReferenceError - # Omit invalid branch - end - end - - branches + gitaly_ref_client.remote_branches(remote_name) end end end diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 343487bc361..b8213929c6a 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -22,7 +22,8 @@ module Gitlab 'tr_TR' => 'Türkçe', 'id_ID' => 'Bahasa Indonesia', 'fil_PH' => 'Filipino', - 'pl_PL' => 'Polski' + 'pl_PL' => 'Polski', + 'cs_CZ' => 'Čeština' }.freeze def available_locales diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 091e81028bb..81807ed659c 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -47,7 +47,7 @@ module Gitlab end def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: []) - @relation_name = OVERRIDES[relation_sym] || relation_sym + @relation_name = self.class.overrides[relation_sym] || relation_sym @relation_hash = relation_hash.except('noteable_id') @members_mapper = members_mapper @user = user @@ -76,6 +76,10 @@ module Gitlab generate_imported_object end + def self.overrides + OVERRIDES + end + private def setup_models diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index b2d75aac1d0..5b68e4470cd 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -1,3 +1,5 @@ +require 'toml-rb' + module Gitlab module SetupHelper class << self @@ -9,7 +11,7 @@ module Gitlab # because it uses a Unix socket. # For development and testing purposes, an extra storage is added to gitaly, # which is not known to Rails, but must be explicitly stubbed. - def gitaly_configuration_toml(gitaly_dir, gitaly_ruby: true) + def gitaly_configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true) storages = [] address = nil @@ -24,10 +26,7 @@ module Gitlab address = val['gitaly_address'] end - # https://gitlab.com/gitlab-org/gitaly/issues/1238 - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - storages << { name: key, path: val.legacy_disk_path } - end + storages << { name: key, path: storage_paths[key] } end if Rails.env.test? @@ -44,12 +43,12 @@ module Gitlab end # rubocop:disable Rails/Output - def create_gitaly_configuration(dir, force: false) + def create_gitaly_configuration(dir, storage_paths, force: false) config_path = File.join(dir, 'config.toml') FileUtils.rm_f(config_path) if force File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f| - f.puts gitaly_configuration_toml(dir) + f.puts gitaly_configuration_toml(dir, storage_paths) end rescue Errno::EEXIST puts "Skipping config.toml generation:" diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb index 473b05257c6..a5105439b12 100644 --- a/lib/gitlab/template/finders/base_template_finder.rb +++ b/lib/gitlab/template/finders/base_template_finder.rb @@ -21,7 +21,7 @@ module Gitlab def category_directory(category) return @base_dir unless category.present? - @base_dir + @categories[category] + File.join(@base_dir, @categories[category]) end class << self diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb index 33f07fa0120..29bc2393ff9 100644 --- a/lib/gitlab/template/finders/repo_template_finder.rb +++ b/lib/gitlab/template/finders/repo_template_finder.rb @@ -27,7 +27,7 @@ module Gitlab directory = select_directory(file_name) raise FileNotFoundError if directory.nil? - category_directory(directory) + file_name + File.join(category_directory(directory), file_name) end def list_files_for(dir) @@ -37,8 +37,8 @@ module Gitlab entries = @repository.tree(:head, dir).entries - names = entries.map(&:name) - names.select { |f| f =~ self.class.filter_regex(@extension) } + paths = entries.map(&:path) + paths.select { |f| f =~ self.class.filter_regex(@extension) } end private @@ -47,10 +47,10 @@ module Gitlab return [] unless @commit # Insert root as directory - directories = ["", @categories.keys] + directories = ["", *@categories.keys] directories.find do |category| - path = category_directory(category) + file_name + path = File.join(category_directory(category), file_name) @repository.blob_at(@commit.id, path) end end diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index e9ca6404fe8..80de3d2ef51 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -1,13 +1,12 @@ namespace :gitlab do namespace :gitaly do desc "GitLab | Install or upgrade gitaly" - task :install, [:dir, :repo] => :gitlab_environment do |t, args| - require 'toml-rb' - + task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab - unless args.dir.present? - abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]") + unless args.dir.present? && args.storage_path.present? + abort %(Please specify the directory where you want to install gitaly and the path for the default storage +Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]") end args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitaly.git') @@ -27,7 +26,8 @@ namespace :gitlab do "BUNDLE_PATH=#{Bundler.bundle_path}") end - Gitlab::SetupHelper.create_gitaly_configuration(args.dir) + storage_paths = { 'default' => args.storage_path } + Gitlab::SetupHelper.create_gitaly_configuration(args.dir, storage_paths) Dir.chdir(args.dir) do # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? @@ -35,17 +35,5 @@ namespace :gitlab do end end end - - desc "GitLab | Print storage configuration in TOML format" - task storage_config: :environment do - require 'toml-rb' - - puts "# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}" - puts "# This is in TOML format suitable for use in Gitaly's config.toml file." - - # Exclude gitaly-ruby configuration because that depends on the gitaly - # installation directory. - puts Gitlab::SetupHelper.gitaly_configuration_toml('', gitaly_ruby: false) - end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bec60cf592a..b01a0068694 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -101,6 +101,9 @@ msgstr[1] "" msgid "%{filePath} deleted" msgstr "" +msgid "%{firstLabel} +%{labelCount} more" +msgstr "" + msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects." msgstr "" @@ -640,6 +643,9 @@ msgstr "" msgid "Authentication log" msgstr "" +msgid "Authentication method" +msgstr "" + msgid "Author" msgstr "" @@ -1987,6 +1993,9 @@ msgstr "" msgid "DashboardProjects|Personal" msgstr "" +msgid "Debug" +msgstr "" + msgid "Dec" msgstr "" @@ -2169,6 +2178,9 @@ msgstr "" msgid "Diffs|Something went wrong while fetching diff lines." msgstr "" +msgid "Direction" +msgstr "" + msgid "Directory name" msgstr "" @@ -2391,6 +2403,9 @@ msgstr "" msgid "Environments|You don't have any environments right now." msgstr "" +msgid "Error" +msgstr "" + msgid "Error Reporting and Logging" msgstr "" @@ -2439,6 +2454,9 @@ msgstr "" msgid "Error updating todo status." msgstr "" +msgid "Error while loading the merge request. Please try again." +msgstr "" + msgid "Estimated" msgstr "" @@ -2517,6 +2535,9 @@ msgstr "" msgid "Failed to remove issue from board, please try again." msgstr "" +msgid "Failed to remove mirror." +msgstr "" + msgid "Failed to remove the pipeline schedule" msgstr "" @@ -2717,6 +2738,9 @@ msgstr "" msgid "Go back" msgstr "" +msgid "Go to" +msgstr "" + msgid "Go to %{link_to_google_takeout}." msgstr "" @@ -2947,6 +2971,9 @@ msgstr "" msgid "IDE|Review" msgstr "" +msgid "IP Address" +msgstr "" + msgid "Identifier" msgstr "" @@ -3031,12 +3058,21 @@ msgstr "" msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept." msgstr "" +msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>." +msgstr "" + msgid "Incompatible Project" msgstr "" +msgid "Indicates whether this runner can pick jobs without tags" +msgstr "" + msgid "Inline" msgstr "" +msgid "Input your repository URL" +msgstr "" + msgid "Install GitLab Runner" msgstr "" @@ -3103,6 +3139,30 @@ msgstr "" msgid "Jobs" msgstr "" +msgid "Job|Browse" +msgstr "" + +msgid "Job|Download" +msgstr "" + +msgid "Job|Job artifacts" +msgstr "" + +msgid "Job|Job has been erased" +msgstr "" + +msgid "Job|Job has been erased by" +msgstr "" + +msgid "Job|Keep" +msgstr "" + +msgid "Job|The artifacts were removed" +msgstr "" + +msgid "Job|The artifacts will be removed" +msgstr "" + msgid "Jul" msgstr "" @@ -3354,6 +3414,9 @@ msgstr "" msgid "Maximum git storage failures" msgstr "" +msgid "Maximum job timeout" +msgstr "" + msgid "May" msgstr "" @@ -3468,6 +3531,21 @@ msgstr "" msgid "Milestones|Promote Milestone" msgstr "" +msgid "Mirror a repository" +msgstr "" + +msgid "Mirror direction" +msgstr "" + +msgid "Mirror repository" +msgstr "" + +msgid "Mirrored repositories" +msgstr "" + +msgid "Mirroring repositories" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" @@ -3528,6 +3606,9 @@ msgstr "" msgid "Network" msgstr "" +msgid "Never" +msgstr "" + msgid "New" msgstr "" @@ -3791,12 +3872,18 @@ msgstr "" msgid "Only comments from the following commit are shown below" msgstr "" +msgid "Only mirror protected branches" +msgstr "" + msgid "Only project members can comment." msgstr "" msgid "Oops, are you sure?" msgstr "" +msgid "Open" +msgstr "" + msgid "Open in Xcode" msgstr "" @@ -3872,6 +3959,9 @@ msgstr "" msgid "Pause" msgstr "" +msgid "Paused Runners don't accept new jobs" +msgstr "" + msgid "Pending" msgstr "" @@ -4364,6 +4454,9 @@ msgstr "" msgid "Promote to group label" msgstr "" +msgid "Protected" +msgstr "" + msgid "Protip:" msgstr "" @@ -4379,6 +4472,9 @@ msgstr "" msgid "Public pipelines" msgstr "" +msgid "Push" +msgstr "" + msgid "Push events" msgstr "" @@ -4576,6 +4672,9 @@ msgstr "" msgid "Revoke" msgstr "" +msgid "Run untagged jobs" +msgstr "" + msgid "Runner token" msgstr "" @@ -4588,6 +4687,9 @@ msgstr "" msgid "Runners can be placed on separate users, servers, and even on your local machine." msgstr "" +msgid "Runners page" +msgstr "" + msgid "Running" msgstr "" @@ -4780,6 +4882,9 @@ msgstr "" msgid "Set up Koding" msgstr "" +msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically." +msgstr "" + msgid "SetPasswordToCloneLink|set a password" msgstr "" @@ -5189,6 +5294,9 @@ msgstr "" msgid "Test coverage parsing" msgstr "" +msgid "The Git LFS objects will <strong>not</strong> be synced." +msgstr "" + msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project" msgstr "" @@ -5246,6 +5354,9 @@ msgstr "" msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>." msgstr "" +msgid "The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> and <code>git://</code>." +msgstr "" + msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "" @@ -5402,12 +5513,21 @@ msgstr "" msgid "This repository" msgstr "" +msgid "This runner will only run on pipelines triggered on protected branches" +msgstr "" + msgid "This source diff could not be displayed because it is too large." msgstr "" +msgid "This timeout will take precedence when lower than Project-defined timeout" +msgstr "" + msgid "This user has no identities" msgstr "" +msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches." +msgstr "" + msgid "Time before an issue gets scheduled" msgstr "" @@ -5643,6 +5763,9 @@ msgstr "" msgid "ToggleButton|Toggle Status: ON" msgstr "" +msgid "Token" +msgstr "" + msgid "Too many changes to show." msgstr "" @@ -5721,9 +5844,15 @@ msgstr "" msgid "Update" msgstr "" +msgid "Update now" +msgstr "" + msgid "Update your group name, description, avatar, and other general settings." msgstr "" +msgid "Updating" +msgstr "" + msgid "Upload <code>GoogleCodeProjectHosting.json</code> here:" msgstr "" @@ -6063,6 +6192,9 @@ msgstr "" msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgstr "" +msgid "You can setup jobs to only use Runners with specific tags. Separate tags with commas." +msgstr "" + msgid "You cannot write to this read-only GitLab instance." msgstr "" diff --git a/locale/unfound_translations.rb b/locale/unfound_translations.rb new file mode 100644 index 00000000000..0826d64049b --- /dev/null +++ b/locale/unfound_translations.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Dynamic translations which needs to be marked by `N_` so they can be found by `rake gettext:find`, see: +# https://github.com/grosser/gettext_i18n_rails#unfound-translations-with-rake-gettextfind + +# NotificationSetting.email_events +N_('NotificationEvent|New note') +N_('NotificationEvent|New issue') +N_('NotificationEvent|Reopen issue') +N_('NotificationEvent|Close issue') +N_('NotificationEvent|Reassign issue') +N_('NotificationEvent|New merge request') +N_('NotificationEvent|Close merge request') +N_('NotificationEvent|Reassign merge request') +N_('NotificationEvent|Merge merge request') +N_('NotificationEvent|Failed pipeline') diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb index bc713b46d81..c2c3bef98e4 100644 --- a/qa/qa/specs/features/project/auto_devops_spec.rb +++ b/qa/qa/specs/features/project/auto_devops_spec.rb @@ -50,7 +50,7 @@ module QA Page::Project::Pipeline::Show.perform do |pipeline| expect(pipeline).to have_build('build', status: :success, wait: 600) expect(pipeline).to have_build('test', status: :success, wait: 600) - expect(pipeline).to have_build('production', status: :success, wait: 600) + expect(pipeline).to have_build('production', status: :success, wait: 1200) end end end diff --git a/rubocop/cop/migration/add_reference.rb b/rubocop/cop/migration/add_reference.rb new file mode 100644 index 00000000000..4b67270c97a --- /dev/null +++ b/rubocop/cop/migration/add_reference.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if a foreign key constraint is added and require a index for it + class AddReference < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`add_reference` requires `index: true`' + + def on_send(node) + return unless in_migration?(node) + + name = node.children[1] + + return unless name == :add_reference + + opts = node.children.last + + add_offense(node, location: :selector) unless opts && opts.type == :hash + + index_present = false + + opts.each_node(:pair) do |pair| + index_present ||= index_enabled?(pair) + end + + add_offense(node, location: :selector) unless index_present + end + + private + + def index_enabled?(pair) + hash_key_type(pair) == :sym && hash_key_name(pair) == :index && pair.children[1].true_type? + end + + def hash_key_type(pair) + pair.children[0].type + end + + def hash_key_name(pair) + pair.children[0].children[0] + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index aa7ae601f75..a427208cdab 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -11,6 +11,7 @@ require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' +require_relative 'cop/migration/add_reference' require_relative 'cop/migration/add_timestamps' require_relative 'cop/migration/datetime' require_relative 'cop/migration/hash_index' diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 421ab006792..fbf116e533b 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -162,6 +162,10 @@ describe ApplicationController do describe 'session expiration' do controller(described_class) do + # The anonymous controller will report 401 and fail to run any actions. + # Normally, GitLab will just redirect you to sign in. + skip_before_action :authenticate_user!, only: :index + def index render text: 'authenticated' end diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb index e133950e684..a3356a86d4b 100644 --- a/spec/controllers/notification_settings_controller_spec.rb +++ b/spec/controllers/notification_settings_controller_spec.rb @@ -21,10 +21,11 @@ describe NotificationSettingsController do end context 'when authorized' do + let(:notification_setting) { user.notification_settings_for(source) } let(:custom_events) do events = {} - NotificationSetting::EMAIL_EVENTS.each do |event| + NotificationSetting.email_events(source).each do |event| events[event.to_s] = true end @@ -36,7 +37,7 @@ describe NotificationSettingsController do end context 'for projects' do - let(:notification_setting) { user.notification_settings_for(project) } + let(:source) { project } it 'creates notification setting' do post :create, @@ -67,7 +68,7 @@ describe NotificationSettingsController do end context 'for groups' do - let(:notification_setting) { user.notification_settings_for(group) } + let(:source) { group } it 'creates notification setting' do post :create, @@ -145,7 +146,7 @@ describe NotificationSettingsController do let(:custom_events) do events = {} - NotificationSetting::EMAIL_EVENTS.each do |event| + notification_setting.email_events.each do |event| events[event] = "true" end end diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index a81b2169b89..81c485fba1a 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -46,6 +46,13 @@ FactoryBot.define do secret SecureRandom.hex end + trait :favicon_upload do + model { build(:appearance) } + path { File.join(secret, filename) } + uploader "FaviconUploader" + secret SecureRandom.hex + end + trait :attachment_upload do transient do mount_point :attachment diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index a0af2dea3ad..baa2b1d8af5 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -44,7 +44,7 @@ describe 'Issue Boards', :js do end it 'creates default lists' do - lists = ['Backlog', 'To Do', 'Doing', 'Closed'] + lists = ['Open', 'To Do', 'Doing', 'Closed'] page.within(find('.board-blank-state')) do click_button('Add default lists') diff --git a/spec/features/commits/user_uses_slash_commands_spec.rb b/spec/features/commits/user_uses_slash_commands_spec.rb new file mode 100644 index 00000000000..9a4b7bd2444 --- /dev/null +++ b/spec/features/commits/user_uses_slash_commands_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Commit > User uses quick actions', :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:commit) { project.commit } + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_commit_path(project, commit.id) + end + + describe 'Tagging a commit' do + let(:tag_name) { 'v1.2.3' } + let(:tag_message) { 'Stable release' } + let(:truncated_commit_sha) { Commit.truncate_sha(commit.sha) } + + it 'tags this commit' do + add_note("/tag #{tag_name} #{tag_message}") + + expect(page).to have_content 'Commands applied' + expect(page).to have_content "tagged commit #{truncated_commit_sha}" + expect(page).to have_content tag_name + + visit project_tag_path(project, tag_name) + expect(page).to have_content tag_name + expect(page).to have_content tag_message + expect(page).to have_content truncated_commit_sha + end + + describe 'preview', :js do + it 'removes quick action from note and explains it' do + preview_note("/tag #{tag_name} #{tag_message}") + + expect(page).not_to have_content '/tag' + expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"} + expect(page).to have_content tag_name + end + end + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb deleted file mode 100644 index bf60b18873c..00000000000 --- a/spec/features/issues/award_emoji_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'rails_helper' - -describe 'Awards Emoji' do - let!(:project) { create(:project, :public) } - let!(:user) { create(:user) } - let(:issue) do - create(:issue, - assignees: [user], - project: project) - end - - context 'authorized user' do - before do - project.add_maintainer(user) - sign_in(user) - end - - describe 'visiting an issue with a legacy award emoji that is not valid anymore' do - before do - # The `heart_tip` emoji is not valid anymore so we need to skip validation - issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) - visit project_issue_path(project, issue) - wait_for_requests - end - - # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 - it 'does not shows a 500 page', :js do - expect(page).to have_text(issue.title) - end - end - - describe 'Click award emoji from issue#show' do - let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } - - before do - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'increments the thumbsdown emoji', :js do - find('[data-name="thumbsdown"]').click - wait_for_requests - expect(thumbsdown_emoji).to have_text("1") - end - - context 'click the thumbsup emoji' do - it 'increments the thumbsup emoji', :js do - find('[data-name="thumbsup"]').click - wait_for_requests - expect(thumbsup_emoji).to have_text("1") - end - - it 'decrements the thumbsdown emoji', :js do - expect(thumbsdown_emoji).to have_text("0") - end - end - - context 'click the thumbsdown emoji' do - it 'increments the thumbsdown emoji', :js do - find('[data-name="thumbsdown"]').click - wait_for_requests - expect(thumbsdown_emoji).to have_text("1") - end - - it 'decrements the thumbsup emoji', :js do - expect(thumbsup_emoji).to have_text("0") - end - end - - it 'toggles the smiley emoji on a note', :js do - toggle_smiley_emoji(true) - - within('.note-body') do - expect(find(emoji_counter)).to have_text("1") - end - - toggle_smiley_emoji(false) - - within('.note-body') do - expect(page).not_to have_selector(emoji_counter) - end - end - - context 'execute /award quick action' do - it 'toggles the emoji award on noteable', :js do - execute_quick_action('/award :100:') - - expect(find(noteable_award_counter)).to have_text("1") - - execute_quick_action('/award :100:') - - expect(page).not_to have_selector(noteable_award_counter) - end - end - end - end - - context 'unauthorized user', :js do - before do - visit project_issue_path(project, issue) - end - - it 'has disabled emoji button' do - expect(first('.award-control')[:class]).to have_text('disabled') - end - end - - def execute_quick_action(cmd) - within('.js-main-target-form') do - fill_in 'note[note]', with: cmd - click_button 'Comment' - end - - wait_for_requests - end - - def thumbsup_emoji - page.all(emoji_counter).first - end - - def thumbsdown_emoji - page.all(emoji_counter).last - end - - def emoji_counter - 'span.js-counter' - end - - def noteable_award_counter - ".awards .active" - end - - def toggle_smiley_emoji(status) - within('.note') do - find('.note-emoji-button').click - end - - unless status - first('[data-name="smiley"]').click - else - find('[data-name="smiley"]').click - end - - wait_for_requests - end -end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb deleted file mode 100644 index e53a4ce49c7..00000000000 --- a/spec/features/issues/award_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -describe 'Issue awards', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } - - describe 'logged in' do - before do - sign_in(user) - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'adds award to issue' do - first('.js-emoji-btn').click - expect(page).to have_selector('.js-emoji-btn.active') - expect(first('.js-emoji-btn')).to have_content '1' - - visit project_issue_path(project, issue) - expect(first('.js-emoji-btn')).to have_content '1' - end - - it 'removes award from issue' do - first('.js-emoji-btn').click - find('.js-emoji-btn.active').click - expect(first('.js-emoji-btn')).to have_content '0' - - visit project_issue_path(project, issue) - expect(first('.js-emoji-btn')).to have_content '0' - end - - it 'only has one menu on the page' do - first('.js-add-award').click - expect(page).to have_selector('.emoji-menu') - - expect(page).to have_selector('.emoji-menu', count: 1) - end - end - - describe 'logged out' do - before do - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'does not see award menu button' do - expect(page).not_to have_selector('.js-award-holder') - end - end -end diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb new file mode 100644 index 00000000000..afa425c2cec --- /dev/null +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -0,0 +1,347 @@ +require 'spec_helper' + +describe 'User interacts with awards' do + let(:user) { create(:user) } + + describe 'User interacts with awards in an issue', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + + before do + project.add_maintainer(user) + sign_in(user) + + visit(project_issue_path(project, issue)) + end + + it 'toggles the thumbsup award emoji' do + page.within('.awards') do + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.award-control.js-emoji-btn') + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) + + page.all('.award-control.js-emoji-btn').each do |element| + expect(element['title']).to eq('') + end + + expect(page.all('.award-control .js-counter')).to all(have_content('0')) + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + end + end + + it 'toggles a custom award emoji' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + page.within('.emoji-menu-content') do + emoji_button = page.first('.js-emoji-btn') + emoji_button.hover + emoji_button.click + end + + page.within('.awards') do + expect(page).to have_selector('.js-emoji-btn') + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + + expect do + page.find('.js-emoji-btn.active').click + wait_for_requests + end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) + end + end + + it 'shows the list of award emoji categories' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + fill_in('emoji-menu-search', with: 'hand') + + page.within('.emoji-menu-content') do + expect(page).to have_selector('[data-name="raised_hand"]') + end + end + + it 'adds an award emoji by a comment' do + page.within('.js-main-target-form') do + fill_in('note[note]', with: ':smile:') + + click_button('Comment') + end + + expect(page).to have_emoji('smile') + end + + context 'when a project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the add award button' do + page.within('.awards') do + expect(page).not_to have_css('.js-add-award') + end + end + end + + context 'User interacts with awards on a note' do + let!(:note) { create(:note, noteable: issue, project: issue.project) } + let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } + + it 'shows the award on the note' do + page.within('.note-awards') do + expect(page).to have_emoji('100') + end + end + + it 'allows adding a vote to an award' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(2) + end + + it 'allows adding a new emoji' do + page.within('.note-actions') do + find('a.js-add-award').click + end + page.within('.emoji-menu-content') do + find('gl-emoji[data-name="8ball"]').click + end + wait_for_requests + + page.within('.note-awards') do + expect(page).to have_emoji('8ball') + end + expect(note.reload.award_emoji.size).to eq(2) + end + + context 'when the project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the buttons for adding new emoji' do + page.within('.note-awards') do + expect(page).not_to have_css('.award-menu-holder') + end + + page.within('.note-actions') do + expect(page).not_to have_css('a.js-add-award') + end + end + + it 'does not allow toggling existing emoji' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(1) + end + end + end + end + + describe 'User interacts with awards on an issue', :js do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + + describe 'logged in' do + before do + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'adds award to issue' do + first('.js-emoji-btn').click + + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit project_issue_path(project, issue) + + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'removes award from issue' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + + expect(first('.js-emoji-btn')).to have_content '0' + + visit project_issue_path(project, issue) + + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'only has one menu on the page' do + first('.js-add-award').click + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'does not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end + end + + describe 'Awards Emoji' do + let!(:project) { create(:project, :public) } + let(:issue) { create(:issue, assignees: [user], project: project) } + + context 'authorized user' do + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'visiting an issue with a legacy award emoji that is not valid anymore' do + before do + # The `heart_tip` emoji is not valid anymore so we need to skip validation + issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) + visit project_issue_path(project, issue) + wait_for_requests + end + + # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 + it 'does not shows a 500 page', :js do + expect(page).to have_text(issue.title) + end + end + + describe 'Click award emoji from issue#show' do + let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } + + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + context 'click the thumbsdown emoji' do + it 'increments the thumbsdown emoji', :js do + find('[data-name="thumbsdown"]').click + wait_for_requests + expect(thumbsdown_emoji).to have_text("1") + end + + it 'decrements the thumbsup emoji', :js do + expect(thumbsup_emoji).to have_text("0") + end + end + + it 'toggles the smiley emoji on a note', :js do + toggle_smiley_emoji(true) + + within('.note-body') do + expect(find(emoji_counter)).to have_text("1") + end + + toggle_smiley_emoji(false) + + within('.note-body') do + expect(page).not_to have_selector(emoji_counter) + end + end + + context 'execute /award quick action' do + it 'toggles the emoji award on noteable', :js do + execute_quick_action('/award :100:') + + expect(find(noteable_award_counter)).to have_text("1") + + execute_quick_action('/award :100:') + + expect(page).not_to have_selector(noteable_award_counter) + end + end + end + end + + context 'unauthorized user', :js do + before do + visit project_issue_path(project, issue) + end + + it 'has disabled emoji button' do + expect(first('.award-control')[:class]).to have_text('disabled') + end + end + + def execute_quick_action(cmd) + within('.js-main-target-form') do + fill_in 'note[note]', with: cmd + click_button 'Comment' + end + + wait_for_requests + end + + def thumbsup_emoji + page.all(emoji_counter).first + end + + def thumbsdown_emoji + page.all(emoji_counter).last + end + + def emoji_counter + 'span.js-counter' + end + + def noteable_award_counter + ".awards .active" + end + + def toggle_smiley_emoji(status) + within('.note') do + find('.note-emoji-button').click + end + + if !status + first('[data-name="smiley"]').click + else + find('[data-name="smiley"]').click + end + + wait_for_requests + end + end +end diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb index c1608be402a..fd4175d5227 100644 --- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb +++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb @@ -28,7 +28,7 @@ describe 'Merge request > User sees MR with deleted source branch', :js do click_on 'Changes' wait_for_requests - expect(page).to have_selector('.diffs.tab-pane .nothing-here-block') + expect(page).to have_selector('.diffs.tab-pane .file-holder') expect(page).to have_content('Source branch does not exist.') end end diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb deleted file mode 100644 index 4d860893abe..00000000000 --- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb +++ /dev/null @@ -1,172 +0,0 @@ -require 'spec_helper' - -describe 'User interacts with awards in an issue', :js do - let(:issue) { create(:issue, project: project)} - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - project.add_maintainer(user) - sign_in(user) - - visit(project_issue_path(project, issue)) - end - - it 'toggles the thumbsup award emoji' do - page.within('.awards') do - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.js-emoji-btn') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.award-control.js-emoji-btn') - expect(page.all('.award-control.js-emoji-btn').size).to eq(2) - - page.all('.award-control.js-emoji-btn').each do |element| - expect(element['title']).to eq('') - end - - page.all('.award-control .js-counter').each do |element| - expect(element).to have_content('0') - end - - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.js-emoji-btn') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - end - end - - it 'toggles a custom award emoji' do - page.within('.awards') do - page.find('.js-add-award').click - end - - page.find('.emoji-menu.is-visible') - - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - - page.within('.emoji-menu-content') do - emoji_button = page.first('.js-emoji-btn') - emoji_button.hover - emoji_button.click - end - - page.within('.awards') do - expect(page).to have_selector('.js-emoji-btn') - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - - expect do - page.find('.js-emoji-btn.active').click - wait_for_requests - end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) - end - end - - it 'shows the list of award emoji categories' do - page.within('.awards') do - page.find('.js-add-award').click - end - - page.find('.emoji-menu.is-visible') - - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - - fill_in('emoji-menu-search', with: 'hand') - - page.within('.emoji-menu-content') do - expect(page).to have_selector('[data-name="raised_hand"]') - end - end - - it 'adds an award emoji by a comment' do - page.within('.js-main-target-form') do - fill_in('note[note]', with: ':smile:') - - click_button('Comment') - end - - expect(page).to have_emoji('smile') - end - - context 'when a project is archived' do - let(:project) { create(:project, :archived) } - - it 'hides the add award button' do - page.within('.awards') do - expect(page).not_to have_css('.js-add-award') - end - end - end - - context 'awards on a note' do - let!(:note) { create(:note, noteable: issue, project: issue.project) } - let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } - - it 'shows the award on the note' do - page.within('.note-awards') do - expect(page).to have_emoji('100') - end - end - - it 'allows adding a vote to an award' do - page.within('.note-awards') do - find('gl-emoji[data-name="100"]').click - end - wait_for_requests - - expect(note.reload.award_emoji.size).to eq(2) - end - - it 'allows adding a new emoji' do - page.within('.note-actions') do - find('a.js-add-award').click - end - page.within('.emoji-menu-content') do - find('gl-emoji[data-name="8ball"]').click - end - wait_for_requests - - page.within('.note-awards') do - expect(page).to have_emoji('8ball') - end - expect(note.reload.award_emoji.size).to eq(2) - end - - context 'when the project is archived' do - let(:project) { create(:project, :archived) } - - it 'hides the buttons for adding new emoji' do - page.within('.note-awards') do - expect(page).not_to have_css('.award-menu-holder') - end - - page.within('.note-actions') do - expect(page).not_to have_css('a.js-add-award') - end - end - - it 'does not allow toggling existing emoji' do - page.within('.note-awards') do - find('gl-emoji[data-name="100"]').click - end - wait_for_requests - - expect(note.reload.award_emoji.size).to eq(1) - end - end - end -end diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb index aeed38aeb76..3925de6cfb9 100644 --- a/spec/features/projects/blobs/shortcuts_blob_spec.rb +++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Blob shortcuts' do +describe 'Blob shortcuts', :js do include TreeHelper let(:project) { create(:project, :public, :repository) } let(:path) { project.repository.ls_files(project.repository.root_ref)[0] } @@ -18,6 +18,7 @@ describe 'Blob shortcuts' do describe 'pressing "y"' do it 'redirects to permalink with commit sha' do visit_blob + wait_for_requests find('body').native.send_key('y') @@ -27,6 +28,7 @@ describe 'Blob shortcuts' do it 'maintains fragment hash when redirecting' do fragment = "L1" visit_blob(fragment) + wait_for_requests find('body').native.send_key('y') diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb index f56174fc85c..f3cf3a282e5 100644 --- a/spec/features/projects/files/user_browses_files_spec.rb +++ b/spec/features/projects/files/user_browses_files_spec.rb @@ -210,9 +210,10 @@ describe "User browses files" do end end - context "when browsing a file content" do + context "when browsing a file content", :js do before do visit(tree_path_root_ref) + wait_for_requests click_link(".gitignore") end diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 0e9f83a16ce..dcb7b947c61 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Projects > Files > User deletes files' do +describe 'Projects > Files > User deletes files', :js do let(:fork_message) do "You're not allowed to make changes to this project directly. "\ "A fork of this project has been created that you can make changes in, so you can submit a merge request." @@ -19,6 +19,7 @@ describe 'Projects > Files > User deletes files' do before do project.add_maintainer(user) visit(project_tree_path_root_ref) + wait_for_requests end it 'deletes the file', :js do @@ -35,10 +36,11 @@ describe 'Projects > Files > User deletes files' do end end - context 'when an user does not have write access' do + context 'when an user does not have write access', :js do before do project2.add_reporter(user) visit(project2_tree_path_root_ref) + wait_for_requests end it 'deletes the file in a forked project', :js do diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index ccc1bc1bc10..9eb65ec159c 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Projects > Files > User edits files' do +describe 'Projects > Files > User edits files', :js do include ProjectForksHelper let(:project) { create(:project, :repository, name: 'Shop') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } @@ -29,13 +29,14 @@ describe 'Projects > Files > User edits files' do end end - context 'when an user has write access' do + context 'when an user has write access', :js do before do project.add_maintainer(user) visit(project_tree_path_root_ref) + wait_for_requests end - it 'inserts a content of a file', :js do + it 'inserts a content of a file' do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -49,13 +50,14 @@ describe 'Projects > Files > User edits files' do it 'does not show the edit link if a file is binary' do binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png') visit(project_blob_path(project, binary_file)) + wait_for_requests page.within '.content' do expect(page).not_to have_link('edit') end end - it 'commits an edited file', :js do + it 'commits an edited file' do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -72,7 +74,7 @@ describe 'Projects > Files > User edits files' do expect(page).to have_content('*.rbca') end - it 'commits an edited file to a new branch', :js do + it 'commits an edited file to a new branch' do click_link('.gitignore') find('.js-edit-blob').click @@ -91,7 +93,7 @@ describe 'Projects > Files > User edits files' do expect(page).to have_content('*.rbca') end - it 'shows the diff of an edited file', :js do + it 'shows the diff of an edited file' do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -106,13 +108,14 @@ describe 'Projects > Files > User edits files' do it_behaves_like 'unavailable for an archived project' end - context 'when an user does not have write access' do + context 'when an user does not have write access', :js do before do project2.add_reporter(user) visit(project2_tree_path_root_ref) + wait_for_requests end - it 'inserts a content of a file in a forked project', :js do + it 'inserts a content of a file in a forked project' do click_link('.gitignore') find('.js-edit-blob').click @@ -134,7 +137,7 @@ describe 'Projects > Files > User edits files' do expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') end - it 'commits an edited file in a forked project', :js do + it 'commits an edited file in a forked project' do click_link('.gitignore') find('.js-edit-blob').click @@ -163,6 +166,7 @@ describe 'Projects > Files > User edits files' do let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } before do visit(project2_tree_path_root_ref) + wait_for_requests end it 'links to the forked project for editing' do diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index 3a81e77c4ba..e3da28d73c3 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Projects > Files > User replaces files' do +describe 'Projects > Files > User replaces files', :js do include DropzoneHelper let(:fork_message) do @@ -21,9 +21,10 @@ describe 'Projects > Files > User replaces files' do before do project.add_maintainer(user) visit(project_tree_path_root_ref) + wait_for_requests end - it 'replaces an existed file with a new one', :js do + it 'replaces an existed file with a new one' do click_link('.gitignore') expect(page).to have_content('.gitignore') @@ -47,9 +48,10 @@ describe 'Projects > Files > User replaces files' do before do project2.add_reporter(user) visit(project2_tree_path_root_ref) + wait_for_requests end - it 'replaces an existed file with a new one in a forked project', :js do + it 'replaces an existed file with a new one in a forked project' do click_link('.gitignore') expect(page).to have_content('.gitignore') diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb index 5259a8942dc..33e9b73efe8 100644 --- a/spec/features/projects/remote_mirror_spec.rb +++ b/spec/features/projects/remote_mirror_spec.rb @@ -17,7 +17,7 @@ describe 'Project remote mirror', :feature do visit project_mirror_path(project) - expect(page).to have_content('The remote repository failed to update.') + expect_mirror_to_have_error_and_timeago('Never') end end @@ -27,8 +27,14 @@ describe 'Project remote mirror', :feature do visit project_mirror_path(project) - expect(page).to have_content('The remote repository failed to update 5 minutes ago.') + expect_mirror_to_have_error_and_timeago('5 minutes ago') end end + + def expect_mirror_to_have_error_and_timeago(timeago) + row = first('.js-mirrors-table-body tr') + expect(row).to have_content('Error') + expect(row).to have_content(timeago) + end end end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index a0f5b234ebc..377a75cbcb3 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -129,9 +129,8 @@ describe 'Projects > Settings > Repository settings' do visit project_settings_repository_path(project) end - it 'shows push mirror settings' do - expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled') - expect(page).to have_selector('#project_remote_mirrors_attributes_0_url') + it 'shows push mirror settings', :js do + expect(page).to have_selector('#mirror_direction') end end end diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb new file mode 100644 index 00000000000..a97903103c9 --- /dev/null +++ b/spec/finders/license_template_finder_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe LicenseTemplateFinder do + describe '#execute' do + subject(:result) { described_class.new(params).execute } + + let(:categories) { categorised_licenses.keys } + let(:categorised_licenses) { result.group_by(&:category) } + + context 'popular: true' do + let(:params) { { popular: true } } + + it 'only returns popular licenses' do + expect(categories).to contain_exactly(:Popular) + expect(categorised_licenses[:Popular]).to be_present + end + end + + context 'popular: false' do + let(:params) { { popular: false } } + + it 'only returns unpopular licenses' do + expect(categories).to contain_exactly(:Other) + expect(categorised_licenses[:Other]).to be_present + end + end + + context 'popular: nil' do + let(:params) { { popular: nil } } + + it 'returns all licenses known by the Licensee gem' do + from_licensee = Licensee::License.all.map { |l| l.key } + + expect(result.map(&:id)).to match_array(from_licensee) + end + + it 'correctly copies all attributes' do + licensee = Licensee::License.all.first + found = result.find { |r| r.key == licensee.key } + + aggregate_failures do + %i[key name content nickname url meta featured?].each do |k| + expect(found.public_send(k)).to eq(licensee.public_send(k)) + end + end + end + end + end +end diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 5856bccb5b8..55ee87163f9 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -5,12 +5,67 @@ describe AvatarsHelper do let(:user) { create(:user) } - describe '#project_icon' do - it 'returns an url for the avatar' do - project = create(:project, :public, avatar: File.open(uploaded_image_temp_path)) + describe '#project_icon & #group_icon' do + shared_examples 'resource with a default avatar' do |source_type| + it 'returns a default avatar div' do + expect(public_send("#{source_type}_icon", *helper_args)) + .to match(%r{<div class="identicon bg\d+">F</div>}) + end + end + + shared_examples 'resource with a custom avatar' do |source_type| + it 'returns a custom avatar image' do + expect(public_send("#{source_type}_icon", *helper_args)) + .to eq "<img src=\"#{resource.avatar.url}\" alt=\"Banana sample\" />" + end + end + + context 'when providing a project' do + it_behaves_like 'resource with a default avatar', 'project' do + let(:resource) { create(:project, name: 'foo') } + let(:helper_args) { [resource] } + end + + it_behaves_like 'resource with a custom avatar', 'project' do + let(:resource) { create(:project, :public, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource] } + end + end + + context 'when providing a project path' do + it_behaves_like 'resource with a default avatar', 'project' do + let(:resource) { create(:project, name: 'foo') } + let(:helper_args) { [resource.full_path] } + end - expect(helper.project_icon(project.full_path).to_s) - .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" + it_behaves_like 'resource with a custom avatar', 'project' do + let(:resource) { create(:project, :public, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource.full_path] } + end + end + + context 'when providing a group' do + it_behaves_like 'resource with a default avatar', 'group' do + let(:resource) { create(:group, name: 'foo') } + let(:helper_args) { [resource] } + end + + it_behaves_like 'resource with a custom avatar', 'group' do + let(:resource) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource] } + end + end + + context 'when providing a group path' do + it_behaves_like 'resource with a default avatar', 'group' do + let(:resource) { create(:group, name: 'foo') } + let(:helper_args) { [resource.full_path] } + end + + it_behaves_like 'resource with a custom avatar', 'group' do + let(:resource) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource.full_path] } + end end end diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb index 630f3eff258..0c0a0003231 100644 --- a/spec/helpers/button_helper_spec.rb +++ b/spec/helpers/button_helper_spec.rb @@ -79,6 +79,18 @@ describe ButtonHelper do end end + context 'without an ssh key on the user and user_show_add_ssh_key_message unset' do + before do + stub_application_setting(user_show_add_ssh_key_message: false) + end + + it 'there is no warning on the dropdown description' do + description = element.search('.dropdown-menu-inner-content').first + + expect(description).to be_nil + end + end + context 'with an ssh key on the user' do before do create(:key, user: user) diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 115807f954b..540a8674ec2 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -3,19 +3,6 @@ require 'spec_helper' describe GroupsHelper do include ApplicationHelper - describe 'group_icon' do - it 'returns an url for the avatar' do - avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif') - - group = create(:group) - group.avatar = fixture_file_upload(avatar_file_path) - group.save! - - expect(helper.group_icon(group).to_s) - .to eq "<img data-src=\"#{group.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" - end - end - describe 'group_icon_url' do it 'returns an url for the avatar' do avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif') diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 82f588d1a08..4b40d523287 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -80,6 +80,26 @@ describe IconsHelper do end end + describe 'audit icon' do + it 'returns right icon name for standard auth' do + icon_name = 'standard' + expect(audit_icon(icon_name).to_s) + .to eq '<i class="fa fa-key"></i>' + end + + it 'returns right icon name for two-factor auth' do + icon_name = 'two-factor' + expect(audit_icon(icon_name).to_s) + .to eq '<i class="fa fa-key"></i>' + end + + it 'returns right icon name for google_oauth2 auth' do + icon_name = 'google_oauth2' + expect(audit_icon(icon_name).to_s) + .to eq '<i class="fa fa-google"></i>' + end + end + describe 'file_type_icon_class' do it 'returns folder class' do expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder' diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 7a4616ec8eb..44a38f7ca82 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -22,11 +22,18 @@ describe('DiffFile', () => { expect(el.id).toEqual(fileHash); expect(el.classList.contains('diff-file')).toEqual(true); + expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); expect(el.querySelector('.js-file-title')).toBeDefined(); expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); - expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + + expect(vm.file.renderIt).toEqual(false); + vm.file.renderIt = true; + + vm.$nextTick(() => { + expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + }); }); describe('collapsed', () => { @@ -34,6 +41,7 @@ describe('DiffFile', () => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1); expect(vm.file.collapsed).toEqual(false); vm.file.collapsed = true; + vm.file.renderIt = true; vm.$nextTick(() => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(0); diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index d3bf9525924..cce36ecc91f 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -39,6 +39,7 @@ export default { viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG', replacedViewPath: null, collapsed: false, + renderIt: false, tooLarge: false, contextLinesPath: '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 1af49f4985c..8f89984c6e5 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -1,6 +1,7 @@ import mutations from '~/diffs/store/mutations'; import * as types from '~/diffs/store/mutation_types'; import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import diffFileMockData from '../mock_data/diff_file'; describe('DiffsStoreMutations', () => { describe('SET_BASE_CONFIG', () => { @@ -24,6 +25,23 @@ describe('DiffsStoreMutations', () => { }); }); + describe('SET_DIFF_DATA', () => { + it('should set diff data type properly', () => { + const state = {}; + const diffMock = { + diff_files: [diffFileMockData], + }; + + mutations[types.SET_DIFF_DATA](state, diffMock); + + const firstLine = state.diffFiles[0].parallelDiffLines[0]; + + expect(firstLine.right.text).toBeUndefined(); + expect(state.diffFiles[0].renderIt).toEqual(true); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + }); + describe('SET_DIFF_VIEW_TYPE', () => { it('should set diff view type properly', () => { const state = {}; diff --git a/spec/javascripts/ide/components/new_dropdown/button_spec.js b/spec/javascripts/ide/components/new_dropdown/button_spec.js index ef083d06ba7..6a326b5bd92 100644 --- a/spec/javascripts/ide/components/new_dropdown/button_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/button_spec.js @@ -46,4 +46,20 @@ describe('IDE new entry dropdown button component', () => { done(); }); }); + + describe('tooltipTitle', () => { + it('returns empty string when showLabel is true', () => { + expect(vm.tooltipTitle).toBe(''); + }); + + it('returns label', done => { + vm.showLabel = false; + + vm.$nextTick(() => { + expect(vm.tooltipTitle).toBe('Testing'); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/preview/clientside_spec.js b/spec/javascripts/ide/components/preview/clientside_spec.js index 3ec65882418..d6983f5a3b8 100644 --- a/spec/javascripts/ide/components/preview/clientside_spec.js +++ b/spec/javascripts/ide/components/preview/clientside_spec.js @@ -292,6 +292,8 @@ describe('IDE clientside preview', () => { describe('update', () => { beforeEach(() => { jasmine.clock().install(); + + vm.sandpackReady = true; vm.manager.updatePreview = jasmine.createSpy('updatePreview'); vm.manager.listener = jasmine.createSpy('updatePreview'); }); @@ -306,7 +308,7 @@ describe('IDE clientside preview', () => { vm.update(); - jasmine.clock().tick(500); + jasmine.clock().tick(250); expect(vm.initPreview).toHaveBeenCalled(); }); @@ -314,7 +316,7 @@ describe('IDE clientside preview', () => { it('calls updatePreview', () => { vm.update(); - jasmine.clock().tick(500); + jasmine.clock().tick(250); expect(vm.manager.updatePreview).toHaveBeenCalledWith(vm.sandboxOpts); }); diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index 90c28c769f7..8564f04ce8a 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -1,12 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; -import { +import actions, { getMergeRequestData, getMergeRequestChanges, getMergeRequestVersions, + openMergeRequest, } from '~/ide/stores/actions/merge_request'; import service from '~/ide/services'; +import { activityBarViews } from '~/ide/constants'; import { resetStore } from '../../helpers'; describe('IDE store merge request actions', () => { @@ -238,4 +240,101 @@ describe('IDE store merge request actions', () => { }); }); }); + + describe('openMergeRequest', () => { + const mr = { + projectId: 'abcproject', + targetProjectId: 'defproject', + mergeRequestId: 2, + }; + let testMergeRequest; + let testMergeRequestChanges; + + beforeEach(() => { + testMergeRequest = { + source_branch: 'abcbranch', + }; + testMergeRequestChanges = { + changes: [], + }; + store.state.entries = { + foo: {}, + bar: {}, + }; + + spyOn(store, 'dispatch').and.callFake((type) => { + switch (type) { + case 'getMergeRequestData': + return Promise.resolve(testMergeRequest); + case 'getMergeRequestChanges': + return Promise.resolve(testMergeRequestChanges); + default: + return Promise.resolve(); + } + }); + }); + + it('dispatch actions for merge request data', done => { + openMergeRequest(store, mr) + .then(() => { + expect(store.dispatch.calls.allArgs()).toEqual([ + ['getMergeRequestData', mr], + ['setCurrentBranchId', testMergeRequest.source_branch], + ['getBranchData', { + projectId: mr.projectId, + branchId: testMergeRequest.source_branch, + }], + ['getFiles', { + projectId: mr.projectId, + branchId: testMergeRequest.source_branch, + }], + ['getMergeRequestVersions', mr], + ['getMergeRequestChanges', mr], + ]); + }) + .then(done) + .catch(done.fail); + }); + + it('updates activity bar view and gets file data, if changes are found', done => { + testMergeRequestChanges.changes = [ + { new_path: 'foo' }, + { new_path: 'bar' }, + ]; + + openMergeRequest(store, mr) + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith('updateActivityBarView', activityBarViews.review); + + testMergeRequestChanges.changes.forEach((change, i) => { + expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', { + file: store.state.entries[change.new_path], + mrChange: change, + }); + expect(store.dispatch).toHaveBeenCalledWith('getFileData', { + path: change.new_path, + makeFileActive: i === 0, + }); + }); + }) + .then(done) + .catch(done.fail); + }); + + it('flashes message, if error', done => { + const flashSpy = spyOnDependency(actions, 'flash'); + store.dispatch.and.returnValue(Promise.reject()); + + openMergeRequest(store, mr) + .then(() => { + fail('Expected openMergeRequest to throw an error'); + }) + .catch(() => { + expect(flashSpy).toHaveBeenCalledWith(jasmine.any(String)); + }) + .then(done) + .catch(done.fail); + + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 6a85968e199..667e3e0a7ef 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -5,6 +5,7 @@ import { showBranchNotFoundError, createNewBranchFromDefault, getBranchData, + openBranch, } from '~/ide/stores/actions'; import store from '~/ide/stores'; import service from '~/ide/services'; @@ -224,4 +225,55 @@ describe('IDE store project actions', () => { }); }); }); + + describe('openBranch', () => { + const branch = { + projectId: 'feature/lorem-ipsum', + branchId: '123-lorem', + }; + + beforeEach(() => { + store.state.entries = { + foo: { pending: false }, + 'foo/bar-pending': { pending: true }, + 'foo/bar': { pending: false }, + }; + + spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); + }); + + it('dispatches branch actions', done => { + openBranch(store, branch) + .then(() => { + expect(store.dispatch.calls.allArgs()).toEqual([ + ['setCurrentBranchId', branch.branchId], + ['getBranchData', branch], + ['getFiles', branch], + ]); + }) + .then(done) + .catch(done.fail); + }); + + it('handles tree entry action, if basePath is given', done => { + openBranch(store, { ...branch, basePath: 'foo/bar/' }) + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith( + 'handleTreeEntryAction', + store.state.entries['foo/bar'], + ); + }) + .then(done) + .catch(done.fail); + }); + + it('does not handle tree entry action, if entry is pending', done => { + openBranch(store, { ...branch, basePath: 'foo/bar-pending' }) + .then(() => { + expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything()); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/branches/actions_spec.js b/spec/javascripts/ide/stores/modules/branches/actions_spec.js index a0fce578958..010f56af03b 100644 --- a/spec/javascripts/ide/stores/modules/branches/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/branches/actions_spec.js @@ -9,7 +9,6 @@ import { receiveBranchesSuccess, fetchBranches, resetBranches, - openBranch, } from '~/ide/stores/modules/branches/actions'; import { branches, projectData } from '../../../mock_data'; @@ -174,20 +173,5 @@ describe('IDE branches actions', () => { ); }); }); - - describe('openBranch', () => { - it('dispatches goToRoute action with path', done => { - const branchId = branches[0].name; - const expectedPath = `/project/${projectData.name_with_namespace}/edit/${branchId}`; - testAction( - openBranch, - branchId, - mockedState, - [], - [{ type: 'goToRoute', payload: expectedPath }], - done, - ); - }); - }); }); }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 1e836dbc3f9..6ce76aaa03b 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -213,6 +213,33 @@ describe('Multi-file store mutations', () => { expect(localState.changedFiles).toEqual([localState.entries.filePath]); }); + + it('does not add tempFile into changedFiles', () => { + localState.entries.filePath = { + deleted: false, + type: 'blob', + tempFile: true, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); + + it('removes tempFile from changedFiles when deleted', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + type: 'blob', + tempFile: true, + }; + + localState.changedFiles.push({ ...localState.entries.filePath }); + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); }); describe('UPDATE_FILE_AFTER_COMMIT', () => { diff --git a/spec/javascripts/jobs/artifacts_block_spec.js b/spec/javascripts/jobs/artifacts_block_spec.js new file mode 100644 index 00000000000..c544c6f3e89 --- /dev/null +++ b/spec/javascripts/jobs/artifacts_block_spec.js @@ -0,0 +1,120 @@ +import Vue from 'vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import component from '~/jobs/components/artifacts_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Artifacts block', () => { + const Component = Vue.extend(component); + let vm; + + const expireAt = '2018-08-14T09:38:49.157Z'; + const timeago = getTimeago(); + const formatedDate = timeago.format(expireAt); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with expired artifacts', () => { + it('renders expired artifact date and info', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull(); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull(); + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); + + describe('with artifacts that will expire', () => { + it('renders will expire artifact date and info', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: false, + willArtifactsExpire: true, + expireAt, + }); + + expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull(); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull(); + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); + + describe('when the user can keep the artifacts', () => { + it('renders the keep button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + keepArtifactsPath: '/keep', + }); + + expect(vm.$el.querySelector('.js-keep-artifacts')).not.toBeNull(); + }); + }); + + describe('when the user can not keep the artifacts', () => { + it('does not render the keep button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-keep-artifacts')).toBeNull(); + }); + }); + + describe('when the user can download the artifacts', () => { + it('renders the download button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + downloadArtifactsPath: '/download', + }); + + expect(vm.$el.querySelector('.js-download-artifacts')).not.toBeNull(); + }); + }); + + describe('when the user can not download the artifacts', () => { + it('does not render the keep button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-download-artifacts')).toBeNull(); + }); + }); + + describe('when the user can browse the artifacts', () => { + it('does not render the browse button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + browseArtifactsPath: '/browse', + }); + + expect(vm.$el.querySelector('.js-browse-artifacts')).not.toBeNull(); + }); + }); + + describe('when the user can not browse the artifacts', () => { + it('does not render the browse button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-browse-artifacts')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/erased_block_spec.js b/spec/javascripts/jobs/erased_block_spec.js new file mode 100644 index 00000000000..7cf32e984a2 --- /dev/null +++ b/spec/javascripts/jobs/erased_block_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import component from '~/jobs/components/erased_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Erased block', () => { + const Component = Vue.extend(component); + let vm; + + const erasedAt = '2016-11-07T11:11:16.525Z'; + const timeago = getTimeago(); + const formatedDate = timeago.format(erasedAt); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with job erased by user', () => { + beforeEach(() => { + vm = mountComponent(Component, { + erasedByUser: true, + username: 'root', + linkToUser: 'gitlab.com/root', + erasedAt, + }); + }); + + it('renders username and link', () => { + expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('gitlab.com/root'); + + expect(vm.$el.textContent).toContain('Job has been erased by'); + expect(vm.$el.textContent).toContain('root'); + }); + + it('renders erasedAt', () => { + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); + + describe('with erased job', () => { + beforeEach(() => { + vm = mountComponent(Component, { + erasedByUser: false, + erasedAt, + }); + }); + + it('renders username and link', () => { + expect(vm.$el.textContent).toContain('Job has been erased'); + }); + + it('renders erasedAt', () => { + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); +}); diff --git a/spec/javascripts/jobs/job_log_spec.js b/spec/javascripts/jobs/job_log_spec.js new file mode 100644 index 00000000000..406f1c4ccc5 --- /dev/null +++ b/spec/javascripts/jobs/job_log_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import component from '~/jobs/components/job_log.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Job Log', () => { + const Component = Vue.extend(component); + let vm; + + const trace = 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>'; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders provided trace', () => { + vm = mountComponent(Component, { + trace, + isReceivingBuildTrace: true, + }); + + expect(vm.$el.querySelector('code').textContent).toContain('Running with gitlab-runner 11.1.0 (081978aa)'); + }); + + describe('while receiving trace', () => { + it('renders animation', () => { + vm = mountComponent(Component, { + trace, + isReceivingBuildTrace: true, + }); + + expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull(); + }); + }); + + describe('when build trace has finishes', () => { + it('does not render animation', () => { + vm = mountComponent(Component, { + trace, + isReceivingBuildTrace: false, + }); + + expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js index 9c4454252ce..21ef5650b80 100644 --- a/spec/javascripts/jobs/sidebar_details_block_spec.js +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; import job from './mock_data'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Sidebar details block', () => { let SidebarComponent; @@ -20,39 +21,53 @@ describe('Sidebar details block', () => { describe('when it is loading', () => { it('should render a loading spinner', () => { - vm = new SidebarComponent({ - propsData: { - job: {}, - isLoading: true, - }, - }).$mount(); - + vm = mountComponent(SidebarComponent, { + job: {}, + isLoading: true, + }); expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); }); }); - describe("when user can't retry", () => { + describe('when there is no retry path retry', () => { it('should not render a retry button', () => { - vm = new SidebarComponent({ - propsData: { - job: {}, - canUserRetry: false, - isLoading: true, - }, - }).$mount(); + vm = mountComponent(SidebarComponent, { + job: {}, + isLoading: false, + }); expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); }); }); - beforeEach(() => { - vm = new SidebarComponent({ - propsData: { + describe('without terminal path', () => { + it('does not render terminal link', () => { + vm = mountComponent(SidebarComponent, { job, - canUserRetry: true, isLoading: false, - }, - }).$mount(); + }); + + expect(vm.$el.querySelector('.js-terminal-link')).toBeNull(); + }); + }); + + describe('with terminal path', () => { + it('renders terminal link', () => { + vm = mountComponent(SidebarComponent, { + job, + isLoading: false, + terminalPath: 'job/43123/terminal', + }); + + expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull(); + }); + }); + + beforeEach(() => { + vm = mountComponent(SidebarComponent, { + job, + isLoading: false, + }); }); describe('actions', () => { @@ -102,13 +117,15 @@ describe('Sidebar details block', () => { }); it('should render runner ID', () => { - expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual( + 'Runner: local ci runner (#1)', + ); }); it('should render timeout information', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-timeout')), - ).toEqual('Timeout: 1m 40s (from runner)'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-timeout'))).toEqual( + 'Timeout: 1m 40s (from runner)', + ); }); it('should render coverage', () => { diff --git a/spec/javascripts/jobs/stuck_block_spec.js b/spec/javascripts/jobs/stuck_block_spec.js new file mode 100644 index 00000000000..4e2108dfdfb --- /dev/null +++ b/spec/javascripts/jobs/stuck_block_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; +import component from '~/jobs/components/stuck_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Stuck Block Job component', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('with no runners for project', () => { + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: true, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders only information about project not having runners', () => { + expect(vm.$el.querySelector('.js-stuck-no-runners')).not.toBeNull(); + expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull(); + }); + + it('renders link to runners page', () => { + expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual( + '/root/project/runners#js-runners-settings', + ); + }); + }); + + describe('with tags', () => { + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: false, + tags: ['docker', 'gitlab-org'], + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders information about the tags not being set', () => { + expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-with-tags')).not.toBeNull(); + expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull(); + }); + + it('renders tags', () => { + expect(vm.$el.textContent).toContain('docker'); + expect(vm.$el.textContent).toContain('gitlab-org'); + }); + + it('renders link to runners page', () => { + expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual( + '/root/project/runners#js-runners-settings', + ); + }); + }); + + describe('without active runners', () => { + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: false, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders information about project not having runners', () => { + expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-no-active-runner')).not.toBeNull(); + }); + + it('renders link to runners page', () => { + expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual( + '/root/project/runners#js-runners-settings', + ); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/reports/modal_open_name_spec.js b/spec/javascripts/reports/components/modal_open_name_spec.js index 8635203c413..b18b3ef03d1 100644 --- a/spec/javascripts/vue_shared/components/reports/modal_open_name_spec.js +++ b/spec/javascripts/reports/components/modal_open_name_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import component from '~/vue_shared/components/reports/modal_open_name.vue'; +import component from '~/reports/components/modal_open_name.vue'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; describe('Modal open name', () => { diff --git a/spec/javascripts/vue_shared/components/reports/report_link_spec.js b/spec/javascripts/reports/components/report_link_spec.js index a4691f3712f..cd6911e2f59 100644 --- a/spec/javascripts/vue_shared/components/reports/report_link_spec.js +++ b/spec/javascripts/reports/components/report_link_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import component from '~/vue_shared/components/reports/report_link.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import component from '~/reports/components/report_link.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('report link', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/reports/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js index 4e3986acb16..6f6eb161d14 100644 --- a/spec/javascripts/vue_shared/components/reports/report_section_spec.js +++ b/spec/javascripts/reports/components/report_section_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import reportSection from '~/vue_shared/components/reports/report_section.vue'; +import reportSection from '~/reports/components/report_section.vue'; import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; describe('Report section', () => { diff --git a/spec/javascripts/vue_shared/components/reports/summary_row_spec.js b/spec/javascripts/reports/components/summary_row_spec.js index ac076f05bc0..fab7693581c 100644 --- a/spec/javascripts/vue_shared/components/reports/summary_row_spec.js +++ b/spec/javascripts/reports/components/summary_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import component from '~/vue_shared/components/reports/summary_row.vue'; +import component from '~/reports/components/summary_row.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Summary row', () => { diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb new file mode 100644 index 00000000000..48140305e26 --- /dev/null +++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::ProjectReferenceFilter do + include FilterSpecHelper + + def invalidate_reference(reference) + "#{reference.reverse}" + end + + def get_reference(project) + project.to_reference_with_postfix + end + + let(:project) { create(:project, :public) } + subject { project } + let(:subject_name) { "project" } + let(:reference) { get_reference(project) } + + it_behaves_like 'user reference or project reference' + + it 'ignores invalid projects' do + exp = act = "Hey #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq(CGI.escapeHTML(exp)) + end + + it 'allows references with text after the > character' do + doc = reference_filter("Hey #{reference}foo") + expect(doc.css('a').first.attr('href')).to eq urls.project_url(subject) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Hey #{CGI.escapeHTML(reference)}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + it 'includes default classes' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project has-tooltip' + end + + context 'in group context' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + let(:nested_group) { create(:group, :nested) } + let(:nested_project) { create(:project, group: nested_group) } + + it 'supports mentioning a project' do + reference = get_reference(project) + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.project_url(project) + end + + it 'supports mentioning a project in a nested group' do + reference = get_reference(nested_project) + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.project_url(nested_project) + end + end + + describe '#projects_hash' do + it 'returns a Hash containing all Projects' do + document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>") + filter = described_class.new(document, project: project) + + expect(filter.projects_hash).to eq({ project.full_path => project }) + end + end + + describe '#projects' do + it 'returns the projects mentioned in a document' do + document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>") + filter = described_class.new(document, project: project) + + expect(filter.projects).to eq([project.full_path]) + end + end +end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 2f86a046d28..334d29a5368 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -3,9 +3,17 @@ require 'spec_helper' describe Banzai::Filter::UserReferenceFilter do include FilterSpecHelper + def get_reference(user) + user.to_reference + end + let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:reference) { user.to_reference } + subject { user } + let(:subject_name) { "user" } + let(:reference) { get_reference(user) } + + it_behaves_like 'user reference or project reference' it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -66,45 +74,6 @@ describe Banzai::Filter::UserReferenceFilter do end end - context 'mentioning a user' do - it_behaves_like 'a reference containing an element node' - - it 'links to a User' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links to a User with a period' do - user = create(:user, name: 'alphA.Beta') - - doc = reference_filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'links to a User with an underscore' do - user = create(:user, name: 'ping_pong_king') - - doc = reference_filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'links to a User with different case-sensitivity' do - user = create(:user, username: 'RescueRanger') - - doc = reference_filter("Hey #{user.to_reference.upcase}") - expect(doc.css('a').length).to eq 1 - expect(doc.css('a').text).to eq(user.to_reference) - end - - it 'includes a data-user attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - end - context 'mentioning a group' do it_behaves_like 'a reference containing an element node' @@ -154,36 +123,6 @@ describe Banzai::Filter::UserReferenceFilter do expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip' end - it 'supports an :only_path context' do - doc = reference_filter("Hey #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.user_path(user) - end - - context 'referencing a user in a link href' do - let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} } - - it 'links to a User' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links with adjacent text' do - doc = reference_filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(%r{\(<a.+>User</a>\.\)}) - end - - it 'includes a data-user attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - end - context 'when a project is not specified' do let(:project) { nil } @@ -227,7 +166,7 @@ describe Banzai::Filter::UserReferenceFilter do end it 'supports mentioning a single user' do - reference = group_member.to_reference + reference = get_reference(group_member) doc = reference_filter("Hey #{reference}", context) expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member) @@ -243,7 +182,7 @@ describe Banzai::Filter::UserReferenceFilter do describe '#namespaces' do it 'returns a Hash containing all Namespaces' do - document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>") filter = described_class.new(document, project: project) ns = user.namespace @@ -253,7 +192,7 @@ describe Banzai::Filter::UserReferenceFilter do describe '#usernames' do it 'returns the usernames mentioned in a document' do - document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>") filter = described_class.new(document, project: project) expect(filter.usernames).to eq([user.username]) diff --git a/spec/lib/banzai/reference_parser/project_parser_spec.rb b/spec/lib/banzai/reference_parser/project_parser_spec.rb new file mode 100644 index 00000000000..e4936aa9e57 --- /dev/null +++ b/spec/lib/banzai/reference_parser/project_parser_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::ReferenceParser::ProjectParser do + include ReferenceParserHelpers + + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-project attribute' do + context 'using an existing project ID' do + it 'returns an Array of projects' do + link['data-project'] = project.id.to_s + + expect(subject.gather_references([link])).to eq([project]) + end + end + + context 'using a non-existing project ID' do + it 'returns an empty Array' do + link['data-project'] = '' + + expect(subject.gather_references([link])).to eq([]) + end + end + + context 'using a private project ID' do + it 'returns an empty Array when unauthorized' do + private_project = create(:project, :private) + + link['data-project'] = private_project.id.to_s + + expect(subject.gather_references([link])).to eq([]) + end + + it 'returns an Array when authorized' do + private_project = create(:project, :private, namespace: user.namespace) + + link['data-project'] = private_project.id.to_s + + expect(subject.gather_references([link])).to eq([private_project]) + end + end + end + end +end diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb index 37b38776775..11e605eece6 100644 --- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb +++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb @@ -244,9 +244,11 @@ describe Gitlab::Cleanup::ProjectUploads do orphaned1 = create(:upload, :personal_snippet_upload, :with_file) orphaned2 = create(:upload, :namespace_upload, :with_file) orphaned3 = create(:upload, :attachment_upload, :with_file) + orphaned4 = create(:upload, :favicon_upload, :with_file) paths << orphaned1.absolute_path paths << orphaned2.absolute_path paths << orphaned3.absolute_path + paths << orphaned4.absolute_path Upload.delete_all expect(logger).not_to receive(:info).with(/move|fix/i) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index eb7148ff108..4e83b27e4a5 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -48,10 +48,10 @@ describe Gitlab::Database::MigrationHelpers do allow(model).to receive(:transaction_open?).and_return(false) end - context 'using PostgreSQL' do + context 'using PostgreSQL', :postgresql do before do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - allow(model).to receive(:disable_statement_timeout) + allow(model).to receive(:disable_statement_timeout).and_call_original end it 'creates the index concurrently' do @@ -114,12 +114,12 @@ describe Gitlab::Database::MigrationHelpers do before do allow(model).to receive(:transaction_open?).and_return(false) allow(model).to receive(:index_exists?).and_return(true) + allow(model).to receive(:disable_statement_timeout).and_call_original end context 'using PostgreSQL' do before do allow(model).to receive(:supports_drop_index_concurrently?).and_return(true) - allow(model).to receive(:disable_statement_timeout) end describe 'by column name' do @@ -162,7 +162,7 @@ describe Gitlab::Database::MigrationHelpers do context 'using MySQL' do it 'removes an index' do - expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(Gitlab::Database).to receive(:postgresql?).and_return(false).twice expect(model).to receive(:remove_index) .with(:users, { column: :foo }) @@ -224,21 +224,26 @@ describe Gitlab::Database::MigrationHelpers do context 'using PostgreSQL' do before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) allow(Gitlab::Database).to receive(:mysql?).and_return(false) end it 'creates a concurrent foreign key and validates it' do - expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + expect(model).to receive(:execute).with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id) end it 'appends a valid ON DELETE statement' do - expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).with(/ON DELETE SET NULL/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + expect(model).to receive(:execute).with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id, @@ -291,13 +296,68 @@ describe Gitlab::Database::MigrationHelpers do describe '#disable_statement_timeout' do context 'using PostgreSQL' do - it 'disables statement timeouts' do + it 'disables statement timeouts to current transaction only' do expect(Gitlab::Database).to receive(:postgresql?).and_return(true) - expect(model).to receive(:execute).with('SET statement_timeout TO 0') + expect(model).to receive(:execute).with('SET LOCAL statement_timeout TO 0') model.disable_statement_timeout end + + # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner) + context 'with real environment', :postgresql, :delete do + before do + model.execute("SET statement_timeout TO '20000'") + end + + after do + model.execute('RESET ALL') + end + + it 'defines statement to 0 only for current transaction' do + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s') + + model.connection.transaction do + model.disable_statement_timeout + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0') + end + + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s') + end + end + + context 'when passing a blocks' do + it 'disables statement timeouts on session level and executes the block' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(model).to receive(:execute).with('SET statement_timeout TO 0') + expect(model).to receive(:execute).with('RESET ALL') + + expect { |block| model.disable_statement_timeout(&block) }.to yield_control + end + + # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner) + context 'with real environment', :postgresql, :delete do + before do + model.execute("SET statement_timeout TO '20000'") + end + + after do + model.execute('RESET ALL') + end + + it 'defines statement to 0 for any code run inside the block' do + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s') + + model.disable_statement_timeout do + model.connection.transaction do + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0') + end + + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0') + end + end + end + end end context 'using MySQL' do @@ -308,6 +368,16 @@ describe Gitlab::Database::MigrationHelpers do model.disable_statement_timeout end + + context 'when passing a blocks' do + it 'executes the block of code' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).not_to receive(:execute) + + expect { |block| model.disable_statement_timeout(&block) }.to yield_control + end + end end end diff --git a/spec/lib/gitlab/git/merge_base_spec.rb b/spec/lib/gitlab/git/merge_base_spec.rb new file mode 100644 index 00000000000..2f4e043a20f --- /dev/null +++ b/spec/lib/gitlab/git/merge_base_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Git::MergeBase do + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } + subject(:merge_base) { described_class.new(repository, refs) } + + shared_context 'existing refs with a merge base', :existing_refs do + let(:refs) do + %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209) + end + end + + shared_context 'when passing a missing ref', :missing_ref do + let(:refs) do + %w(304d257dcb821665ab5110318fc58a007bd104ed aaaa) + end + end + + shared_context 'when passing refs that do not have a common ancestor', :no_common_ancestor do + let(:refs) { ['304d257dcb821665ab5110318fc58a007bd104ed', TestEnv::BRANCH_SHA['orphaned-branch']] } + end + + describe '#sha' do + context 'when the refs exist', :existing_refs do + it 'returns the SHA of the merge base' do + expect(merge_base.sha).not_to be_nil + end + + it 'memoizes the result' do + expect(repository).to receive(:merge_base).once.and_call_original + + 2.times { merge_base.sha } + end + end + + context 'when passing a missing ref', :missing_ref do + it 'does not call merge_base on the repository but raises an error' do + expect(repository).not_to receive(:merge_base) + + expect { merge_base.sha }.to raise_error(Gitlab::Git::UnknownRef) + end + end + + it 'returns `nil` when the refs do not have a common ancestor', :no_common_ancestor do + expect(merge_base.sha).to be_nil + end + + it 'returns a merge base when passing 2 branch names' do + merge_base = described_class.new(repository, %w(master feature)) + + expect(merge_base.sha).to be_present + end + + it 'returns a merge base when passing a tag name' do + merge_base = described_class.new(repository, %w(master v1.0.0)) + + expect(merge_base.sha).to be_present + end + end + + describe '#commit' do + context 'for existing refs with a merge base', :existing_refs do + it 'finds the commit for the merge base' do + expect(merge_base.commit).to be_a(Commit) + end + + it 'only looks up the commit once' do + expect(repository).to receive(:commit_by).once.and_call_original + + 2.times { merge_base.commit } + end + end + + it 'does not try to find the commit when there is no sha', :no_common_ancestor do + expect(repository).not_to receive(:commit_by) + + merge_base.commit + end + end + + describe '#unknown_refs', :missing_ref do + it 'returns the the refs passed that are not part of the repository' do + expect(merge_base.unknown_refs).to contain_exactly('aaaa') + end + + it 'only looks up the commits once' do + expect(merge_base).to receive(:commits_for_refs).once.and_call_original + + 2.times { merge_base.unknown_refs } + end + end +end diff --git a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb new file mode 100644 index 00000000000..2eabccd5dff --- /dev/null +++ b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Template::Finders::RepoTemplateFinder do + set(:project) { create(:project, :repository) } + + let(:categories) { { 'HTML' => 'html' } } + + subject(:finder) { described_class.new(project, 'files/', '.html', categories) } + + describe '#read' do + it 'returns the content of the given path' do + result = finder.read('files/html/500.html') + + expect(result).to be_present + end + + it 'raises an error if the path does not exist' do + expect { finder.read('does/not/exist') }.to raise_error(described_class::FileNotFoundError) + end + end + + describe '#find' do + it 'returns the full path of the found template' do + result = finder.find('500') + + expect(result).to eq('files/html/500.html') + end + end + + describe '#list_files_for' do + it 'returns the full path of the found files' do + result = finder.list_files_for('files/html') + + expect(result).to contain_exactly('files/html/500.html') + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 32b8755ee9a..42b627b6823 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1103,6 +1103,12 @@ describe Ci::Build do it { is_expected.to be_cancelable } end + + context 'when build is created' do + let(:build) { create(:ci_build, :created) } + + it { is_expected.to be_cancelable } + end end context 'when build is not cancelable' do diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 20600f5fa38..f2aad455d5f 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -30,7 +30,7 @@ describe InternalId do context 'with existing issues' do before do - rand(1..10).times { create(:issue, project: project) } + create_list(:issue, 2, project: project) described_class.delete_all end @@ -54,7 +54,7 @@ describe InternalId do end it 'generates a strictly monotone, gapless sequence' do - seq = (0..rand(100)).map do + seq = Array.new(10).map do described_class.generate_next(issue, scope, usage, init) end normalized = seq.map { |i| i - seq.min } diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb new file mode 100644 index 00000000000..c633e1908d4 --- /dev/null +++ b/spec/models/license_template_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe LicenseTemplate do + describe '#content' do + it 'calls a proc exactly once if provided' do + lazy = build_template(-> { 'bar' }) + content = lazy.content + + expect(content).to eq('bar') + expect(content.object_id).to eq(lazy.content.object_id) + + content.replace('foo') + expect(lazy.content).to eq('foo') + end + + it 'returns a string if provided' do + lazy = build_template('bar') + + expect(lazy.content).to eq('bar') + end + end + + describe '#resolve!' do + let(:content) do + <<~TEXT + Pretend License + + [project] + + Copyright (c) [year] [fullname] + TEXT + end + + let(:expected) do + <<~TEXT + Pretend License + + Foo Project + + Copyright (c) 1985 Nick Thomas + TEXT + end + + let(:template) { build_template(content) } + + it 'updates placeholders in a copy of the template content' do + expect(template.content.object_id).to eq(content.object_id) + + template.resolve!(project_name: "Foo Project", fullname: "Nick Thomas", year: "1985") + + expect(template.content).to eq(expected) + expect(template.content.object_id).not_to eq(content.object_id) + end + end + + def build_template(content) + described_class.new(id: 'foo', name: 'foo', category: :Other, content: content) + end +end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index 77c475b9f52..e545b674b4f 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -94,9 +94,39 @@ RSpec.describe NotificationSetting do end end - context 'email events' do - it 'includes EXCLUDED_WATCHER_EVENTS in EMAIL_EVENTS' do - expect(described_class::EMAIL_EVENTS).to include(*described_class::EXCLUDED_WATCHER_EVENTS) + describe '.email_events' do + subject { described_class.email_events } + + it 'returns email events' do + expect(subject).to include( + :new_note, + :new_issue, + :reopen_issue, + :close_issue, + :reassign_issue, + :new_merge_request, + :reopen_merge_request, + :close_merge_request, + :reassign_merge_request, + :merge_merge_request, + :failed_pipeline, + :success_pipeline + ) + end + + it 'includes EXCLUDED_WATCHER_EVENTS' do + expect(subject).to include(*described_class::EXCLUDED_WATCHER_EVENTS) + end + end + + describe '#email_events' do + let(:source) { build(:group) } + + subject { build(:notification_setting, source: source) } + + it 'calls email_events' do + expect(described_class).to receive(:email_events).with(source) + subject.email_events end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 03beb9187ed..076de06cf99 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -364,6 +364,15 @@ describe Project do it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) } end + describe '#to_reference_with_postfix' do + it 'returns the full path with reference_postfix' do + namespace = create(:namespace, path: 'sample-namespace') + project = create(:project, path: 'sample-project', namespace: namespace) + + expect(project.to_reference_with_postfix).to eq 'sample-namespace/sample-project>' + end + end + describe '#to_reference' do let(:owner) { create(:user, name: 'Gitlab') } let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 52ec8dbe25a..2859d5149ec 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -296,41 +296,31 @@ describe Repository do end describe '#new_commits' do - shared_examples 'finding unreferenced commits' do - set(:project) { create(:project, :repository) } - let(:repository) { project.repository } + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } - subject { repository.new_commits(rev) } + subject { repository.new_commits(rev) } - context 'when there are no new commits' do - let(:rev) { repository.commit.id } + context 'when there are no new commits' do + let(:rev) { repository.commit.id } - it 'returns an empty array' do - expect(subject).to eq([]) - end + it 'returns an empty array' do + expect(subject).to eq([]) end + end - context 'when new commits are found' do - let(:branch) { 'orphaned-branch' } - let!(:rev) { repository.commit(branch).id } + context 'when new commits are found' do + let(:branch) { 'orphaned-branch' } + let!(:rev) { repository.commit(branch).id } - it 'returns the commits' do - repository.delete_branch(branch) + it 'returns the commits' do + repository.delete_branch(branch) - expect(subject).not_to be_empty - expect(subject).to all( be_a(::Commit) ) - expect(subject.size).to eq(1) - end + expect(subject).not_to be_empty + expect(subject).to all( be_a(::Commit) ) + expect(subject.size).to eq(1) end end - - context 'when Gitaly handles the request' do - it_behaves_like 'finding unreferenced commits' - end - - context 'when Gitaly is disabled', :disable_gitaly do - it_behaves_like 'finding unreferenced commits' - end end describe '#commits_by' do diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 5814d834572..6adbbb40489 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -3,6 +3,32 @@ require 'spec_helper' describe API::Jobs do include HttpIOHelpers + shared_examples 'a job with artifacts and trace' do |result_is_array: true| + context 'with artifacts and trace' do + let!(:second_job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) } + + it 'returns artifacts and trace data', :skip_before_request do + get api(api_endpoint, api_user) + json_job = result_is_array ? json_response.select { |job| job['id'] == second_job.id }.first : json_response + + expect(json_job['artifacts_file']).not_to be_nil + expect(json_job['artifacts_file']).not_to be_empty + expect(json_job['artifacts_file']['filename']).to eq(second_job.artifacts_file.filename) + expect(json_job['artifacts_file']['size']).to eq(second_job.artifacts_file.size) + expect(json_job['artifacts']).not_to be_nil + expect(json_job['artifacts']).to be_an Array + expect(json_job['artifacts'].size).to eq(second_job.job_artifacts.length) + json_job['artifacts'].each do |artifact| + expect(artifact).not_to be_nil + file_type = Ci::JobArtifact.file_types[artifact['file_type']] + expect(artifact['size']).to eq(second_job.job_artifacts.where(file_type: file_type).first.size) + expect(artifact['filename']).to eq(second_job.job_artifacts.where(file_type: file_type).first.filename) + expect(artifact['file_format']).to eq(second_job.job_artifacts.where(file_type: file_type).first.file_format) + end + end + end + end + set(:project) do create(:project, :repository, public_builds: false) end @@ -49,6 +75,20 @@ describe API::Jobs do expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) end + context 'without artifacts and trace' do + it 'returns no artifacts nor trace data' do + json_job = json_response.first + + expect(json_job['artifacts_file']).to be_nil + expect(json_job['artifacts']).to be_an Array + expect(json_job['artifacts']).to be_empty + end + end + + it_behaves_like 'a job with artifacts and trace' do + let(:api_endpoint) { "/projects/#{project.id}/jobs" } + end + it 'returns pipeline data' do json_job = json_response.first @@ -60,7 +100,7 @@ describe API::Jobs do end it 'avoids N+1 queries', :skip_before_request do - first_build = create(:ci_build, :artifacts, pipeline: pipeline) + first_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) first_build.runner = create(:ci_runner) first_build.user = create(:user) first_build.save @@ -68,7 +108,7 @@ describe API::Jobs do control_count = ActiveRecord::QueryRecorder.new { go }.count second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) - second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) + second_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: second_pipeline) second_build.runner = create(:ci_runner) second_build.user = create(:user) second_build.save @@ -117,9 +157,11 @@ describe API::Jobs do describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do let(:query) { Hash.new } - before do - job - get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + before do |example| + unless example.metadata[:skip_before_request] + job + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + end end context 'authorized user' do @@ -133,6 +175,13 @@ describe API::Jobs do expect(json_response).not_to be_empty expect(json_response.first['commit']['id']).to eq project.commit.id expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) + expect(json_response.first['artifacts_file']).to be_nil + expect(json_response.first['artifacts']).to be_an Array + expect(json_response.first['artifacts']).to be_empty + end + + it_behaves_like 'a job with artifacts and trace' do + let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" } end it 'returns pipeline data' do @@ -183,7 +232,7 @@ describe API::Jobs do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query end.count - 3.times { create(:ci_build, :artifacts, pipeline: pipeline) } + 3.times { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) } expect do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query @@ -201,8 +250,10 @@ describe API::Jobs do end describe 'GET /projects/:id/jobs/:job_id' do - before do - get api("/projects/#{project.id}/jobs/#{job.id}", api_user) + before do |example| + unless example.metadata[:skip_before_request] + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) + end end context 'authorized user' do @@ -219,10 +270,17 @@ describe API::Jobs do expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at) expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at) expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) + expect(json_response['artifacts_file']).to be_nil + expect(json_response['artifacts']).to be_an Array + expect(json_response['artifacts']).to be_empty expect(json_response['duration']).to eq(job.duration) expect(json_response['web_url']).to be_present end + it_behaves_like 'a job with artifacts and trace', result_is_array: false do + let(:api_endpoint) { "/projects/#{project.id}/jobs/#{second_job.id}" } + end + it 'returns pipeline data' do json_job = json_response diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 6063afc213d..519638ebb82 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -465,4 +465,77 @@ describe API::Repositories do end end end + + describe 'GET :id/repository/merge_base' do + let(:refs) do + %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209) + end + + subject(:request) do + get(api("/projects/#{project.id}/repository/merge_base", current_user), refs: refs) + end + + shared_examples 'merge base' do + it 'returns the common ancestor' do + request + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['id']).to be_present + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'merge base' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:current_user) { nil } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'merge base' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:current_user) { guest } + end + end + + context 'when passing refs that do not exist' do + it_behaves_like '400 response' do + let(:refs) { %w(304d257dcb821665ab5110318fc58a007bd104ed missing) } + let(:current_user) { user } + let(:message) { 'Could not find ref: missing' } + end + end + + context 'when passing refs that do not have a merge base' do + it_behaves_like '404 response' do + let(:refs) { ['304d257dcb821665ab5110318fc58a007bd104ed', TestEnv::BRANCH_SHA['orphaned-branch']] } + let(:current_user) { user } + let(:message) { '404 Merge Base Not Found' } + end + end + + context 'when not enough refs are passed' do + let(:refs) { %w(only-one) } + let(:current_user) { user } + + it 'renders a bad request error' do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Provide exactly 2 refs') + end + end + end end diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index 6bb53fdc98d..d1e16ab9ca9 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -56,6 +56,8 @@ describe API::Templates do end it 'returns a license template' do + expect(response).to have_gitlab_http_status(200) + expect(json_response['key']).to eq('mit') expect(json_response['name']).to eq('MIT License') expect(json_response['nickname']).to be_nil @@ -181,6 +183,7 @@ describe API::Templates do it 'replaces the copyright owner placeholder with the name of the current user' do get api('/templates/licenses/mit', user) + expect(response).to have_gitlab_http_status(200) expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") end end diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb new file mode 100644 index 00000000000..8f795bb561e --- /dev/null +++ b/spec/rubocop/cop/migration/add_reference_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/add_reference' + +describe RuboCop::Cop::Migration::AddReference do + include CopHelper + + let(:cop) { described_class.new } + + context 'outside of a migration' do + it 'does not register any offenses' do + expect_no_offenses(<<~RUBY) + def up + add_reference(:projects, :users) + end + RUBY + end + end + + context 'in a migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when using add_reference without index' do + expect_offense(<<~RUBY) + call do + add_reference(:projects, :users) + ^^^^^^^^^^^^^ `add_reference` requires `index: true` + end + RUBY + end + + it 'registers an offense when using add_reference index disabled' do + expect_offense(<<~RUBY) + def up + add_reference(:projects, :users, index: false) + ^^^^^^^^^^^^^ `add_reference` requires `index: true` + end + RUBY + end + + it 'does not register an offense when using add_reference with index enabled' do + expect_no_offenses(<<~RUBY) + def up + add_reference(:projects, :users, index: true) + end + RUBY + end + end +end diff --git a/spec/serializers/project_mirror_serializer_spec.rb b/spec/serializers/project_mirror_serializer_spec.rb new file mode 100644 index 00000000000..5e47163532a --- /dev/null +++ b/spec/serializers/project_mirror_serializer_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe ProjectMirrorSerializer do + it 'represents ProjectMirror entities' do + expect(described_class.entity_class).to eq(ProjectMirrorEntity) + end +end diff --git a/spec/services/ci/enqueue_build_service_spec.rb b/spec/services/ci/enqueue_build_service_spec.rb new file mode 100644 index 00000000000..e41b8e4800b --- /dev/null +++ b/spec/services/ci/enqueue_build_service_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Ci::EnqueueBuildService, '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:ci_build) { create(:ci_build, :created) } + + subject { described_class.new(project, user).execute(ci_build) } + + it 'enqueues the build' do + subject + + expect(ci_build.pending?).to be_truthy + end +end diff --git a/spec/services/commits/tag_service_spec.rb b/spec/services/commits/tag_service_spec.rb new file mode 100644 index 00000000000..82377a8dace --- /dev/null +++ b/spec/services/commits/tag_service_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Commits::TagService do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + let(:commit) { project.commit } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + let(:service) { described_class.new(project, user, opts) } + + shared_examples 'tag failure' do + it 'returns a hash with the :error status' do + result = service.execute(commit) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + + it 'does not add a system note' do + service.execute(commit) + + description_notes = find_notes('tag') + expect(description_notes).to be_empty + end + end + + def find_notes(action) + commit + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + + context 'valid params' do + let(:opts) do + { + tag_name: 'v1.2.3', + tag_message: 'Release' + } + end + + def find_notes(action) + commit + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + + context 'when tagging succeeds' do + it 'returns a hash with the :success status and created tag' do + result = service.execute(commit) + + expect(result[:status]).to eq(:success) + + tag = result[:tag] + expect(tag.name).to eq(opts[:tag_name]) + expect(tag.message).to eq(opts[:tag_message]) + end + + it 'adds a system note' do + service.execute(commit) + + description_notes = find_notes('tag') + expect(description_notes.length).to eq(1) + end + end + + context 'when tagging fails' do + let(:tag_error) { 'GitLab: You are not allowed to push code to this project.' } + + before do + tag_stub = instance_double(Tags::CreateService) + allow(Tags::CreateService).to receive(:new).and_return(tag_stub) + allow(tag_stub).to receive(:execute).and_return({ + status: :error, message: tag_error + }) + end + + it_behaves_like 'tag failure' do + let(:error_message) { tag_error } + end + end + end + + context 'invalid params' do + let(:opts) do + {} + end + + it_behaves_like 'tag failure' do + let(:error_message) { 'Missing parameter tag_name' } + end + end + end +end diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 784dac55454..a8c994c101c 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -11,40 +11,6 @@ describe Notes::QuickActionsService do end end - shared_examples 'note on noteable that does not support quick actions' do - include_context 'note on noteable' - - before do - note.note = note_text - end - - describe 'note with only command' do - describe '/close, /label, /assign & /milestone' do - let(:note_text) { %(/close\n/assign @#{assignee.username}") } - - it 'saves the note and does not alter the note text' do - content, command_params = service.extract_commands(note) - - expect(content).to eq note_text - expect(command_params).to be_empty - end - end - end - - describe 'note with command & text' do - describe '/close, /label, /assign & /milestone' do - let(:note_text) { %(HELLO\n/close\n/assign @#{assignee.username}\nWORLD) } - - it 'saves the note and does not alter the note text' do - content, command_params = service.extract_commands(note) - - expect(content).to eq note_text - expect(command_params).to be_empty - end - end - end - end - shared_examples 'note on noteable that supports quick actions' do include_context 'note on noteable' @@ -147,16 +113,16 @@ describe Notes::QuickActionsService do expect(described_class.noteable_update_service(note)).to eq(Issues::UpdateService) end - it 'returns Issues::UpdateService for a note on a merge request' do + it 'returns MergeRequests::UpdateService for a note on a merge request' do note = create(:note_on_merge_request, project: project) expect(described_class.noteable_update_service(note)).to eq(MergeRequests::UpdateService) end - it 'returns nil for a note on a commit' do + it 'returns Commits::TagService for a note on a commit' do note = create(:note_on_commit, project: project) - expect(described_class.noteable_update_service(note)).to be_nil + expect(described_class.noteable_update_service(note)).to eq(Commits::TagService) end end @@ -175,7 +141,7 @@ describe Notes::QuickActionsService do let(:note) { create(:note_on_commit, project: project) } it 'returns false' do - expect(described_class.supported?(note)).to be_falsy + expect(described_class.supported?(note)).to be_truthy end end end @@ -203,10 +169,6 @@ describe Notes::QuickActionsService do it_behaves_like 'note on noteable that supports quick actions' do let(:note) { build(:note_on_merge_request, project: project) } end - - it_behaves_like 'note on noteable that does not support quick actions' do - let(:note) { build(:note_on_commit, project: project) } - end end context 'CE restriction for issue assignees' do diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index 81dc7c57f4a..507909d9231 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -65,6 +65,31 @@ describe PreviewMarkdownService do end end + context 'commit description' do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit } + let(:params) do + { + text: "My work\n/tag v1.2.3 Stable release", + quick_actions_target_type: 'Commit', + quick_actions_target_id: commit.id + } + end + let(:service) { described_class.new(project, user, params) } + + it 'removes quick actions from text' do + result = service.execute + + expect(result[:text]).to eq 'My work' + end + + it 'explains quick actions effect' do + result = service.execute + + expect(result[:commands]).to eq 'Tags this commit to v1.2.3 with "Stable release".' + end + end + it 'sets correct markdown engine' do service = described_class.new(project, user, { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION }) result = service.execute diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 743e281183e..bf1c157c4a2 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -6,6 +6,7 @@ describe QuickActions::InterpretService do let(:developer2) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } + let(:commit) { create(:commit, project: project) } let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:bug) { create(:label, project: project, title: 'Bug') } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } @@ -347,6 +348,14 @@ describe QuickActions::InterpretService do end end + shared_examples 'tag command' do + it 'tags a commit' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(tag_name: tag_name, tag_message: tag_message) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -628,16 +637,6 @@ describe QuickActions::InterpretService do let(:issuable) { merge_request } end - it_behaves_like 'todo command' do - let(:content) { '/todo' } - let(:issuable) { issue } - end - - it_behaves_like 'todo command' do - let(:content) { '/todo' } - let(:issuable) { merge_request } - end - it_behaves_like 'done command' do let(:content) { '/done' } let(:issuable) { issue } @@ -787,6 +786,28 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + context '/todo' do + let(:content) { '/todo' } + + context 'if issuable is an Issue' do + it_behaves_like 'todo command' do + let(:issuable) { issue } + end + end + + context 'if issuable is a MergeRequest' do + it_behaves_like 'todo command' do + let(:issuable) { merge_request } + end + end + + context 'if issuable is a Commit' do + it_behaves_like 'empty command' do + let(:issuable) { commit } + end + end + end + context '/copy_metadata command' do let(:todo_label) { create(:label, project: project, title: 'To Do') } let(:inreview_label) { create(:label, project: project, title: 'In Review') } @@ -971,6 +992,12 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end end + + context 'if issuable is a Commit' do + let(:content) { '/award :100:' } + let(:issuable) { commit } + it_behaves_like 'empty command' + end end context '/shrug command' do @@ -1102,6 +1129,32 @@ describe QuickActions::InterpretService do it_behaves_like 'empty command' end end + + context '/tag command' do + let(:issuable) { commit } + + context 'ignores command with no argument' do + it_behaves_like 'empty command' do + let(:content) { '/tag' } + end + end + + context 'tags a commit with a tag name' do + it_behaves_like 'tag command' do + let(:tag_name) { 'v1.2.3' } + let(:tag_message) { nil } + let(:content) { "/tag #{tag_name}" } + end + end + + context 'tags a commit with a tag name and message' do + it_behaves_like 'tag command' do + let(:tag_name) { 'v1.2.3' } + let(:tag_message) { 'Stable release' } + let(:content) { "/tag #{tag_name} #{tag_message}" } + end + end + end end describe '#explain' do @@ -1319,5 +1372,39 @@ describe QuickActions::InterpretService do expect(explanations).to eq(["Moves this issue to test/project."]) end end + + describe 'tag a commit' do + describe 'with a tag name' do + context 'without a message' do + let(:content) { '/tag v1.2.3' } + + it 'includes the tag name only' do + _, explanations = service.explain(content, commit) + + expect(explanations).to eq(["Tags this commit to v1.2.3."]) + end + end + + context 'with an empty message' do + let(:content) { '/tag v1.2.3 ' } + + it 'includes the tag name only' do + _, explanations = service.explain(content, commit) + + expect(explanations).to eq(["Tags this commit to v1.2.3."]) + end + end + end + + describe 'with a tag name and message' do + let(:content) { '/tag v1.2.3 Stable release' } + + it 'includes the tag name and message' do + _, explanations = service.explain(content, commit) + + expect(explanations).to eq(["Tags this commit to v1.2.3 with \"Stable release\"."]) + end + end + end end end diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb new file mode 100644 index 00000000000..0aeb29cbeec --- /dev/null +++ b/spec/services/quick_actions/target_service_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe QuickActions::TargetService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + shared_examples 'no target' do |type_id:| + it 'returns nil' do + target = service.execute(type, type_id) + + expect(target).to be_nil + end + end + + shared_examples 'find target' do + it 'returns the target' do + found_target = service.execute(type, target_id) + + expect(found_target).to eq(target) + end + end + + shared_examples 'build target' do |type_id:| + it 'builds a new target' do + target = service.execute(type, type_id) + + expect(target.project).to eq(project) + expect(target).to be_new_record + end + end + + context 'for issue' do + let(:target) { create(:issue, project: project) } + let(:target_id) { target.iid } + let(:type) { 'Issue' } + + it_behaves_like 'find target' + it_behaves_like 'build target', type_id: nil + it_behaves_like 'build target', type_id: -1 + end + + context 'for merge request' do + let(:target) { create(:merge_request, source_project: project) } + let(:target_id) { target.iid } + let(:type) { 'MergeRequest' } + + it_behaves_like 'find target' + it_behaves_like 'build target', type_id: nil + it_behaves_like 'build target', type_id: -1 + end + + context 'for commit' do + let(:project) { create(:project, :repository) } + let(:target) { project.commit.parent } + let(:target_id) { target.sha } + let(:type) { 'Commit' } + + it_behaves_like 'find target' + it_behaves_like 'no target', type_id: 'invalid_sha' + + context 'with nil target_id' do + let(:target) { project.commit } + let(:target_id) { nil } + + it_behaves_like 'find target' + end + end + + context 'for unknown type' do + let(:type) { 'unknown' } + + it_behaves_like 'no target', type_id: :unused + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 57d081cffb3..442de61f69b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -108,6 +108,25 @@ describe SystemNoteService do end end + describe '.tag_commit' do + let(:noteable) do + project.commit + end + let(:tag_name) { 'v1.2.3' } + + subject { described_class.tag_commit(noteable, project, author, tag_name) } + + it_behaves_like 'a system note' do + let(:action) { 'tag' } + end + + it 'sets the note text' do + link = "http://localhost/#{project.full_path}/tags/#{tag_name}" + + expect(subject.note).to eq "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})" + end + end + describe '.change_assignee' do subject { described_class.change_assignee(noteable, project, author, assignee) } diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb index eb5da662ab5..476d80f3a93 100644 --- a/spec/support/banzai/reference_filter_shared_examples.rb +++ b/spec/support/banzai/reference_filter_shared_examples.rb @@ -11,3 +11,76 @@ shared_examples 'a reference containing an element node' do expect(doc.children.first.inner_html).to eq(inner_html) end end + +# Requires a reference, subject and subject_name: +# subject { create(:user) } +# let(:reference) { subject.to_reference } +# let(:subject_name) { 'user' } +shared_examples 'user reference or project reference' do + shared_examples 'it contains a data- attribute' do + it 'includes a data- attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute("data-#{subject_name}") + expect(link.attr("data-#{subject_name}")).to eq subject.id.to_s + end + end + + context 'mentioning a resource' do + it_behaves_like 'a reference containing an element node' + it_behaves_like 'it contains a data- attribute' + + it "links to a resource" do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.send("#{subject_name}_url", subject) + end + + it 'links to a resource with a period' do + subject = create(subject_name.to_sym, name: 'alphA.Beta') + + doc = reference_filter("Hey #{get_reference(subject)}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a resource with an underscore' do + subject = create(subject_name.to_sym, name: 'ping_pong_king') + + doc = reference_filter("Hey #{get_reference(subject)}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a resource with different case-sensitivity' do + subject = create(subject_name.to_sym, name: 'RescueRanger') + reference = get_reference(subject) + + doc = reference_filter("Hey #{reference.upcase}") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').text).to eq(reference) + end + end + + it 'supports an :only_path context' do + doc = reference_filter("Hey #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.send "#{subject_name}_path", subject + end + + context 'referencing a resource in a link href' do + let(:reference) { %Q{<a href="#{get_reference(subject)}">Some text</a>} } + + it_behaves_like 'it contains a data- attribute' + + it 'links to the resource' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.send "#{subject_name}_url", subject + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(%r{\(<a.+>Some text</a>\.\)}) + end + end +end diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb index 2b9f8b30c60..89517fde6e2 100644 --- a/spec/support/helpers/features/notes_helpers.rb +++ b/spec/support/helpers/features/notes_helpers.rb @@ -20,6 +20,13 @@ module Spec end end end + + def preview_note(text) + page.within('.js-main-target-form') do + fill_in('note[note]', with: text) + click_on('Preview') + end + end end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index f392660d2c7..21103771d1f 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -67,6 +67,7 @@ module TestEnv TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**') REPOS_STORAGE = 'default'.freeze + BROKEN_STORAGE = 'broken'.freeze # Test environment # @@ -157,10 +158,11 @@ module TestEnv component_timed_setup('Gitaly', install_dir: gitaly_dir, version: Gitlab::GitalyClient.expected_server_version, - task: "gitlab:gitaly:install[#{gitaly_dir}]") do + task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do - # Always re-create config, in case it's outdated. This is fast anyway. - Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true) + # Re-create config, to specify the broken storage path + storage_paths = { 'default' => repos_path, 'broken' => broken_path } + Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, storage_paths, force: true) start_gitaly(gitaly_dir) end @@ -256,6 +258,10 @@ module TestEnv @repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path end + def broken_path + @broken_path ||= Gitlab.config.repositories.storages[BROKEN_STORAGE].legacy_disk_path + end + def backup_path Gitlab.config.backup.path end diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb index bbac6ca6a9c..b4164cff922 100644 --- a/spec/support/import_export/configuration_helper.rb +++ b/spec/support/import_export/configuration_helper.rb @@ -9,7 +9,7 @@ module ConfigurationHelper end def relation_class_for_name(relation_name) - relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name + relation_name = Gitlab::ImportExport::RelationFactory.overrides[relation_name.to_sym] || relation_name Gitlab::ImportExport::RelationFactory.relation_class(relation_name) end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 4545226d78c..e6e4d9504d9 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -8,13 +8,23 @@ describe 'gitlab:gitaly namespace rake task' do describe 'install' do let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s } + let(:storage_path) { Rails.root.join('tmp/tests/repositories').to_s } let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp } + subject { run_rake_task('gitlab:gitaly:install', clone_path, storage_path) } + context 'no dir given' do it 'aborts and display a help message' do # avoid writing task output to spec progress allow($stderr).to receive :write - expect { run_rake_task('gitlab:gitaly:install') }.to raise_error /Please specify the directory where you want to install gitaly/ + expect { run_rake_task('gitlab:gitaly:install') }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/ + end + end + + context 'no storage path given' do + it 'aborts and display a help message' do + allow($stderr).to receive :write + expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/ end end @@ -23,7 +33,7 @@ describe 'gitlab:gitaly namespace rake task' do expect(main_object) .to receive(:checkout_or_clone_version).and_raise 'Git error' - expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' + expect { subject }.to raise_error 'Git error' end end @@ -36,7 +46,7 @@ describe 'gitlab:gitaly namespace rake task' do expect(main_object) .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end end @@ -59,7 +69,7 @@ describe 'gitlab:gitaly namespace rake task' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end end @@ -72,7 +82,7 @@ describe 'gitlab:gitaly namespace rake task' do it 'calls make in the gitaly directory' do expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end context 'when Rails.env is test' do @@ -89,55 +99,10 @@ describe 'gitlab:gitaly namespace rake task' do it 'calls make in the gitaly directory with --no-deployment flag for bundle' do expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end end end end end - - describe 'storage_config' do - it 'prints storage configuration in a TOML format' do - config = { - 'default' => Gitlab::GitalyClient::StorageSettings.new( - 'path' => '/path/to/default', - 'gitaly_address' => 'unix:/path/to/my.socket' - ), - 'nfs_01' => Gitlab::GitalyClient::StorageSettings.new( - 'path' => '/path/to/nfs_01', - 'gitaly_address' => 'unix:/path/to/my.socket' - ) - } - allow(Gitlab.config.repositories).to receive(:storages).and_return(config) - allow(Rails.env).to receive(:test?).and_return(false) - - expected_output = '' - Timecop.freeze do - expected_output = <<~TOML - # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} - # This is in TOML format suitable for use in Gitaly's config.toml file. - bin_dir = "tmp/tests/gitaly" - socket_path = "/path/to/my.socket" - [gitlab-shell] - dir = "#{Gitlab.config.gitlab_shell.path}" - [[storage]] - name = "default" - path = "/path/to/default" - [[storage]] - name = "nfs_01" - path = "/path/to/nfs_01" - TOML - end - - expect { run_rake_task('gitlab:gitaly:storage_config')} - .to output(expected_output).to_stdout - - parsed_output = TomlRB.parse(expected_output) - config.each do |name, params| - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path }) - end - end - end - end end diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb index c57319869f3..0189f926a5f 100644 --- a/spec/views/shared/notes/_form.html.haml_spec.rb +++ b/spec/views/shared/notes/_form.html.haml_spec.rb @@ -16,7 +16,7 @@ describe 'shared/notes/_form' do render end - %w[issue merge_request].each do |noteable| + %w[issue merge_request commit].each do |noteable| context "with a note on #{noteable}" do let(:note) { build(:"note_on_#{noteable}", project: project) } @@ -25,12 +25,4 @@ describe 'shared/notes/_form' do end end end - - context 'with a note on a commit' do - let(:note) { build(:note_on_commit, project: project) } - - it 'says that only markdown is supported, not quick actions' do - expect(rendered).to have_content('Markdown is supported') - end - end end diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index b698248bc38..ffcf5648075 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -641,10 +641,10 @@ rollout 100%: function install_dependencies() { apk add -U openssl curl tar gzip bash ca-certificates git - wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.23-r3/glibc-2.23-r3.apk - apk add glibc-2.23-r3.apk - rm glibc-2.23-r3.apk + wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub + wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk + apk add glibc-2.28-r0.apk + rm glibc-2.28-r0.apk curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx mv linux-amd64/helm /usr/bin/ @@ -720,10 +720,29 @@ rollout 100%: if [[ -f Dockerfile ]]; then echo "Building Dockerfile-based application..." - docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . + docker build \ + --build-arg HTTP_PROXY="$HTTP_PROXY" \ + --build-arg http_proxy="$http_proxy" \ + --build-arg HTTPS_PROXY="$HTTPS_PROXY" \ + --build-arg https_proxy="$https_proxy" \ + --build-arg FTP_PROXY="$FTP_PROXY" \ + --build-arg ftp_proxy="$ftp_proxy" \ + --build-arg NO_PROXY="$NO_PROXY" \ + --build-arg no_proxy="$no_proxy" \ + -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . else echo "Building Heroku-based application using gliderlabs/herokuish docker image..." - docker run -i -e BUILDPACK_URL --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build + docker run -i \ + -e BUILDPACK_URL \ + -e HTTP_PROXY \ + -e http_proxy \ + -e HTTPS_PROXY \ + -e https_proxy \ + -e FTP_PROXY \ + -e ftp_proxy \ + -e NO_PROXY \ + -e no_proxy \ + --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" docker rm "$CI_CONTAINER_NAME" >/dev/null echo "" diff --git a/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml index 9f4cc0574d6..983d7b5250e 100644 --- a/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml @@ -1,24 +1,25 @@ # Full project: https://gitlab.com/pages/middleman -image: ruby:2.3 +image: ruby:2.4 +variables: + LANG: "C.UTF-8" cache: paths: - vendor -test: - script: +before_script: - apt-get update -yqqq - apt-get install -y nodejs - bundle install --path vendor + +test: + script: - bundle exec middleman build except: - master pages: script: - - apt-get update -yqqq - - apt-get install -y nodejs - - bundle install --path vendor - bundle exec middleman build artifacts: paths: |