Merge CSS
This commit is contained in:
commit
eb839b9af5
381 changed files with 5737 additions and 1854 deletions
|
@ -8,7 +8,8 @@
|
||||||
"globals": {
|
"globals": {
|
||||||
"_": false,
|
"_": false,
|
||||||
"gl": false,
|
"gl": false,
|
||||||
"gon": false
|
"gon": false,
|
||||||
|
"localStorage": false
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"filenames"
|
"filenames"
|
||||||
|
|
102
.gitlab-ci.yml
102
.gitlab-ci.yml
|
@ -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:
|
||||||
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:
|
||||||
|
|
||||||
.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:
|
||||||
- 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
|
||||||
|
|
||||||
.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
|
||||||
|
|
||||||
.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:
|
||||||
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:
|
||||||
- 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,6 +305,7 @@ 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:
|
||||||
|
@ -295,6 +313,7 @@ lint-doc:
|
||||||
|
|
||||||
bundler:check:
|
bundler:check:
|
||||||
stage: test
|
stage: test
|
||||||
|
<<: *dedicated-runner
|
||||||
<<: *ruby-static-analysis
|
<<: *ruby-static-analysis
|
||||||
script:
|
script:
|
||||||
- bundle check
|
- bundle check
|
||||||
|
@ -302,6 +321,7 @@ bundler: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:
|
||||||
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:
|
||||||
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:
|
||||||
- coverage/assets/
|
- coverage/assets/
|
||||||
|
|
||||||
lint:javascript:
|
lint:javascript:
|
||||||
|
<<: *dedicated-runner
|
||||||
cache:
|
cache:
|
||||||
paths:
|
paths:
|
||||||
- node_modules/
|
- node_modules/
|
||||||
|
@ -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:
|
||||||
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:
|
||||||
|
|
||||||
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:
|
||||||
pages:
|
pages:
|
||||||
before_script: []
|
before_script: []
|
||||||
stage: pages
|
stage: pages
|
||||||
|
<<: *dedicated-runner
|
||||||
dependencies:
|
dependencies:
|
||||||
- coverage
|
- coverage
|
||||||
- teaspoon
|
- teaspoon
|
||||||
|
@ -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:
|
||||||
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.)
|
||||||
|
|
||||||
### 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:
|
||||||
|
|
33
CHANGELOG.md
33
CHANGELOG.md
|
@ -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.
|
||||||
- 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.
|
||||||
- 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
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
4.0.0
|
4.0.3
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1.1.0
|
1.1.1
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -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
|
||||||
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
|
||||||
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:
|
||||||
|
|
||||||
- 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 @@
|
||||||
// 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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
: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 @@
|
||||||
: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 @@
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
playIconSvg: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) => {
|
|
||||||
element.appendChild(playIcon.cloneNode(true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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 @@
|
||||||
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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
commitIconSvg: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
playIconSvg: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
@ -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 @@
|
||||||
<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 @@
|
||||||
<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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
} 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 @@
|
||||||
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 @@
|
||||||
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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,38 +1,81 @@
|
||||||
/* 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 @@
|
||||||
$('#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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
49
app/assets/javascripts/signin_tabs_memoizer.js.es6
Normal file
49
app/assets/javascripts/signin_tabs_memoizer.js.es6
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/* 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 @@
|
||||||
(() => {
|
(() => {
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
$(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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
* name
|
* name
|
||||||
* ref_url
|
* ref_url
|
||||||
*/
|
*/
|
||||||
commit_ref: {
|
commitRef: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
|
@ -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 @@
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
commitIconSvg: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -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 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 @@
|
||||||
|
|
||||||
<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 @@
|
||||||
</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 @@
|
||||||
|
|
||||||
@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 @@
|
||||||
@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";
|
||||||
|
|
27
app/assets/stylesheets/framework/asciidoctor.scss
Normal file
27
app/assets/stylesheets/framework/asciidoctor.scss
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
.awards {
|
.awards {
|
||||||
.emoji-icon {
|
.emoji-icon {
|
||||||
width: 19px;
|
width: 20px;
|
||||||
height: 19px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,3 @@
|
||||||
.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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
21
app/assets/stylesheets/framework/broadcast-messages.scss
Normal file
21
app/assets/stylesheets/framework/broadcast-messages.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.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;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
@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 {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
|
@ -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 {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.str-truncated {
|
||||||
|
&-60 {
|
||||||
|
@include str-truncated(60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-100 {
|
||||||
|
@include str-truncated(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 @@
|
||||||
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 @@
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.responsive-table {
|
||||||
|
@media (max-width: $screen-sm-max) {
|
||||||
|
th {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
width: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 @@
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,182 +0,0 @@
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
13
app/assets/stylesheets/pages/deploy_keys.scss
Normal file
13
app/assets/stylesheets/pages/deploy_keys.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.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 @@
|
||||||
@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 {
|
||||||
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 {
|
||||||
.fa {
|
.fa {
|
||||||
color: $notes-action-color;
|
color: $notes-action-color;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
.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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 @@
|
||||||
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;
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
.tag-buttons {
|
|
||||||
line-height: 40px;
|
|
||||||
|
|
||||||
.btn:not(.dropdown-toggle) {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
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
|
||||||
@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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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 @@
|
||||||
# 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
|
||||||
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
|
||||||
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
|
||||||
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 @@
|
||||||
# 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
|
||||||
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
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
if line.blank?
|
if line.blank?
|
||||||
" ".html_safe
|
" ".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
|
||||||
@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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
.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
|
||||||
|
|
37
app/models/ci/stage.rb
Normal file
37
app/models/ci/stage.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
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
|
end
|
||||||
|
|
||||||
def has_been_reverted?(current_user = nil, noteable = self)
|
@merged_merge_request_hash[current_user]
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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)}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
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
|
||||||
(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
|
||||||
|
|
||||||
|
|
70
app/models/concerns/routable.rb
Normal file
70
app/models/concerns/routable.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# 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
|
||||||
@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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
@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
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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,12 +85,8 @@ 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?
|
|
||||||
|
|
||||||
@has_visible_content = cache.fetch(:has_visible_content?) do
|
|
||||||
branch_count > 0
|
branch_count > 0
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def commit(ref = 'HEAD')
|
def commit(ref = 'HEAD')
|
||||||
return nil unless exists?
|
return nil unless exists?
|
||||||
|
@ -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
|
||||||
# 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
|
||||||
|
|
||||||
# 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
|
||||||
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,
|
||||||
|
|
22
app/models/route.rb
Normal file
22
app/models/route.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
::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
|
||||||
|
|
4
app/services/discussions/base_service.rb
Normal file
4
app/services/discussions/base_service.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Discussions
|
||||||
|
class BaseService < ::BaseService
|
||||||
|
end
|
||||||
|
end
|
24
app/services/discussions/resolve_service.rb
Normal file
24
app/services/discussions/resolve_service.rb
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
|
@ -122,7 +122,8 @@ class IssuableBaseService < BaseService
|
||||||
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
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
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)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue