Commit eb839b9a authored by Filipa Lacerda's avatar Filipa Lacerda

Merge CSS

parents ce867db6 3445136b
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
"globals": { "globals": {
"_": false, "_": false,
"gl": false, "gl": false,
"gon": false "gon": false,
"localStorage": false
}, },
"plugins": [ "plugins": [
"filenames" "filenames"
......
...@@ -30,7 +30,12 @@ stages: ...@@ -30,7 +30,12 @@ stages:
- post-test - post-test
- pages - pages
# Prepare and merge knapsack tests # Predefined scopes
.dedicated-runner: &dedicated-runner
tags:
- gitlab-org
- 2gb
.knapsack-state: &knapsack-state .knapsack-state: &knapsack-state
services: [] services: []
variables: variables:
...@@ -45,47 +50,14 @@ stages: ...@@ -45,47 +50,14 @@ stages:
paths: paths:
- knapsack/ - knapsack/
knapsack:
<<: *knapsack-state
stage: prepare
script:
- mkdir -p knapsack/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
update-knapsack:
<<: *knapsack-state
stage: post-test
script:
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
.use-db: &use-db .use-db: &use-db
services: services:
- mysql:latest - mysql:latest
- redis:alpine - redis:alpine
setup-test-env:
<<: *use-db
stage: prepare
script:
- bundle exec rake assets:precompile 2>/dev/null
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
expire_in: 7d
paths:
- public/assets
- tmp/tests
.rspec-knapsack: &rspec-knapsack .rspec-knapsack: &rspec-knapsack
stage: test stage: test
<<: *dedicated-runner
<<: *use-db <<: *use-db
script: script:
- JOB_NAME=( $CI_BUILD_NAME ) - JOB_NAME=( $CI_BUILD_NAME )
...@@ -103,6 +75,7 @@ setup-test-env: ...@@ -103,6 +75,7 @@ setup-test-env:
.spinach-knapsack: &spinach-knapsack .spinach-knapsack: &spinach-knapsack
stage: test stage: test
<<: *dedicated-runner
<<: *use-db <<: *use-db
script: script:
- JOB_NAME=( $CI_BUILD_NAME ) - JOB_NAME=( $CI_BUILD_NAME )
...@@ -118,6 +91,44 @@ setup-test-env: ...@@ -118,6 +91,44 @@ setup-test-env:
- knapsack/ - knapsack/
- coverage/ - coverage/
# Prepare and merge knapsack tests
knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: prepare
script:
- mkdir -p knapsack/
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
setup-test-env:
<<: *use-db
<<: *dedicated-runner
stage: prepare
script:
- bundle exec rake assets:precompile 2>/dev/null
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
expire_in: 7d
paths:
- public/assets
- tmp/tests
update-knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: post-test
script:
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
rspec 0 20: *rspec-knapsack rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack rspec 2 20: *rspec-knapsack
...@@ -166,10 +177,12 @@ spinach 9 10: *spinach-knapsack ...@@ -166,10 +177,12 @@ spinach 9 10: *spinach-knapsack
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21 .rspec-knapsack-ruby21: &rspec-knapsack-ruby21
<<: *rspec-knapsack <<: *rspec-knapsack
<<: *dedicated-runner
<<: *ruby-21 <<: *ruby-21
.spinach-knapsack-ruby21: &spinach-knapsack-ruby21 .spinach-knapsack-ruby21: &spinach-knapsack-ruby21
<<: *spinach-knapsack <<: *spinach-knapsack
<<: *dedicated-runner
<<: *ruby-21 <<: *ruby-21
rspec 0 20 ruby21: *rspec-knapsack-ruby21 rspec 0 20 ruby21: *rspec-knapsack-ruby21
...@@ -214,6 +227,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 ...@@ -214,6 +227,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.exec: &exec .exec: &exec
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner
stage: test stage: test
script: script:
- bundle exec $CI_BUILD_NAME - bundle exec $CI_BUILD_NAME
...@@ -249,12 +263,14 @@ rake ee_compat_check: ...@@ -249,12 +263,14 @@ rake ee_compat_check:
rake db:migrate:reset: rake db:migrate:reset:
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner
script: script:
- rake db:migrate:reset - rake db:migrate:reset
rake db:seed_fu: rake db:seed_fu:
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner
variables: variables:
SIZE: "1" SIZE: "1"
SETUP_DB: "false" SETUP_DB: "false"
...@@ -276,6 +292,7 @@ teaspoon: ...@@ -276,6 +292,7 @@ teaspoon:
- node_modules/ - node_modules/
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner
script: script:
- npm install - npm install
- npm link istanbul - npm link istanbul
...@@ -288,20 +305,23 @@ teaspoon: ...@@ -288,20 +305,23 @@ teaspoon:
lint-doc: lint-doc:
stage: test stage: test
<<: *dedicated-runner
image: "phusion/baseimage:latest" image: "phusion/baseimage:latest"
before_script: [] before_script: []
script: script:
- scripts/lint-doc.sh - scripts/lint-doc.sh
bundler:check: bundler:check:
stage: test stage: test
<<: *ruby-static-analysis <<: *dedicated-runner
script: <<: *ruby-static-analysis
script:
- bundle check - bundle check
bundler:audit: bundler:audit:
stage: test stage: test
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
...@@ -313,6 +333,7 @@ bundler:audit: ...@@ -313,6 +333,7 @@ bundler:audit:
migration paths: migration paths:
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
only: only:
...@@ -334,6 +355,7 @@ migration paths: ...@@ -334,6 +355,7 @@ migration paths:
coverage: coverage:
stage: post-test stage: post-test
services: [] services: []
<<: *dedicated-runner
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
...@@ -347,6 +369,7 @@ coverage: ...@@ -347,6 +369,7 @@ coverage:
- coverage/assets/ - coverage/assets/
lint:javascript: lint:javascript:
<<: *dedicated-runner
cache: cache:
paths: paths:
- node_modules/ - node_modules/
...@@ -358,6 +381,7 @@ lint:javascript: ...@@ -358,6 +381,7 @@ lint:javascript:
- npm --silent run eslint - npm --silent run eslint
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner
cache: cache:
paths: paths:
- node_modules/ - node_modules/
...@@ -379,6 +403,7 @@ lint:javascript:report: ...@@ -379,6 +403,7 @@ lint:javascript:report:
trigger_docs: trigger_docs:
stage: post-test stage: post-test
image: "alpine" image: "alpine"
<<: *dedicated-runner
before_script: before_script:
- apk update && apk add curl - apk update && apk add curl
variables: variables:
...@@ -394,6 +419,7 @@ trigger_docs: ...@@ -394,6 +419,7 @@ trigger_docs:
notify:slack: notify:slack:
stage: post-test stage: post-test
<<: *dedicated-runner
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
...@@ -409,6 +435,7 @@ notify:slack: ...@@ -409,6 +435,7 @@ notify:slack:
pages: pages:
before_script: [] before_script: []
stage: pages stage: pages
<<: *dedicated-runner
dependencies: dependencies:
- coverage - coverage
- teaspoon - teaspoon
...@@ -423,11 +450,12 @@ pages: ...@@ -423,11 +450,12 @@ pages:
paths: paths:
- public - public
only: only:
- master - master@gitlab-org/gitlab-ce
# Insurance in case a gem needed by one of our releases gets yanked from # Insurance in case a gem needed by one of our releases gets yanked from
# rubygems.org in the future. # rubygems.org in the future.
cache gems: cache gems:
<<: *dedicated-runner
only: only:
- tags - tags
variables: variables:
...@@ -437,3 +465,5 @@ cache gems: ...@@ -437,3 +465,5 @@ cache gems:
artifacts: artifacts:
paths: paths:
- vendor/cache - vendor/cache
only:
- master@gitlab-org/gitlab-ce
...@@ -21,6 +21,8 @@ logs, and code as it's very hard to read otherwise.) ...@@ -21,6 +21,8 @@ logs, and code as it's very hard to read otherwise.)
### Output of checks ### Output of checks
(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com)
#### Results of GitLab application Check #### Results of GitLab application Check
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
......
...@@ -2,6 +2,19 @@ ...@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.14.4 (2016-12-08)
- Fix diff view permalink highlighting. !7090
- Fix pipeline author for Slack and use pipeline id for pipeline link. !7506
- Fix compatibility with Internet Explorer 11 for merge requests. !7525 (Steffen Rauh)
- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
- Fix Cicking on tabs on pipeline page should set URL. !7709
- Authorize users into imported GitLab project.
- Destroy a user's session when they delete their own account.
- Don't accidentally mark unsafe diff lines as HTML safe.
- Replace MR access checks with use of MergeRequestsFinder.
- Remove visible content caching.
## 8.14.3 (2016-12-02) ## 8.14.3 (2016-12-02)
- Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744 - Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744
...@@ -251,6 +264,11 @@ entry. ...@@ -251,6 +264,11 @@ entry.
- Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page - Fix 404 when visit /projects page
## 8.13.9 (2016-12-08)
- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
- Replace MR access checks with use of MergeRequestsFinder.
## 8.13.8 (2016-12-02) ## 8.13.8 (2016-12-02)
- Pass tag SHA to post-receive hook when tag is created via UI. !7700 - Pass tag SHA to post-receive hook when tag is created via UI. !7700
...@@ -495,6 +513,21 @@ entry. ...@@ -495,6 +513,21 @@ entry.
- Fix broken Project API docs (Takuya Noguchi) - Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master) - Migrate invalid project members (owner -> master)
## 8.12.12 (2016-12-08)
- Replace MR access checks with use of MergeRequestsFinder
- Reenables /user API request to return private-token if user is admin and request is made with sudo
## 8.12.11 (2016-12-02)
- No changes
## 8.12.10 (2016-11-28)
- Fix information disclosure in `Projects::BlobController#update`
- Fix missing access checks on issue lookup using IssuableFinder
- Replace issue access checks with use of IssuableFinder
## 8.12.9 (2016-11-07) ## 8.12.9 (2016-11-07)
- Fix XSS issue in Markdown autolinker - Fix XSS issue in Markdown autolinker
......
...@@ -271,7 +271,7 @@ group :development, :test do ...@@ -271,7 +271,7 @@ group :development, :test do
gem 'fuubar', '~> 2.0.0' gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.5.0' gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.6.0' gem 'factory_girl_rails', '~> 4.7.0'
gem 'rspec-rails', '~> 3.5.0' gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5' gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rails', '~> 0.2.1'
......
...@@ -177,10 +177,10 @@ GEM ...@@ -177,10 +177,10 @@ GEM
excon (0.52.0) excon (0.52.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
factory_girl (4.5.0) factory_girl (4.7.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.6.0) factory_girl_rails (4.7.0)
factory_girl (~> 4.5.0) factory_girl (~> 4.7.0)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.9.2) faraday (0.9.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
...@@ -819,7 +819,7 @@ DEPENDENCIES ...@@ -819,7 +819,7 @@ DEPENDENCIES
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8) email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.6.0) factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0) ffaker (~> 2.0.0)
flay (~> 2.6.1) flay (~> 2.6.1)
fog-aws (~> 0.9) fog-aws (~> 0.9)
......
...@@ -76,7 +76,7 @@ GitLab is a Ruby on Rails application that runs on the following software: ...@@ -76,7 +76,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL - Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.3 - Ruby (MRI) 2.3
- Git 2.7.4+ - Git 2.8.4+
- Redis 2.8+ - Redis 2.8+
- MySQL or PostgreSQL - MySQL or PostgreSQL
......
...@@ -70,6 +70,8 @@ ...@@ -70,6 +70,8 @@
// e.g. // e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@) // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess(file, { skipFocus } = {}) { requestFileSuccess(file, { skipFocus } = {}) {
if (!file) return;
const oldValue = this.editor.getValue(); const oldValue = this.editor.getValue();
let newValue = file.content; let newValue = file.content;
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
switch (page) { switch (page) {
case 'sessions:new': case 'sessions:new':
new UsernameValidator(); new UsernameValidator();
new ActiveTabMemoizer();
break; break;
case 'projects:boards:show': case 'projects:boards:show':
case 'projects:boards:index': case 'projects:boards:index':
......
...@@ -74,6 +74,8 @@ ...@@ -74,6 +74,8 @@
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath, newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath, helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
}; };
}, },
...@@ -227,7 +229,9 @@ ...@@ -227,7 +229,9 @@
:model="model" :model="model"
:toggleRow="toggleRow.bind(model)" :toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"></tr> :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0" <tr v-if="model.isOpen && model.children && model.children.length > 0"
is="environment-item" is="environment-item"
...@@ -235,7 +239,9 @@ ...@@ -235,7 +239,9 @@
:model="children" :model="children"
:toggleRow="toggleRow.bind(children)" :toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"> :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:commit-icon-svg="commitIconSvg">
</tr> </tr>
</template> </template>
......
...@@ -12,38 +12,18 @@ ...@@ -12,38 +12,18 @@
required: false, required: false,
default: () => [], default: () => [],
}, },
},
/**
* Appends the svg icon that were render in the index page.
* In order to reuse the svg instead of copy and paste in this template
* we need to render it outside this component using =custom_icon partial.
*
* TODO: Remove this when webpack is merged.
*
*/
mounted() {
const playIcon = document.querySelector('.play-icon-svg.hidden svg');
const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container');
const actionContainers = this.$el.querySelectorAll('.action-play-icon-container');
// Phantomjs does not have support to iterate a nodelist.
const actionsArray = [].slice.call(actionContainers);
if (playIcon && actionsArray && dropdownContainer) {
dropdownContainer.appendChild(playIcon.cloneNode(true));
actionsArray.forEach((element) => { playIconSvg: {
element.appendChild(playIcon.cloneNode(true)); type: String,
}); required: false,
} },
}, },
template: ` template: `
<div class="inline"> <div class="inline">
<div class="dropdown"> <div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="dropdown-play-icon-container"></span> <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
...@@ -53,7 +33,9 @@ ...@@ -53,7 +33,9 @@
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
class="js-manual-action-link"> class="js-manual-action-link">
<span class="action-play-icon-container"></span>
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
<span> <span>
{{action.name}} {{action.name}}
</span> </span>
......
...@@ -7,14 +7,14 @@ ...@@ -7,14 +7,14 @@
window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: { props: {
external_url: { externalUrl: {
type: String, type: String,
default: '', default: '',
}, },
}, },
template: ` template: `
<a class="btn external_url" :href="external_url" target="_blank"> <a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a> </a>
`, `,
......
...@@ -58,6 +58,16 @@ ...@@ -58,6 +58,16 @@
required: false, required: false,
default: false, default: false,
}, },
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
}, },
data() { data() {
...@@ -451,11 +461,12 @@ ...@@ -451,11 +461,12 @@
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
:commit_ref="commitRef" :commit-ref="commitRef"
:commit_url="commitUrl" :commit-url="commitUrl"
:short_sha="commitShortSha" :short-sha="commitShortSha"
:title="commitTitle" :title="commitTitle"
:author="commitAuthor"> :author="commitAuthor"
:commit-icon-svg="commitIconSvg">
</commit-component> </commit-component>
</div> </div>
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
...@@ -476,6 +487,7 @@ ...@@ -476,6 +487,7 @@
<div v-if="hasManualActions && canCreateDeployment" <div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container"> class="inline js-manual-actions-container">
<actions-component <actions-component
:play-icon-svg="playIconSvg"
:actions="manualActions"> :actions="manualActions">
</actions-component> </actions-component>
</div> </div>
...@@ -483,22 +495,22 @@ ...@@ -483,22 +495,22 @@
<div v-if="model.external_url && canReadEnvironment" <div v-if="model.external_url && canReadEnvironment"
class="inline js-external-url-container"> class="inline js-external-url-container">
<external-url-component <external-url-component
:external_url="model.external_url"> :external-url="model.external_url">
</external_url-component> </external-url-component>
</div> </div>
<div v-if="isStoppable && canCreateDeployment" <div v-if="isStoppable && canCreateDeployment"
class="inline js-stop-component-container"> class="inline js-stop-component-container">
<stop-component <stop-component
:stop_url="model.stop_path"> :stop-url="model.stop_path">
</stop-component> </stop-component>
</div> </div>
<div v-if="canRetry && canCreateDeployment" <div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container"> class="inline js-rollback-component-container">
<rollback-component <rollback-component
:is_last_deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry_url="retryUrl"> :retry-url="retryUrl">
</rollback-component> </rollback-component>
</div> </div>
</div> </div>
......
...@@ -7,19 +7,20 @@ ...@@ -7,19 +7,20 @@
window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: { props: {
retry_url: { retryUrl: {
type: String, type: String,
default: '', default: '',
}, },
is_last_deployment: {
isLastDeployment: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, },
template: ` template: `
<a class="btn" :href="retry_url" data-method="post" rel="nofollow"> <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<span v-if="is_last_deployment"> <span v-if="isLastDeployment">
Re-deploy Re-deploy
</span> </span>
<span v-else> <span v-else>
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
window.gl.environmentsList.StopComponent = Vue.component('stop-component', { window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: { props: {
stop_url: { stopUrl: {
type: String, type: String,
default: '', default: '',
}, },
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
template: ` template: `
<a class="btn stop-env-link" <a class="btn stop-env-link"
:href="stop_url" :href="stopUrl"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
rel="nofollow"> rel="nofollow">
......
...@@ -650,6 +650,11 @@ ...@@ -650,6 +650,11 @@
} else if(value) { } else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
} }
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
return;
}
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
if (field && field.length) { if (field && field.length) {
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
setTimeago = true; setTimeago = true;
} }
$timeagoEls.each(function() { $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
var $el = $(this); var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime'))); $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
...@@ -39,6 +39,8 @@ ...@@ -39,6 +39,8 @@
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
}); });
} }
$el.attr('data-timeago-rendered', true);
gl.utils.renderTimeago($el); gl.utils.renderTimeago($el);
}); });
}; };
......
/* eslint-disable */ /* eslint-disable class-methods-use-this */
((w) => { (() => {
w.gl = w.gl || {}; window.gl = window.gl || {};
class Members { class Members {
constructor() { constructor() {
this.addListeners(); this.addListeners();
this.initGLDropdown();
} }
addListeners() { addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit); $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
} }
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
$btn.glDropdown({
selectable: true,
isSelectable(selected, $el) {
return !$el.hasClass('is-active');
},
fieldName: $btn.data('field-name'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel(selected, $el) {
return $el.text();
},
clicked: (selected, $link) => {
this.formSubmit(null, $link);
},
});
});
}
removeRow(e) { removeRow(e) {
const $target = $(e.target); const $target = $(e.target);
if ($target.hasClass('btn-remove')) { if ($target.hasClass('btn-remove')) {
$target.closest('.member') $target.closest('.member')
.fadeOut(function () { .fadeOut(function fadeOutMemberRow() {
$(this).remove(); $(this).remove();
}); });
} }
} }
formSubmit() { formSubmit(e, $el = null) {
$(this).closest('form').trigger("submit.rails").end().disable(); const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
$this.closest('form').trigger('submit.rails');
$toggle.disable();
$dateInput.disable();
} }
formSuccess() { formSuccess(e) {
$(this).find('.js-member-update-control').enable(); const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
$toggle.enable();
$dateInput.enable();
}
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
return {
$memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
} }
} }
gl.Members = Members; gl.Members = Members;
})(window); })();
...@@ -40,19 +40,26 @@ ...@@ -40,19 +40,26 @@
$('#modal_merge_info').modal({ $('#modal_merge_info').modal({
show: false show: false
}); });
this.firstCICheck = true;
this.readyForCICheck = false;
this.readyForCIEnvironmentCheck = false;
this.cancel = false;
clearInterval(this.fetchBuildStatusInterval);
clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners(); this.clearEventListeners();
this.addEventListeners(); this.addEventListeners();
this.getCIStatus(false); this.getCIStatus(false);
this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon(); this.retrieveSuccessIcon();
this.pollCIStatus();
this.pollCIEnvironmentsStatus(); this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
maxInterval: 30000,
hiddenInterval: 120000,
incrementByFactorOf: 5000,
});
this.ciEnvironmentStatusInterval = new global.SmartInterval({
callback: this.getCIEnvironmentsStatus.bind(this),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
notifyPermissions(); notifyPermissions();
} }
...@@ -60,10 +67,6 @@ ...@@ -60,10 +67,6 @@
return $(document).off('page:change.merge_request'); return $(document).off('page:change.merge_request');
}; };
MergeRequestWidget.prototype.cancelPolling = function() {
return this.cancel = true;
};
MergeRequestWidget.prototype.addEventListeners = function() { MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages; var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
...@@ -72,9 +75,6 @@ ...@@ -72,9 +75,6 @@
var page; var page;
page = $('body').data('page').split(':').last(); page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) { if (allowedPages.indexOf(page) < 0) {
clearInterval(_this.fetchBuildStatusInterval);
clearInterval(_this.fetchBuildEnvironmentStatusInterval);
_this.cancelPolling();
return _this.clearEventListeners(); return _this.clearEventListeners();
} }
}; };
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix; return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) { } else if (data.merge_error) {
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else { } else {
callback = function() { callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch); return merge_request_widget.mergeInProgress(deleteSourceBranch);
...@@ -114,6 +114,11 @@ ...@@ -114,6 +114,11 @@
}); });
}; };
MergeRequestWidget.prototype.cancelPolling = function () {
this.ciStatusInterval.cancel();
this.ciEnvironmentStatusInterval.cancel();
};
MergeRequestWidget.prototype.getMergeStatus = function() { MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) { return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data); return $('.mr-state-widget').replaceWith(data);
...@@ -131,18 +136,6 @@ ...@@ -131,18 +136,6 @@
} }
}; };
MergeRequestWidget.prototype.pollCIStatus = function() {
return this.fetchBuildStatusInterval = setInterval(((function(_this) {
return function() {
if (!_this.readyForCICheck) {
return;
}
_this.getCIStatus(true);
return _this.readyForCICheck = false;
};
})(this)), 10000);
};
MergeRequestWidget.prototype.getCIStatus = function(showNotification) { MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this; var _this;
_this = this; _this = this;
...@@ -150,23 +143,17 @@ ...@@ -150,23 +143,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) { return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) { return function(data) {
var message, status, title; var message, status, title;
if (_this.cancel) {
return;
}
_this.readyForCICheck = true;
if (data.status === '') { if (data.status === '') {
return; return;
} }
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status; _this.opts.ci_status = data.status;
_this.showCIStatus(data.status); _this.showCIStatus(data.status);
if (data.coverage) { if (data.coverage) {
_this.showCICoverage(data.coverage); _this.showCICoverage(data.coverage);
} }
// The first check should only update the UI, a notification if (showNotification) {
// should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
title = _this.opts.ci_title.preparing; title = _this.opts.ci_title.preparing;
...@@ -184,24 +171,13 @@ ...@@ -184,24 +171,13 @@
return Turbolinks.visit(_this.opts.builds_path); return Turbolinks.visit(_this.opts.builds_path);
}); });
} }
return _this.firstCICheck = false;
} }
}; };
})(this)); })(this));
}; };
MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
if (!this.readyForCIEnvironmentCheck) return;
this.getCIEnvironmentsStatus();
this.readyForCIEnvironmentCheck = false;
}, 300000);
};
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => { $.getJSON(this.opts.ci_environments_status_url, (environments) => {
if (this.cancel) return;
this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments); if (environments && environments.length) this.renderEnvironments(environments);
}); });
}; };
...@@ -212,11 +188,11 @@ ...@@ -212,11 +188,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return; if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE); const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
if (!environment.stop_url) { if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove(); $('.js-stop-env-link', $template).remove();
} }
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else { } else {
......
/* eslint no-param-reassign: ["error", { "props": false }]*/
/* eslint no-new: "off" */
((global) => {
/**
* Memorize the last selected tab after reloading a page.
* Does that setting the current selected tab in the localStorage
*/
class ActiveTabMemoizer {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
this.bootstrap();
}
bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
tabs[0].addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
}
});
}
this.showTab();
}
showTab() {
const anchorName = this.readData();
if (anchorName) {
const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
if (tab) {
tab.click();
}
}
}
saveData(val) {
localStorage.setItem(this.currentTabKey, val);
}
readData() {
return localStorage.getItem(this.currentTabKey);
}
}
global.ActiveTabMemoizer = ActiveTabMemoizer;
})(window);
...@@ -7,24 +7,31 @@ ...@@ -7,24 +7,31 @@
(() => { (() => {
class SmartInterval { class SmartInterval {
/** /**
* @param { function } callback Function to be called on each iteration (required) * @param { function } opts.callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily * when the page is hidden
* @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } opts.lazyStart Configure if timer is initialized on
* instantiation or lazily
* @param { boolean } opts.immediateExecution Configure if callback should
* be executed before the first interval.
*/ */
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { constructor(opts = {}) {
this.cfg = { this.cfg = {
callback, callback: opts.callback,
startingInterval, startingInterval: opts.startingInterval,
maxInterval, maxInterval: opts.maxInterval,
incrementByFactorOf, hiddenInterval: opts.hiddenInterval,
lazyStart, incrementByFactorOf: opts.incrementByFactorOf,
lazyStart: opts.lazyStart,
immediateExecution: opts.immediateExecution,
}; };
this.state = { this.state = {
intervalId: null, intervalId: null,
currentInterval: startingInterval, currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible', pageVisibility: 'visible',
}; };
...@@ -36,6 +43,11 @@ ...@@ -36,6 +43,11 @@
const cfg = this.cfg; const cfg = this.cfg;
const state = this.state; const state = this.state;
if (cfg.immediateExecution) {
cfg.immediateExecution = false;
cfg.callback();
}
state.intervalId = window.setInterval(() => { state.intervalId = window.setInterval(() => {
cfg.callback(); cfg.callback();
...@@ -54,14 +66,29 @@ ...@@ -54,14 +66,29 @@
this.stopTimer(); this.stopTimer();
} }
onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel();
}
}
// start a timer, using the existing interval // start a timer, using the existing interval
resume() { resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start(); this.start();
} }
onVisibilityVisible() {
this.cancel();
this.start();
}
destroy() { destroy() {
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload'); $(document).off('visibilitychange').off('page:before-unload');
} }
...@@ -80,11 +107,7 @@ ...@@ -80,11 +107,7 @@
initVisibilityChangeHandling() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab no longer shown (prevents cached pages from polling)
$(document) document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
} }
initPageUnloadHandling() { initPageUnloadHandling() {
...@@ -92,10 +115,11 @@ ...@@ -92,10 +115,11 @@
$(document).on('page:before-unload', () => this.cancel()); $(document).on('page:before-unload', () => this.cancel());
} }
handleVisibilityChange() { handleVisibilityChange(e) {
const state = this.state; this.state.pageVisibility = e.target.visibilityState;
const intervalAction = this.isPageVisible() ?
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; this.onVisibilityVisible :
this.onVisibilityHidden;
intervalAction.apply(this); intervalAction.apply(this);
} }
...@@ -111,6 +135,7 @@ ...@@ -111,6 +135,7 @@
incrementInterval() { incrementInterval() {
const cfg = this.cfg; const cfg = this.cfg;
const currentInterval = this.getCurrentInterval(); const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf; let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) { if (nextInterval > cfg.maxInterval) {
...@@ -120,6 +145,8 @@ ...@@ -120,6 +145,8 @@
this.setCurrentInterval(nextInterval); this.setCurrentInterval(nextInterval);
} }
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() { stopTimer() {
const state = this.state; const state = this.state;
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
* name * name
* ref_url * ref_url
*/ */
commit_ref: { commitRef: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
...@@ -32,16 +32,16 @@ ...@@ -32,16 +32,16 @@
/** /**
* Used to link to the commit sha. * Used to link to the commit sha.
*/ */
commit_url: { commitUrl: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
/** /**
* Used to show the commit short_sha that links to the commit url. * Used to show the commit short sha that links to the commit url.
*/ */
short_sha: { shortSha: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
...@@ -68,6 +68,11 @@ ...@@ -68,6 +68,11 @@
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
commitIconSvg: {
type: String,
required: false,
},
}, },
computed: { computed: {
...@@ -80,7 +85,7 @@ ...@@ -80,7 +85,7 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
hasCommitRef() { hasCommitRef() {
return this.commit_ref && this.commit_ref.name && this.commit_ref.ref_url; return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
}, },
/** /**
...@@ -110,24 +115,6 @@ ...@@ -110,24 +115,6 @@
}, },
}, },
/**
* In order to reuse the svg instead of copy and paste in this template
* we need to render it outside this component using =custom_icon partial.
* Make sure it has this structure:
* .commit-icon-svg.hidden
* svg
*
* TODO: Find a better way to include SVG
*/
mounted() {
const commitIconContainer = this.$el.querySelector('.commit-icon-container');
const commitIcon = document.querySelector('.commit-icon-svg.hidden svg');
if (commitIconContainer && commitIcon) {
commitIconContainer.appendChild(commitIcon.cloneNode(true));
}
},
template: ` template: `
<div class="branch-commit"> <div class="branch-commit">
...@@ -138,15 +125,15 @@ ...@@ -138,15 +125,15 @@
<a v-if="hasCommitRef" <a v-if="hasCommitRef"
class="monospace branch-name" class="monospace branch-name"
:href="commit_ref.ref_url"> :href="commitRef.ref_url">
{{commit_ref.name}} {{commitRef.name}}
</a> </a>
<div class="icon-container commit-icon commit-icon-container"></div> <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-id monospace" <a class="commit-id monospace"
:href="commit_url"> :href="commitUrl">
{{short_sha}} {{shortSha}}
</a> </a>
<p class="commit-title"> <p class="commit-title">
...@@ -162,7 +149,7 @@ ...@@ -162,7 +149,7 @@
</a> </a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commit_url"> :href="commitUrl">
{{title}} {{title}}
</a> </a>
</span> </span>
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
@import "framework/animations.scss"; @import "framework/animations.scss";
@import "framework/avatar.scss"; @import "framework/avatar.scss";
@import "framework/asciidoctor.scss";
@import "framework/blocks.scss"; @import "framework/blocks.scss";
@import "framework/buttons.scss"; @import "framework/buttons.scss";
@import "framework/calendar.scss"; @import "framework/calendar.scss";
...@@ -40,3 +41,6 @@ ...@@ -40,3 +41,6 @@
@import "framework/blank"; @import "framework/blank";
@import "framework/wells.scss"; @import "framework/wells.scss";
@import "framework/page-header.scss"; @import "framework/page-header.scss";
@import "framework/awards.scss";
@import "framework/images.scss";
@import "framework/broadcast-messages";
.admonitionblock td.icon {
width: 1%;
[class^="fa icon-"] {
@extend .fa-2x;
}
.icon-note {
@extend .fa-thumb-tack;
}
.icon-tip {
@extend .fa-lightbulb-o;
}
.icon-warning {
@extend .fa-exclamation-triangle;
}
.icon-caution {
@extend .fa-fire;
}
.icon-important {
@extend .fa-exclamation-circle;
}
}
.awards { .awards {
.emoji-icon { .emoji-icon {
width: 19px; width: 20px;
height: 19px; height: 20px;
} }
} }
...@@ -136,5 +136,6 @@ ...@@ -136,5 +136,6 @@
.award-control-icon { .award-control-icon {
color: $award-emoji-new-btn-icon-color; color: $award-emoji-new-btn-icon-color;
margin-top: 1px;
} }
} }
.light-well {
background-color: $background-color;
padding: 15px;
}
.centered-light-block { .centered-light-block {
text-align: center; text-align: center;
color: $gl-gray; color: $gl-gray;
...@@ -274,6 +269,10 @@ ...@@ -274,6 +269,10 @@
} }
} }
.emoji-icon {
display: inline-block;
}
@media(max-width: $screen-xs-max) { @media(max-width: $screen-xs-max) {
margin-top: 50px; margin-top: 50px;
text-align: center; text-align: center;
......
.broadcast-message {
@extend .alert-warning;
padding: 10px;
text-align: center;
div,
p {
display: inline;
margin: 0;
a {
color: inherit;
text-decoration: underline;
}
}
}
.broadcast-message-preview {
@extend .broadcast-message;
margin-bottom: 20px;
}
@mixin btn-default { @mixin btn-default {
border-radius: 3px; border-radius: 3px;
font-size: $gl-font-size; font-size: $gl-font-size;
font-weight: 500; font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding; padding: $gl-vert-padding $gl-btn-padding;
&:focus, &:focus,
......
...@@ -255,6 +255,7 @@ img.emoji { ...@@ -255,6 +255,7 @@ img.emoji {
height: 20px; height: 20px;
vertical-align: top; vertical-align: top;
width: 20px; width: 20px;
margin-top: 1px;
} }
.chart { .chart {
...@@ -379,7 +380,9 @@ table { ...@@ -379,7 +380,9 @@ table {
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
.hide-bottom-border { border-bottom: none !important; } .hide-bottom-border {
border-bottom: none !important;
}
.gl-accessibility { .gl-accessibility {
&:focus { &:focus {
...@@ -396,3 +399,13 @@ table { ...@@ -396,3 +399,13 @@ table {
z-index: 1; z-index: 1;
} }
} }
.str-truncated {
&-60 {
@include str-truncated(60%);
}
&-100 {
@include str-truncated(100%);
}
}
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&[disabled] {
background-color: $input-bg-disabled;
cursor: not-allowed;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -106,13 +106,13 @@ ul.task-list { ...@@ -106,13 +106,13 @@ ul.task-list {
} }
} }
// Generic content list
ul.content-list { ul.content-list {
@include basic-list; @include basic-list;
margin: 0; margin: 0;
padding: 0; padding: 0;
> li { li {
border-color: $table-border-color; border-color: $table-border-color;
font-size: $list-font-size; font-size: $list-font-size;
color: $list-text-color; color: $list-text-color;
...@@ -193,6 +193,41 @@ ul.content-list { ...@@ -193,6 +193,41 @@ ul.content-list {
} }
} }
// Content list using flexbox
.flex-list {
.flex-row {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
white-space: nowrap;
}
.row-main-content {
flex: 1 1 auto;
overflow: hidden;
padding-right: 8px;
}
.row-title {
font-weight: 600;
}
.row-second-line {
display: block;
}
.dropdown {
.btn-block {
margin-bottom: 0;
line-height: inherit;
}
}
.label-default {
color: $btn-transparent-color;
}
}
.panel > .content-list > li { .panel > .content-list > li {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
......
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
border-bottom: 2px solid $link-underline-blue; border-bottom: 2px solid $link-underline-blue;
color: $black; color: $black;
font-weight: 600; font-weight: 600;
.badge {
color: $black;
}
} }
.badge { .badge {
...@@ -268,6 +272,16 @@ ...@@ -268,6 +272,16 @@
width: auto; width: auto;
} }
} }
&.multi-line {
.nav-text {
line-height: 20px;
}
.nav-controls {
padding: 17px 0;
}
}
} }
.layout-nav { .layout-nav {
......
...@@ -34,6 +34,10 @@ table { ...@@ -34,6 +34,10 @@ table {
background-color: $background-color; background-color: $background-color;
font-weight: normal; font-weight: normal;
border-bottom: none; border-bottom: none;
&.wide {
width: 55%;
}
} }
td { td {
...@@ -42,3 +46,16 @@ table { ...@@ -42,3 +46,16 @@ table {
} }
} }
} }
.responsive-table {
@media (max-width: $screen-sm-max) {
th {
width: 100%;
}
td {
width: 100%;
float: left;
}
}
}
...@@ -427,12 +427,6 @@ $common-gray-dark: #444; ...@@ -427,12 +427,6 @@ $common-gray-dark: #444;
$common-red: $gl-text-red; $common-red: $gl-text-red;
$common-green: $gl-text-green; $common-green: $gl-text-green;
/*
* Dashboard
*/
$dashboard-project-access-icon-color: #888;
/* /*
* Editor * Editor
*/ */
......
...@@ -43,3 +43,16 @@ ...@@ -43,3 +43,16 @@
background-color: $well-expand-item; background-color: $well-expand-item;
} }
} }
.light-well {
background-color: $background-color;
padding: 15px;
}
.well-centered {
h1 {
font-weight: normal;
text-align: center;
font-size: 48px;
}
}
/**
* Admin area
*
*/
.admin-dashboard {
.data {
a {
h1 {
line-height: 48px;
font-size: 48px;
padding: 20px;
text-align: center;
font-weight: normal;
}
}
}
.str-truncated {
max-width: 60%;
}
}
.admin-filter form {
.select2-container {
width: 100%;
}
.controls {
margin-left: 130px;
}
.form-actions {
padding-left: 130px;
background: $white-light;
}
.visibility-levels {
.controls {
margin-bottom: 9px;
}
i {
color: inherit;
}
}
}
.broadcast-messages {
.message {
line-height: 2;
}
}
.broadcast-message {
@extend .alert-warning;
padding: 10px;
text-align: center;
> div,
p {
display: inline;
margin: 0;
a {
color: inherit;
text-decoration: underline;
}
}
}
.broadcast-message-preview {
@extend .broadcast-message;
margin-bottom: 20px;
}
// Users List
.users-list {
.user-row {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
white-space: nowrap;
}
.user-details {
flex: 1 1 auto;
overflow: hidden;
padding-right: 8px;
}
.user-name {
display: inline-block;
font-weight: 600;
}
.user-name,
.user-email {
overflow: hidden;
text-overflow: ellipsis;
}
.dropdown {
.btn-block {
margin-bottom: 0;
line-height: inherit;
}
}
.label-default {
color: $btn-transparent-color;
}
}
.abuse-reports {
.table {
table-layout: fixed;
}
.subheading {
padding-bottom: $gl-padding;
}
.message {
word-wrap: break-word;
}
.btn {
white-space: normal;
padding: $gl-btn-padding;
}
th {
width: 15%;
&.wide {
width: 55%;
}
}
@media (max-width: $screen-sm-max) {
th {
width: 100%;
}
td {
width: 100%;
float: left;
}
}
.no-reports {
.emoji-icon {
margin-left: $btn-side-margin;
margin-top: 3px;
}
span {
font-size: 18px;
}
}
}
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
}
}
.deploy-keys-list {
width: 100%;
overflow: auto;
table {
border: 1px solid $table-border-color;
}
}
.deploy-keys-title {
padding-bottom: 2px;
line-height: 2;
}
.well-confirmation {
margin-bottom: 20px;
border-bottom: 1px solid $gray-darker;
> h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 400;
}
.lead {
margin-bottom: 20px;
}
ul,
ol {
padding-left: 0;
}
li {
list-style-type: none;
}
}
.confirmation-content {
a {
color: $md-link-color;
}
}
.dashboard {
.side {
.panel {
.panel-heading {
background: $background-color;
border-top-left-radius: 0;
}
border-top-left-radius: 0;
}
}
}
.dashboard-search-filter {
padding: 5px;
.search-text-input {
float: left;
@extend .col-md-2;
}
.btn {
margin-left: 5px;
float: left;
}
}
.project-access-icon {
margin-left: 10px;
float: left;
margin-right: 15px;
margin-bottom: 15px;
i {
color: $dashboard-project-access-icon-color;
}
}
.dash-project-access-icon {
float: left;
margin-right: 5px;
width: 16px;
}
.deploy-keys-list {
width: 100%;
overflow: auto;
table {
border: 1px solid $table-border-color;
}
}
.deploy-keys-title {
padding-bottom: 2px;
line-height: 2;
}
...@@ -54,6 +54,10 @@ ...@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 50%; width: 50%;
} }
.dropdown-menu-toggle {
width: 100%;
}
} }
.member-access-text { .member-access-text {
......
...@@ -124,7 +124,7 @@ ul.notes { ...@@ -124,7 +124,7 @@ ul.notes {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%); background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
} }
&.hide-shade { &.hide-shade {
...@@ -413,7 +413,6 @@ ul.notes { ...@@ -413,7 +413,6 @@ ul.notes {
.fa { .fa {
color: $notes-action-color; color: $notes-action-color;
position: relative; position: relative;
top: 1px;
font-size: 17px; font-size: 17px;
} }
......
.notification-list-item { .notification-list-item {
line-height: 34px; line-height: 34px;
.dropdown-menu {
@extend .dropdown-menu-align-right;
}
} }
.notification { .notification {
......
...@@ -280,6 +280,12 @@ ...@@ -280,6 +280,12 @@
} }
} }
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
}
}
// Pipeline visualization // Pipeline visualization
.toggle-pipeline-btn { .toggle-pipeline-btn {
......
...@@ -188,6 +188,10 @@ ...@@ -188,6 +188,10 @@
margin-left: 10px; margin-left: 10px;
} }
.notification-dropdown .dropdown-menu {
@extend .dropdown-menu-align-right;
}
.download-button { .download-button {
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
margin-left: 0; margin-left: 0;
......
.tag-buttons {
line-height: 40px;
.btn:not(.dropdown-toggle) {
margin-left: 10px;
}
}
...@@ -56,7 +56,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -56,7 +56,7 @@ class Admin::GroupsController < Admin::ApplicationController
private private
def group def group
@group ||= Group.find_by(path: params[:id]) @group ||= Group.find_by_full_path(params[:id])
end end
def group_params def group_params
......
...@@ -81,10 +81,8 @@ module CreatesCommit ...@@ -81,10 +81,8 @@ module CreatesCommit
def merge_request_exists? def merge_request_exists?
return @merge_request if defined?(@merge_request) return @merge_request if defined?(@merge_request)
@merge_request = @mr_target_project.merge_requests.opened.find_by( @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
source_branch: @mr_source_branch, find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch)
target_branch: @mr_target_branch
)
end end
def different_project? def different_project?
......
...@@ -6,7 +6,12 @@ module MergeRequestsAction ...@@ -6,7 +6,12 @@ module MergeRequestsAction
@label = merge_requests_finder.labels.first @label = merge_requests_finder.labels.first
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
.non_archived
.page(params[:page]) .page(params[:page])
end end
private
def filter_params
super.merge(non_archived: true)
end
end end
...@@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController ...@@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController
def group def group
unless @group unless @group
id = params[:group_id] || params[:id] id = params[:group_id] || params[:id]
@group = Group.find_by(path: id) @group = Group.find_by_full_path(id)
unless @group && can?(current_user, :read_group, @group) unless @group && can?(current_user, :read_group, @group)
@group = nil @group = nil
......
...@@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank? return render_404 if @target_branch.blank?
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
success_path: successful_change_path, failure_path: failed_change_path) success_path: successful_change_path, failure_path: failed_change_path)
end end
...@@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @target_branch.blank? return render_404 if @target_branch.blank?
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.",
success_path: successful_change_path, failure_path: failed_change_path) success_path: successful_change_path, failure_path: failed_change_path)
end end
private private
def successful_change_path def successful_change_path
return referenced_merge_request_url if @commit.merged_merge_request referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
namespace_project_commits_url(@project.namespace, @project, @target_branch)
end end
def failed_change_path def failed_change_path
return referenced_merge_request_url if @commit.merged_merge_request referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id])
namespace_project_commit_url(@project.namespace, @project, params[:id])
end end
def referenced_merge_request_url def referenced_merge_request_url
namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request) if merge_request = @commit.merged_merge_request(current_user)
namespace_project_merge_request_url(@project.namespace, @project, merge_request)
end
end end
def commit def commit
......
...@@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController
@note_counts = project.notes.where(commit_id: @commits.map(&:id)). @note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
@merge_request = @project.merge_requests.opened. @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
respond_to do |format| respond_to do |format|
......
...@@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController
end end
def merge_request def merge_request
@merge_request ||= @project.merge_requests.opened. @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
end end
end end
...@@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController ...@@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :authorize_resolve_discussion! before_action :authorize_resolve_discussion!
def resolve def resolve
discussion.resolve!(current_user) Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
render json: { render json: {
resolved_by: discussion.resolved_by.try(:name), resolved_by: discussion.resolved_by.try(:name),
...@@ -26,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController ...@@ -26,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
private private
def merge_request def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id]) @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end end
def discussion def discussion
......
...@@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new( params[:issue] ||= ActionController::Parameters.new(
assignee_id: "" assignee_id: ""
) )
build_params = issue_params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = @noteable = Issues::BuildService.new(project, current_user, build_params).execute
@issue = @noteable = @project.issues.new(issue_params)
respond_with(@issue) respond_with(@issue)
end end
...@@ -75,7 +76,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -75,7 +76,9 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create def create
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :awardable, :issue alias_method :awardable, :issue
alias_method :spammable, :issue alias_method :spammable, :issue
def merge_request_for_resolving_discussions
return unless merge_request_iid = params[:merge_request_for_resolving_discussions]
@merge_request_for_resolving_discussions ||= MergeRequestsFinder.new(current_user, project_id: project.id).
execute.
find_by(iid: merge_request_iid)
end
def authorize_read_issue! def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
end end
......
...@@ -10,14 +10,37 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -10,14 +10,37 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project.project_members @project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
group = @project.group
if group
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
# FIXME: This whole logic should be moved to a finder!
non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
group_members = group.group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
end
if params[:search].present? if params[:search].present?
users = @project.users.search(params[:search]).to_a user_ids = @project.users.search(params[:search]).select(:id)
@project_members = @project_members.where(user_id: users) @project_members = @project_members.where(user_id: user_ids)
if group_members
user_ids = group.users.search(params[:search]).select(:id)
group_members = group_members.where(user_id: user_ids)
end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end end
@project_members = @project_members.order(access_level: :desc).page(params[:page]) wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
order(access_level: :desc).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @requesters = AccessRequestsFinder.new(@project).execute(current_user)
......
...@@ -18,7 +18,7 @@ class Projects::TodosController < Projects::ApplicationController ...@@ -18,7 +18,7 @@ class Projects::TodosController < Projects::ApplicationController
when "issue" when "issue"
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
when "merge_request" when "merge_request"
@project.merge_requests.find(params[:issuable_id]) MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
end end
end end
end end
......
...@@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController
DeleteUserService.new(current_user).execute(current_user) DeleteUserService.new(current_user).execute(current_user)
respond_to do |format| respond_to do |format|
format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } format.html do
session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed."
end
end end
end end
......
...@@ -31,10 +31,18 @@ class SessionsController < Devise::SessionsController ...@@ -31,10 +31,18 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil, resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil) reset_password_sent_at: nil)
end end
# hide the signed-in notification
flash[:notice] = nil
log_audit_event(current_user, with: authentication_method) log_audit_event(current_user, with: authentication_method)
end end
end end
def destroy
super
# hide the signed_out notice
flash[:notice] = nil
end
private private
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
# search: string # search: string
# label_name: string # label_name: string
# sort: string # sort: string
# non_archived: boolean
# #
class IssuableFinder class IssuableFinder
NONE = '0' NONE = '0'
...@@ -38,6 +39,7 @@ class IssuableFinder ...@@ -38,6 +39,7 @@ class IssuableFinder
items = by_author(items) items = by_author(items)
items = by_label(items) items = by_label(items)
items = by_due_date(items) items = by_due_date(items)
items = by_non_archived(items)
sort(items) sort(items)
end end
...@@ -75,6 +77,10 @@ class IssuableFinder ...@@ -75,6 +77,10 @@ class IssuableFinder
counts counts
end end
def find_by!(*params)
execute.find_by!(*params)
end
def group def group
return @group if defined?(@group) return @group if defined?(@group)
...@@ -356,6 +362,10 @@ class IssuableFinder ...@@ -356,6 +362,10 @@ class IssuableFinder
end end
end end
def by_non_archived(items)
params[:non_archived].present? ? items.non_archived : items
end
def current_user_related? def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end end
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# search: string # search: string
# label_name: string # label_name: string
# sort: string # sort: string
# non_archived: boolean
# #
class MergeRequestsFinder < IssuableFinder class MergeRequestsFinder < IssuableFinder
def klass def klass
......
...@@ -14,7 +14,7 @@ class NotesFinder ...@@ -14,7 +14,7 @@ class NotesFinder
when "issue" when "issue"
IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes project.snippets.find(target_id).notes
else else
......
class SnippetsFinder class SnippetsFinder
def execute(current_user, params = {}) def execute(current_user, params = {})
filter = params[:filter] filter = params[:filter]
user = params.fetch(:user, current_user)
case filter case filter
when :all then when :all then
snippets(current_user).fresh snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, user, params[:scope])
when :by_project when :by_project
by_project(current_user, params[:project]) by_project(current_user, params[:project])
end end
......
...@@ -5,8 +5,9 @@ module CiStatusHelper ...@@ -5,8 +5,9 @@ module CiStatusHelper
end end
def ci_status_with_icon(status, target = nil) def ci_status_with_icon(status, target = nil)
content = ci_icon_for_status(status) + ci_label_for_status(status) content = ci_icon_for_status(status) + ci_text_for_status(status)
klass = "ci-status ci-#{status}" klass = "ci-status ci-#{status}"
if target if target
link_to content, target, class: klass link_to content, target, class: klass
else else
...@@ -14,7 +15,19 @@ module CiStatusHelper ...@@ -14,7 +15,19 @@ module CiStatusHelper
end end
end end
def ci_text_for_status(status)
if detailed_status?(status)
status.text
else
status
end
end
def ci_label_for_status(status) def ci_label_for_status(status)
if detailed_status?(status)
return status.label
end
case status case status
when 'success' when 'success'
'passed' 'passed'
...@@ -31,6 +44,10 @@ module CiStatusHelper ...@@ -31,6 +44,10 @@ module CiStatusHelper
end end
def ci_icon_for_status(status) def ci_icon_for_status(status)
if detailed_status?(status)
return custom_icon(status.icon)
end
icon_name = icon_name =
case status case status
when 'success' when 'success'
...@@ -94,4 +111,10 @@ module CiStatusHelper ...@@ -94,4 +111,10 @@ module CiStatusHelper
class: klass, title: title, data: data class: klass, title: title, data: data
end end
end end
def detailed_status?(status)
status.respond_to?(:text) &&
status.respond_to?(:label) &&
status.respond_to?(:icon)
end
end end
...@@ -130,7 +130,7 @@ module CommitsHelper ...@@ -130,7 +130,7 @@ module CommitsHelper
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user return unless current_user
tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
if can_collaborate_with_project? if can_collaborate_with_project?
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
...@@ -154,7 +154,7 @@ module CommitsHelper ...@@ -154,7 +154,7 @@ module CommitsHelper
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user return unless current_user
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
if can_collaborate_with_project? if can_collaborate_with_project?
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
......
...@@ -55,7 +55,9 @@ module DiffHelper ...@@ -55,7 +55,9 @@ module DiffHelper
if line.blank? if line.blank?
"&nbsp;".html_safe "&nbsp;".html_safe
else else
line.sub(/^[\-+ ]/, '').html_safe # We can't use `sub` because the HTML-safeness of `line` will not survive.
line[0] = '' if line.start_with?('+', '-', ' ')
line
end end
end end
......
...@@ -45,6 +45,12 @@ module EventsHelper ...@@ -45,6 +45,12 @@ module EventsHelper
@project.feature_available?(feature_key, current_user) @project.feature_available?(feature_key, current_user)
end end
def comments_visible?
event_filter_visible(:repository) ||
event_filter_visible(:merge_requests) ||
event_filter_visible(:issues)
end
def event_preposition(event) def event_preposition(event)
if event.push? || event.commented? || event.target if event.push? || event.commented? || event.target
"at" "at"
......
...@@ -159,6 +159,11 @@ module GitlabRoutingHelper ...@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end end
# Snippets
def personal_snippet_url(snippet, *args)
snippet_url(snippet)
end
# Groups # Groups
## Members ## Members
......
...@@ -5,7 +5,7 @@ module GroupsHelper ...@@ -5,7 +5,7 @@ module GroupsHelper
def group_icon(group) def group_icon(group)
if group.is_a?(String) if group.is_a?(String)
group = Group.find_by(path: group) group = Group.find_by_full_path(group)
end end
group.try(:avatar_url) || image_path('no_group_avatar.png') group.try(:avatar_url) || image_path('no_group_avatar.png')
......
...@@ -21,8 +21,6 @@ module Ci ...@@ -21,8 +21,6 @@ module Ci
after_create :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
...@@ -98,17 +96,35 @@ module Ci ...@@ -98,17 +96,35 @@ module Ci
sha[0...8] sha[0...8]
end end
def self.stages
# We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(pipeline: pluck(:id)).stages
end
def self.total_duration def self.total_duration
where.not(duration: nil).sum(:duration) where.not(duration: nil).sum(:duration)
end end
def stages_with_latest_statuses def stages_count
statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) statuses.select(:stage).distinct.count
end
def stages_name
statuses.order(:stage_idx).distinct.
pluck(:stage, :stage_idx).map(&:first)
end
def stages
status_sql = statuses.latest.where('stage=sg.stage').status_sql
stages_query = statuses.group('stage').select(:stage)
.order('max(stage_idx)')
stages_with_statuses = CommitStatus.from(stages_query, :sg).
pluck('sg.stage', status_sql)
stages_with_statuses.map do |stage|
Ci::Stage.new(self, name: stage.first, status: stage.last)
end
end
def artifacts
builds.latest.with_artifacts_not_expired
end end
def project_id def project_id
...@@ -320,6 +336,10 @@ module Ci ...@@ -320,6 +336,10 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end end
def detailed_status
Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate!
end
private private
def pipeline_data def pipeline_data
......
module Ci
# Currently this is artificial object, constructed dynamically
# We should migrate this object to actual database record in the future
class Stage
include StaticModel
attr_reader :pipeline, :name
delegate :project, to: :pipeline
def initialize(pipeline, name:, status: nil)
@pipeline = pipeline
@name = name
@status = status
end
def to_param
name
end
def status
@status ||= statuses.latest.status
end
def detailed_status
Gitlab::Ci::Status::Stage::Factory.new(self).fabricate!
end
def statuses
@statuses ||= pipeline.statuses.where(stage: name)
end
def builds
@builds ||= pipeline.builds.where(stage: name)
end
end
end
...@@ -4,10 +4,10 @@ module Ci ...@@ -4,10 +4,10 @@ module Ci
belongs_to :project, foreign_key: :gl_project_id belongs_to :project, foreign_key: :gl_project_id
validates_uniqueness_of :key, scope: :gl_project_id
validates :key, validates :key,
presence: true, presence: true,
length: { within: 0..255 }, uniqueness: { scope: :gl_project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/, format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." } message: "can contain only letters, digits and '_'." }
......
...@@ -245,44 +245,47 @@ class Commit ...@@ -245,44 +245,47 @@ class Commit
project.repository.next_branch("cherry-pick-#{short_id}", mild: true) project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end end
def revert_description def revert_description(user)
if merged_merge_request if merged_merge_request?(user)
"This reverts merge request #{merged_merge_request.to_reference}" "This reverts merge request #{merged_merge_request(user).to_reference}"
else else
"This reverts commit #{sha}" "This reverts commit #{sha}"
end end
end end
def revert_message def revert_message(user)
%Q{Revert "#{title.strip}"\n\n#{revert_description}} %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
end end
def reverts_commit?(commit) def reverts_commit?(commit, user)
description? && description.include?(commit.revert_description) description? && description.include?(commit.revert_description(user))
end end
def merge_commit? def merge_commit?
parents.size > 1 parents.size > 1
end end
def merged_merge_request def merged_merge_request(current_user)
return @merged_merge_request if defined?(@merged_merge_request) # Memoize with per-user access check
@merged_merge_request_hash ||= Hash.new do |hash, user|
@merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit? hash[user] = merged_merge_request_no_cache(user)
end
@merged_merge_request_hash[current_user]
end end
def has_been_reverted?(current_user = nil, noteable = self) def has_been_reverted?(current_user, noteable = self)
ext = all_references(current_user) ext = all_references(current_user)
noteable.notes_with_associations.system.each do |note| noteable.notes_with_associations.system.each do |note|
note.all_references(current_user, extractor: ext) note.all_references(current_user, extractor: ext)
end end
ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
end end
def change_type_title def change_type_title(user)
merged_merge_request ? 'merge request' : 'commit' merged_merge_request?(user) ? 'merge request' : 'commit'
end end
# Get the URI type of the given path # Get the URI type of the given path
...@@ -350,4 +353,12 @@ class Commit ...@@ -350,4 +353,12 @@ class Commit
changes changes
end end
def merged_merge_request?(user)
!!merged_merge_request(user)
end
def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end
end end
...@@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base ...@@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base
end end
scope :exclude_ignored, -> do scope :exclude_ignored, -> do
quoted_when = connection.quote_column_name('when')
# We want to ignore failed_but_allowed jobs # We want to ignore failed_but_allowed jobs
where("allow_failure = ? OR status IN (?)", where("allow_failure = ? OR status IN (?)",
false, all_state_names - [:failed, :canceled]). false, all_state_names - [:failed, :canceled])
# We want to ignore skipped manual jobs
where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
# We want to ignore skipped on_failure
where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
end end
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
state_machine :status do state_machine :status do
event :enqueue do event :enqueue do
...@@ -117,20 +112,6 @@ class CommitStatus < ActiveRecord::Base ...@@ -117,20 +112,6 @@ class CommitStatus < ActiveRecord::Base
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
end end
def self.stages
# We group by stage name, but order stages by theirs' index
unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end
def self.stages_status
# We execute subquery for each stage to calculate a stage status
statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
statuses.inject({}) do |h, k|
h[k.first] = k.last
h
end
end
def failed_but_allowed? def failed_but_allowed?
allow_failure? && (failed? || canceled?) allow_failure? && (failed? || canceled?)
end end
......
...@@ -4,7 +4,7 @@ module HasStatus ...@@ -4,7 +4,7 @@ module HasStatus
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped] STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running] ACTIVE_STATUSES = %w[pending running]
COMPLETED_STATUSES = %w[success failed canceled] COMPLETED_STATUSES = %w[success failed canceled skipped]
ORDERED_STATUSES = %w[failed pending running canceled success skipped] ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do class_methods do
...@@ -23,9 +23,10 @@ module HasStatus ...@@ -23,9 +23,10 @@ module HasStatus
canceled = scope.canceled.select('count(*)').to_sql canceled = scope.canceled.select('count(*)').to_sql
"(CASE "(CASE
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created' WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
......
...@@ -41,7 +41,7 @@ module Issuable ...@@ -41,7 +41,7 @@ module Issuable
has_one :metrics has_one :metrics
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { maximum: 255 }
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :assigned_to, ->(u) { where(assignee_id: u.id)}
......
module Milestoneish module Milestoneish
def closed_items_count(user = nil) def closed_items_count(user)
issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end end
def total_items_count(user = nil) def total_items_count(user)
issues_visible_to_user(user).size + merge_requests.size issues_visible_to_user(user).size + merge_requests.size
end end
def complete?(user = nil) def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
end end
def percent_complete(user = nil) def percent_complete(user)
((closed_items_count(user) * 100) / total_items_count(user)).abs ((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError rescue ZeroDivisionError
0 0
...@@ -29,7 +29,7 @@ module Milestoneish ...@@ -29,7 +29,7 @@ module Milestoneish
(Date.today - start_date).to_i (Date.today - start_date).to_i
end end
def issues_visible_to_user(user = nil) def issues_visible_to_user(user)
issues.visible_to_user(user) issues.visible_to_user(user)
end end
......
# Store object full path in separate table for easy lookup and uniq validation
# Object must have path db field and respond to full_path and full_path_changed? methods.
module Routable
extend ActiveSupport::Concern
included do
has_one :route, as: :source, autosave: true, dependent: :destroy
validates_associated :route
before_validation :update_route_path, if: :full_path_changed?
end
class_methods do
# Finds a single object by full path match in routes table.
#
# Usage:
#
# Klass.find_by_full_path('gitlab-org/gitlab-ce')
#
# Returns a single object, or nil.
def find_by_full_path(path)
# On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
# any literal matches come first, for this we have to use "BINARY".
# Without this there's still no guarantee in what order MySQL will return
# rows.
binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple objects by their full paths.
#
# Usage:
#
# Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
#
# Returns an ActiveRecord::Relation.
def where_paths_in(paths)
wheres = []
cast_lower = Gitlab::Database.postgresql?
paths.each do |path|
path = connection.quote(path)
where = "(routes.path = #{path})"
if cast_lower
where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
end
wheres << where
end
if wheres.empty?
none
else
joins(:route).where(wheres.join(' OR '))
end
end
end
private
def update_route_path
route || build_route(source: self)
route.path = full_path
end
end
...@@ -88,6 +88,10 @@ class Discussion ...@@ -88,6 +88,10 @@ class Discussion
@first_note ||= @notes.first @first_note ||= @notes.first
end end
def first_note_to_resolve
@first_note_to_resolve ||= notes.detect(&:to_be_resolved?)
end
def last_note def last_note
@last_note ||= @notes.last @last_note ||= @notes.last
end end
......
...@@ -9,7 +9,7 @@ class Environment < ActiveRecord::Base ...@@ -9,7 +9,7 @@ class Environment < ActiveRecord::Base
validates :name, validates :name,
presence: true, presence: true,
uniqueness: { scope: :project_id }, uniqueness: { scope: :project_id },
length: { within: 0..255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.environment_name_regex, format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message } message: Gitlab::Regex.environment_name_regex_message }
......
...@@ -8,10 +8,18 @@ class Key < ActiveRecord::Base ...@@ -8,10 +8,18 @@ class Key < ActiveRecord::Base
before_validation :generate_fingerprint before_validation :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 } validates :title,
validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } presence: true,
validates :key, format: { without: /\n|\r/, message: 'should be a single line' } length: { maximum: 255 }
validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } validates :key,
presence: true,
length: { maximum: 5000 },
format: { with: /\A(ssh|ecdsa)-.*\Z/ }
validates :key,
format: { without: /\n|\r/, message: 'should be a single line' }
validates :fingerprint,
uniqueness: true,
presence: { message: 'cannot be generated' }
delegate :name, :email, to: :user, prefix: true delegate :name, :email, to: :user, prefix: true
......
...@@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork? validate :validate_fork, unless: :closed_without_fork?
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
end
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) }
...@@ -476,6 +478,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -476,6 +478,14 @@ class MergeRequest < ActiveRecord::Base
@diff_discussions ||= self.notes.diff_notes.discussions @diff_discussions ||= self.notes.diff_notes.discussions
end end
def resolvable_discussions
@resolvable_discussions ||= diff_discussions.select(&:to_be_resolved?)
end
def discussions_can_be_resolved_by?(user)
resolvable_discussions.all? { |discussion| discussion.can_resolve?(user) }
end
def find_diff_discussion(discussion_id) def find_diff_discussion(discussion_id)
notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
return if notes.empty? return if notes.empty?
...@@ -797,7 +807,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -797,7 +807,7 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end end
def can_be_reverted?(current_user = nil) def can_be_reverted?(current_user)
merge_commit && !merge_commit.has_been_reverted?(current_user, self) merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end end
......
...@@ -4,25 +4,29 @@ class Namespace < ActiveRecord::Base ...@@ -4,25 +4,29 @@ class Namespace < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include Sortable include Sortable
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include Routable
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
has_many :children, class_name: "Namespace", foreign_key: :parent_id
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
length: { within: 0..255 },
namespace_name: true,
presence: true, presence: true,
uniqueness: true uniqueness: true,
length: { maximum: 255 },
namespace_name: true
validates :description, length: { within: 0..255 } validates :description, length: { maximum: 255 }
validates :path, validates :path,
length: { within: 1..255 },
namespace: true,
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false },
length: { maximum: 255 },
namespace: true
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
...@@ -86,7 +90,7 @@ class Namespace < ActiveRecord::Base ...@@ -86,7 +90,7 @@ class Namespace < ActiveRecord::Base
end end
def to_param def to_param
path full_path
end end
def human_name def human_name
...@@ -150,6 +154,14 @@ class Namespace < ActiveRecord::Base ...@@ -150,6 +154,14 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled Gitlab.config.lfs.enabled
end end
def full_path
if parent
parent.full_path + '/' + path
else
path
end
end
private private
def repository_storage_paths def repository_storage_paths
...@@ -185,4 +197,8 @@ class Namespace < ActiveRecord::Base ...@@ -185,4 +197,8 @@ class Namespace < ActiveRecord::Base
where(projects: { namespace_id: id }). where(projects: { namespace_id: id }).
find_each(&:refresh_members_authorized_projects) find_each(&:refresh_members_authorized_projects)
end end
def full_path_changed?
path_changed? || parent_id_changed?
end
end end
...@@ -99,7 +99,7 @@ class Note < ActiveRecord::Base ...@@ -99,7 +99,7 @@ class Note < ActiveRecord::Base
end end
def discussions def discussions
Discussion.for_notes(all) Discussion.for_notes(fresh)
end end
def grouped_diff_discussions def grouped_diff_discussions
......
...@@ -14,6 +14,7 @@ class Project < ActiveRecord::Base ...@@ -14,6 +14,7 @@ class Project < ActiveRecord::Base
include TokenAuthenticatable include TokenAuthenticatable
include ProjectFeaturesCompatibility include ProjectFeaturesCompatibility
include SelectForProjectAuthorization include SelectForProjectAuthorization
include Routable
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
...@@ -172,13 +173,13 @@ class Project < ActiveRecord::Base ...@@ -172,13 +173,13 @@ class Project < ActiveRecord::Base
validates :description, length: { maximum: 2000 }, allow_blank: true validates :description, length: { maximum: 2000 }, allow_blank: true
validates :name, validates :name,
presence: true, presence: true,
length: { within: 0..255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.project_name_regex, format: { with: Gitlab::Regex.project_name_regex,
message: Gitlab::Regex.project_name_regex_message } message: Gitlab::Regex.project_name_regex_message }
validates :path, validates :path,
presence: true, presence: true,
project_path: true, project_path: true,
length: { within: 0..255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex, format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message } message: Gitlab::Regex.project_path_regex_message }
validates :namespace, presence: true validates :namespace, presence: true
...@@ -324,87 +325,6 @@ class Project < ActiveRecord::Base ...@@ -324,87 +325,6 @@ class Project < ActiveRecord::Base
non_archived.where(table[:name].matches(pattern)) non_archived.where(table[:name].matches(pattern))
end end
# Finds a single project for the given path.
#
# path - The full project path (including namespace path).
#
# Returns a Project, or nil if no project could be found.
def find_with_namespace(path)
namespace_path, project_path = path.split('/', 2)
return unless namespace_path && project_path
namespace_path = connection.quote(namespace_path)
project_path = connection.quote(project_path)
# On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
# any literal matches come first, for this we have to use "BINARY".
# Without this there's still no guarantee in what order MySQL will return
# rows.
binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
"AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple projects by their full paths.
#
# Each path must be in the following format:
#
# namespace_path/project_path
#
# For example:
#
# gitlab-org/gitlab-ce
#
# Usage:
#
# Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
#
# This would return the projects with the full paths matching the values
# given.
#
# paths - An Array of full paths (namespace path + project path) for which
# to find the projects.
#
# Returns an ActiveRecord::Relation.
def where_paths_in(paths)
wheres = []
cast_lower = Gitlab::Database.postgresql?
paths.each do |path|
namespace_path, project_path = path.split('/', 2)
next unless namespace_path && project_path
namespace_path = connection.quote(namespace_path)
project_path = connection.quote(project_path)
where = "(namespaces.path = #{namespace_path}
AND projects.path = #{project_path})"
if cast_lower
where = "(
#{where}
OR (
LOWER(namespaces.path) = LOWER(#{namespace_path})
AND LOWER(projects.path) = LOWER(#{project_path})
)
)"
end
wheres << where
end
if wheres.empty?
none
else
joins(:namespace).where(wheres.join(' OR '))
end
end
def visibility_levels def visibility_levels
Gitlab::VisibilityLevel.options Gitlab::VisibilityLevel.options
end end
...@@ -440,6 +360,10 @@ class Project < ActiveRecord::Base ...@@ -440,6 +360,10 @@ class Project < ActiveRecord::Base
def group_ids def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end end
# Add alias for Routable method for compatibility with old code.
# In future all calls `find_with_namespace` should be replaced with `find_by_full_path`
alias_method :find_with_namespace, :find_by_full_path
end end
def lfs_enabled? def lfs_enabled?
...@@ -879,13 +803,14 @@ class Project < ActiveRecord::Base ...@@ -879,13 +803,14 @@ class Project < ActiveRecord::Base
end end
alias_method :human_name, :name_with_namespace alias_method :human_name, :name_with_namespace
def path_with_namespace def full_path
if namespace if namespace && path
namespace.path + '/' + path namespace.full_path + '/' + path
else else
path path
end end
end end
alias_method :path_with_namespace, :full_path
def execute_hooks(data, hooks_scope = :push_hooks) def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook| hooks.send(hooks_scope).each do |hook|
...@@ -1373,4 +1298,8 @@ class Project < ActiveRecord::Base ...@@ -1373,4 +1298,8 @@ class Project < ActiveRecord::Base
def validate_board_limit(board) def validate_board_limit(board)
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end end
def full_path_changed?
path_changed? || namespace_id_changed?
end
end end
...@@ -85,11 +85,7 @@ class Repository ...@@ -85,11 +85,7 @@ class Repository
# This method return true if repository contains some content visible in project page. # This method return true if repository contains some content visible in project page.
# #
def has_visible_content? def has_visible_content?
return @has_visible_content unless @has_visible_content.nil? branch_count > 0
@has_visible_content = cache.fetch(:has_visible_content?) do
branch_count > 0
end
end end
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
...@@ -374,12 +370,6 @@ class Repository ...@@ -374,12 +370,6 @@ class Repository
return unless empty? return unless empty?
expire_method_caches(%i(empty?)) expire_method_caches(%i(empty?))
expire_has_visible_content_cache
end
def expire_has_visible_content_cache
cache.expire(:has_visible_content?)
@has_visible_content = nil
end end
def lookup_cache def lookup_cache
...@@ -467,7 +457,6 @@ class Repository ...@@ -467,7 +457,6 @@ class Repository
# Runs code after a new branch has been created. # Runs code after a new branch has been created.
def after_create_branch def after_create_branch
expire_branches_cache expire_branches_cache
expire_has_visible_content_cache
repository_event(:push_branch) repository_event(:push_branch)
end end
...@@ -481,7 +470,6 @@ class Repository ...@@ -481,7 +470,6 @@ class Repository
# Runs code after an existing branch has been removed. # Runs code after an existing branch has been removed.
def after_remove_branch def after_remove_branch
expire_has_visible_content_cache
expire_branches_cache expire_branches_cache
end end
...@@ -962,7 +950,7 @@ class Repository ...@@ -962,7 +950,7 @@ class Repository
update_branch_with_hooks(user, base_branch) do update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user) committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged, source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message, message: commit.revert_message(user),
author: committer, author: committer,
committer: committer, committer: committer,
tree: revert_tree_id, tree: revert_tree_id,
......
class Route < ActiveRecord::Base
belongs_to :source, polymorphic: true
validates :source, presence: true
validates :path,
length: { within: 1..255 },
presence: true,
uniqueness: { case_sensitive: false }
after_update :rename_children, if: :path_changed?
def rename_children
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
Route.where('path LIKE ?', "#{path_was}%").each do |route|
# Note that update column skips validation and callbacks.
# We need this to avoid recursive call of rename_children method
route.update_column(:path, route.path.sub(path_was, path))
end
end
end
...@@ -27,9 +27,9 @@ class Snippet < ActiveRecord::Base ...@@ -27,9 +27,9 @@ class Snippet < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { maximum: 255 }
validates :file_name, validates :file_name,
length: { within: 0..255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.file_name_regex, format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message } message: Gitlab::Regex.file_name_regex_message }
...@@ -94,6 +94,10 @@ class Snippet < ActiveRecord::Base ...@@ -94,6 +94,10 @@ class Snippet < ActiveRecord::Base
0 0
end end
def file_name
super.to_s
end
# alias for compatibility with blobs and highlighting # alias for compatibility with blobs and highlighting
def path def path
file_name file_name
......
module Ci module Ci
class BuildPolicy < CommitStatusPolicy class BuildPolicy < CommitStatusPolicy
def rules def rules
can! :read_build if @subject.project.public_builds?
super super
# If we can't read build we should also not have that # If we can't read build we should also not have that
......
...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
end end
unless @user.external?
can! :create_personal_snippet
end
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
end end
......
...@@ -12,9 +12,6 @@ class ProjectPolicy < BasePolicy ...@@ -12,9 +12,6 @@ class ProjectPolicy < BasePolicy
guest_access! guest_access!
public_access! public_access!
# Allow to read builds for internal projects
can! :read_build if project.public_builds?
if project.request_access_enabled && if project.request_access_enabled &&
!(owner || user.admin? || project.team.member?(user) || project_group_member?(user)) !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access can! :request_access
...@@ -46,6 +43,11 @@ class ProjectPolicy < BasePolicy ...@@ -46,6 +43,11 @@ class ProjectPolicy < BasePolicy
can! :create_note can! :create_note
can! :upload_file can! :upload_file
can! :read_cycle_analytics can! :read_cycle_analytics
if project.public_builds?
can! :read_pipeline
can! :read_build
end
end end
def reporter_access! def reporter_access!
......
...@@ -44,11 +44,11 @@ module Ci ...@@ -44,11 +44,11 @@ module Ci
def valid_statuses_for_when(value) def valid_statuses_for_when(value)
case value case value
when 'on_success' when 'on_success'
%w[success] %w[success skipped]
when 'on_failure' when 'on_failure'
%w[failed] %w[failed]
when 'always' when 'always'
%w[success failed] %w[success failed skipped]
else else
[] []
end end
......
...@@ -34,7 +34,7 @@ module Commits ...@@ -34,7 +34,7 @@ module Commits
repository.public_send(action, current_user, @commit, into, tree_id) repository.public_send(action, current_user, @commit, into, tree_id)
success success
else else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg raise ChangeError, error_msg
end end
......
...@@ -20,6 +20,10 @@ class DestroyGroupService ...@@ -20,6 +20,10 @@ class DestroyGroupService
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end end
group.children.each do |group|
DestroyGroupService.new(group, current_user).async_execute
end
group.really_destroy! group.really_destroy!
end end
end end
module Discussions
class BaseService < ::BaseService
end
end
module Discussions
class ResolveService < Discussions::BaseService
def execute(one_or_more_discussions)
Array(one_or_more_discussions).each { |discussion| resolve_discussion(discussion) }
end
def resolve_discussion(discussion)
return unless discussion.can_resolve?(current_user)
discussion.resolve!(current_user)
MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue
end
def merge_request
params[:merge_request]
end
def follow_up_issue
params[:follow_up_issue]
end
end
end
...@@ -120,9 +120,10 @@ class IssuableBaseService < BaseService ...@@ -120,9 +120,10 @@ class IssuableBaseService < BaseService
def merge_slash_commands_into_params!(issuable) def merge_slash_commands_into_params!(issuable)
description, command_params = description, command_params =
SlashCommands::InterpretService.new(project, current_user). SlashCommands::InterpretService.new(project, current_user).
execute(params[:description], issuable) execute(params[:description], issuable)
params[:description] = description # Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if params.has_key?(:description)
params.merge!(command_params) params.merge!(command_params)
end end
......
module Issues module Issues
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
attr_reader :merge_request_for_resolving_discussions
def initialize(*args)
super
@merge_request_for_resolving_discussions ||= params.delete(:merge_request_for_resolving_discussions)
end
def hook_data(issue, action) def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user) issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.build(issue) issue_url = Gitlab::UrlBuilder.build(issue)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment