Merge CSS
This commit is contained in:
commit
eb839b9af5
|
@ -8,7 +8,8 @@
|
|||
"globals": {
|
||||
"_": false,
|
||||
"gl": false,
|
||||
"gon": false
|
||||
"gon": false,
|
||||
"localStorage": false
|
||||
},
|
||||
"plugins": [
|
||||
"filenames"
|
||||
|
|
102
.gitlab-ci.yml
102
.gitlab-ci.yml
|
@ -30,7 +30,12 @@ stages:
|
|||
- post-test
|
||||
- pages
|
||||
|
||||
# Prepare and merge knapsack tests
|
||||
# Predefined scopes
|
||||
.dedicated-runner: &dedicated-runner
|
||||
tags:
|
||||
- gitlab-org
|
||||
- 2gb
|
||||
|
||||
.knapsack-state: &knapsack-state
|
||||
services: []
|
||||
variables:
|
||||
|
@ -45,47 +50,14 @@ stages:
|
|||
paths:
|
||||
- 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
|
||||
services:
|
||||
- mysql:latest
|
||||
- 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
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *use-db
|
||||
script:
|
||||
- JOB_NAME=( $CI_BUILD_NAME )
|
||||
|
@ -103,6 +75,7 @@ setup-test-env:
|
|||
|
||||
.spinach-knapsack: &spinach-knapsack
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *use-db
|
||||
script:
|
||||
- JOB_NAME=( $CI_BUILD_NAME )
|
||||
|
@ -118,6 +91,44 @@ setup-test-env:
|
|||
- knapsack/
|
||||
- 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 1 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
|
||||
<<: *dedicated-runner
|
||||
<<: *ruby-21
|
||||
|
||||
.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
|
||||
<<: *spinach-knapsack
|
||||
<<: *dedicated-runner
|
||||
<<: *ruby-21
|
||||
|
||||
rspec 0 20 ruby21: *rspec-knapsack-ruby21
|
||||
|
@ -214,6 +227,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
|
|||
|
||||
.exec: &exec
|
||||
<<: *ruby-static-analysis
|
||||
<<: *dedicated-runner
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec $CI_BUILD_NAME
|
||||
|
@ -249,12 +263,14 @@ rake ee_compat_check:
|
|||
rake db:migrate:reset:
|
||||
stage: test
|
||||
<<: *use-db
|
||||
<<: *dedicated-runner
|
||||
script:
|
||||
- rake db:migrate:reset
|
||||
|
||||
rake db:seed_fu:
|
||||
stage: test
|
||||
<<: *use-db
|
||||
<<: *dedicated-runner
|
||||
variables:
|
||||
SIZE: "1"
|
||||
SETUP_DB: "false"
|
||||
|
@ -276,6 +292,7 @@ teaspoon:
|
|||
- node_modules/
|
||||
stage: test
|
||||
<<: *use-db
|
||||
<<: *dedicated-runner
|
||||
script:
|
||||
- npm install
|
||||
- npm link istanbul
|
||||
|
@ -288,6 +305,7 @@ teaspoon:
|
|||
|
||||
lint-doc:
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
image: "phusion/baseimage:latest"
|
||||
before_script: []
|
||||
script:
|
||||
|
@ -295,6 +313,7 @@ lint-doc:
|
|||
|
||||
bundler:check:
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *ruby-static-analysis
|
||||
script:
|
||||
- bundle check
|
||||
|
@ -302,6 +321,7 @@ bundler:check:
|
|||
bundler:audit:
|
||||
stage: test
|
||||
<<: *ruby-static-analysis
|
||||
<<: *dedicated-runner
|
||||
only:
|
||||
- master@gitlab-org/gitlab-ce
|
||||
- master@gitlab-org/gitlab-ee
|
||||
|
@ -313,6 +333,7 @@ bundler:audit:
|
|||
migration paths:
|
||||
stage: test
|
||||
<<: *use-db
|
||||
<<: *dedicated-runner
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
only:
|
||||
|
@ -334,6 +355,7 @@ migration paths:
|
|||
coverage:
|
||||
stage: post-test
|
||||
services: []
|
||||
<<: *dedicated-runner
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
USE_BUNDLE_INSTALL: "true"
|
||||
|
@ -347,6 +369,7 @@ coverage:
|
|||
- coverage/assets/
|
||||
|
||||
lint:javascript:
|
||||
<<: *dedicated-runner
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
|
@ -358,6 +381,7 @@ lint:javascript:
|
|||
- npm --silent run eslint
|
||||
|
||||
lint:javascript:report:
|
||||
<<: *dedicated-runner
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
|
@ -379,6 +403,7 @@ lint:javascript:report:
|
|||
trigger_docs:
|
||||
stage: post-test
|
||||
image: "alpine"
|
||||
<<: *dedicated-runner
|
||||
before_script:
|
||||
- apk update && apk add curl
|
||||
variables:
|
||||
|
@ -394,6 +419,7 @@ trigger_docs:
|
|||
|
||||
notify:slack:
|
||||
stage: post-test
|
||||
<<: *dedicated-runner
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
USE_BUNDLE_INSTALL: "false"
|
||||
|
@ -409,6 +435,7 @@ notify:slack:
|
|||
pages:
|
||||
before_script: []
|
||||
stage: pages
|
||||
<<: *dedicated-runner
|
||||
dependencies:
|
||||
- coverage
|
||||
- teaspoon
|
||||
|
@ -423,11 +450,12 @@ pages:
|
|||
paths:
|
||||
- public
|
||||
only:
|
||||
- master
|
||||
- master@gitlab-org/gitlab-ce
|
||||
|
||||
# Insurance in case a gem needed by one of our releases gets yanked from
|
||||
# rubygems.org in the future.
|
||||
cache gems:
|
||||
<<: *dedicated-runner
|
||||
only:
|
||||
- tags
|
||||
variables:
|
||||
|
@ -437,3 +465,5 @@ cache gems:
|
|||
artifacts:
|
||||
paths:
|
||||
- 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
|
||||
|
||||
(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com)
|
||||
|
||||
#### Results of GitLab application Check
|
||||
|
||||
(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
|
||||
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)
|
||||
|
||||
- Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744
|
||||
|
@ -251,6 +264,11 @@ entry.
|
|||
- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
|
||||
- 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)
|
||||
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
- 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 '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-retry', '~> 0.4.5'
|
||||
gem 'spinach-rails', '~> 0.2.1'
|
||||
|
|
|
@ -177,10 +177,10 @@ GEM
|
|||
excon (0.52.0)
|
||||
execjs (2.6.0)
|
||||
expression_parser (0.9.0)
|
||||
factory_girl (4.5.0)
|
||||
factory_girl (4.7.0)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_girl_rails (4.6.0)
|
||||
factory_girl (~> 4.5.0)
|
||||
factory_girl_rails (4.7.0)
|
||||
factory_girl (~> 4.7.0)
|
||||
railties (>= 3.0.0)
|
||||
faraday (0.9.2)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
|
@ -819,7 +819,7 @@ DEPENDENCIES
|
|||
dropzonejs-rails (~> 0.7.1)
|
||||
email_reply_parser (~> 0.5.8)
|
||||
email_spec (~> 1.6.0)
|
||||
factory_girl_rails (~> 4.6.0)
|
||||
factory_girl_rails (~> 4.7.0)
|
||||
ffaker (~> 2.0.0)
|
||||
flay (~> 2.6.1)
|
||||
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
|
||||
- Ruby (MRI) 2.3
|
||||
- Git 2.7.4+
|
||||
- Git 2.8.4+
|
||||
- Redis 2.8+
|
||||
- MySQL or PostgreSQL
|
||||
|
||||
|
|
|
@ -70,6 +70,8 @@
|
|||
// e.g.
|
||||
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
|
||||
requestFileSuccess(file, { skipFocus } = {}) {
|
||||
if (!file) return;
|
||||
|
||||
const oldValue = this.editor.getValue();
|
||||
let newValue = file.content;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
switch (page) {
|
||||
case 'sessions:new':
|
||||
new UsernameValidator();
|
||||
new ActiveTabMemoizer();
|
||||
break;
|
||||
case 'projects:boards:show':
|
||||
case 'projects:boards:index':
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
|
||||
newEnvironmentPath: environmentsData.newEnvironmentPath,
|
||||
helpPagePath: environmentsData.helpPagePath,
|
||||
commitIconSvg: environmentsData.commitIconSvg,
|
||||
playIconSvg: environmentsData.playIconSvg,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -227,7 +229,9 @@
|
|||
:model="model"
|
||||
:toggleRow="toggleRow.bind(model)"
|
||||
: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"
|
||||
is="environment-item"
|
||||
|
@ -235,7 +239,9 @@
|
|||
:model="children"
|
||||
:toggleRow="toggleRow.bind(children)"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed">
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:commit-icon-svg="commitIconSvg">
|
||||
</tr>
|
||||
|
||||
</template>
|
||||
|
|
|
@ -12,38 +12,18 @@
|
|||
required: false,
|
||||
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: `
|
||||
<div class="inline">
|
||||
<div class="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>
|
||||
</a>
|
||||
|
||||
|
@ -53,7 +33,9 @@
|
|||
data-method="post"
|
||||
rel="nofollow"
|
||||
class="js-manual-action-link">
|
||||
<span class="action-play-icon-container"></span>
|
||||
|
||||
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
|
||||
|
||||
<span>
|
||||
{{action.name}}
|
||||
</span>
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
|
||||
props: {
|
||||
external_url: {
|
||||
externalUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
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>
|
||||
</a>
|
||||
`,
|
||||
|
|
|
@ -58,6 +58,16 @@
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
commitIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
playIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -451,11 +461,12 @@
|
|||
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
:commit_ref="commitRef"
|
||||
:commit_url="commitUrl"
|
||||
:short_sha="commitShortSha"
|
||||
:commit-ref="commitRef"
|
||||
:commit-url="commitUrl"
|
||||
:short-sha="commitShortSha"
|
||||
:title="commitTitle"
|
||||
:author="commitAuthor">
|
||||
:author="commitAuthor"
|
||||
:commit-icon-svg="commitIconSvg">
|
||||
</commit-component>
|
||||
</div>
|
||||
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
|
||||
|
@ -476,6 +487,7 @@
|
|||
<div v-if="hasManualActions && canCreateDeployment"
|
||||
class="inline js-manual-actions-container">
|
||||
<actions-component
|
||||
:play-icon-svg="playIconSvg"
|
||||
:actions="manualActions">
|
||||
</actions-component>
|
||||
</div>
|
||||
|
@ -483,22 +495,22 @@
|
|||
<div v-if="model.external_url && canReadEnvironment"
|
||||
class="inline js-external-url-container">
|
||||
<external-url-component
|
||||
:external_url="model.external_url">
|
||||
</external_url-component>
|
||||
:external-url="model.external_url">
|
||||
</external-url-component>
|
||||
</div>
|
||||
|
||||
<div v-if="isStoppable && canCreateDeployment"
|
||||
class="inline js-stop-component-container">
|
||||
<stop-component
|
||||
:stop_url="model.stop_path">
|
||||
:stop-url="model.stop_path">
|
||||
</stop-component>
|
||||
</div>
|
||||
|
||||
<div v-if="canRetry && canCreateDeployment"
|
||||
class="inline js-rollback-component-container">
|
||||
<rollback-component
|
||||
:is_last_deployment="isLastDeployment"
|
||||
:retry_url="retryUrl">
|
||||
:is-last-deployment="isLastDeployment"
|
||||
:retry-url="retryUrl">
|
||||
</rollback-component>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,19 +7,20 @@
|
|||
|
||||
window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
|
||||
props: {
|
||||
retry_url: {
|
||||
retryUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
is_last_deployment: {
|
||||
|
||||
isLastDeployment: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn" :href="retry_url" data-method="post" rel="nofollow">
|
||||
<span v-if="is_last_deployment">
|
||||
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
|
||||
<span v-if="isLastDeployment">
|
||||
Re-deploy
|
||||
</span>
|
||||
<span v-else>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
|
||||
props: {
|
||||
stop_url: {
|
||||
stopUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
@ -15,7 +15,7 @@
|
|||
|
||||
template: `
|
||||
<a class="btn stop-env-link"
|
||||
:href="stop_url"
|
||||
:href="stopUrl"
|
||||
data-confirm="Are you sure you want to stop this environment?"
|
||||
data-method="post"
|
||||
rel="nofollow">
|
||||
|
|
|
@ -650,6 +650,11 @@
|
|||
} else if(value) {
|
||||
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)) {
|
||||
el.removeClass(ACTIVE_CLASS);
|
||||
if (field && field.length) {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
setTimeago = true;
|
||||
}
|
||||
|
||||
$timeagoEls.each(function() {
|
||||
$timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
|
||||
var $el = $(this);
|
||||
$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>'
|
||||
});
|
||||
}
|
||||
|
||||
$el.attr('data-timeago-rendered', true);
|
||||
gl.utils.renderTimeago($el);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,38 +1,81 @@
|
|||
/* eslint-disable */
|
||||
((w) => {
|
||||
w.gl = w.gl || {};
|
||||
/* eslint-disable class-methods-use-this */
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
class Members {
|
||||
constructor() {
|
||||
this.addListeners();
|
||||
this.initGLDropdown();
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
|
||||
$('.js-member-update-control').off('change').on('change', this.formSubmit);
|
||||
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
|
||||
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
|
||||
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
|
||||
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) {
|
||||
const $target = $(e.target);
|
||||
|
||||
if ($target.hasClass('btn-remove')) {
|
||||
$target.closest('.member')
|
||||
.fadeOut(function () {
|
||||
.fadeOut(function fadeOutMemberRow() {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formSubmit() {
|
||||
$(this).closest('form').trigger("submit.rails").end().disable();
|
||||
formSubmit(e, $el = null) {
|
||||
const $this = e ? $(e.currentTarget) : $el;
|
||||
const { $toggle, $dateInput } = this.getMemberListItems($this);
|
||||
|
||||
$this.closest('form').trigger('submit.rails');
|
||||
|
||||
$toggle.disable();
|
||||
$dateInput.disable();
|
||||
}
|
||||
|
||||
formSuccess() {
|
||||
$(this).find('.js-member-update-control').enable();
|
||||
formSuccess(e) {
|
||||
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;
|
||||
})(window);
|
||||
})();
|
||||
|
|
|
@ -40,19 +40,26 @@
|
|||
$('#modal_merge_info').modal({
|
||||
show: false
|
||||
});
|
||||
this.firstCICheck = true;
|
||||
this.readyForCICheck = false;
|
||||
this.readyForCIEnvironmentCheck = false;
|
||||
this.cancel = false;
|
||||
clearInterval(this.fetchBuildStatusInterval);
|
||||
clearInterval(this.fetchBuildEnvironmentStatusInterval);
|
||||
this.clearEventListeners();
|
||||
this.addEventListeners();
|
||||
this.getCIStatus(false);
|
||||
this.getCIEnvironmentsStatus();
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -60,10 +67,6 @@
|
|||
return $(document).off('page:change.merge_request');
|
||||
};
|
||||
|
||||
MergeRequestWidget.prototype.cancelPolling = function() {
|
||||
return this.cancel = true;
|
||||
};
|
||||
|
||||
MergeRequestWidget.prototype.addEventListeners = function() {
|
||||
var allowedPages;
|
||||
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
|
||||
|
@ -72,9 +75,6 @@
|
|||
var page;
|
||||
page = $('body').data('page').split(':').last();
|
||||
if (allowedPages.indexOf(page) < 0) {
|
||||
clearInterval(_this.fetchBuildStatusInterval);
|
||||
clearInterval(_this.fetchBuildEnvironmentStatusInterval);
|
||||
_this.cancelPolling();
|
||||
return _this.clearEventListeners();
|
||||
}
|
||||
};
|
||||
|
@ -101,7 +101,7 @@
|
|||
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
|
||||
return window.location.href = window.location.pathname + urlSuffix;
|
||||
} else if (data.merge_error) {
|
||||
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
|
||||
return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
|
||||
} else {
|
||||
callback = function() {
|
||||
return merge_request_widget.mergeInProgress(deleteSourceBranch);
|
||||
|
@ -114,6 +114,11 @@
|
|||
});
|
||||
};
|
||||
|
||||
MergeRequestWidget.prototype.cancelPolling = function () {
|
||||
this.ciStatusInterval.cancel();
|
||||
this.ciEnvironmentStatusInterval.cancel();
|
||||
};
|
||||
|
||||
MergeRequestWidget.prototype.getMergeStatus = function() {
|
||||
return $.get(this.opts.merge_check_url, function(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) {
|
||||
var _this;
|
||||
_this = this;
|
||||
|
@ -150,23 +143,17 @@
|
|||
return $.getJSON(this.opts.ci_status_url, (function(_this) {
|
||||
return function(data) {
|
||||
var message, status, title;
|
||||
if (_this.cancel) {
|
||||
return;
|
||||
}
|
||||
_this.readyForCICheck = true;
|
||||
if (data.status === '') {
|
||||
return;
|
||||
}
|
||||
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.showCIStatus(data.status);
|
||||
if (data.coverage) {
|
||||
_this.showCICoverage(data.coverage);
|
||||
}
|
||||
// The first check should only update the UI, a notification
|
||||
// should only be displayed on status changes
|
||||
if (showNotification && !_this.firstCICheck) {
|
||||
if (showNotification) {
|
||||
status = _this.ciLabelForStatus(data.status);
|
||||
if (status === "preparing") {
|
||||
title = _this.opts.ci_title.preparing;
|
||||
|
@ -184,24 +171,13 @@
|
|||
return Turbolinks.visit(_this.opts.builds_path);
|
||||
});
|
||||
}
|
||||
return _this.firstCICheck = false;
|
||||
}
|
||||
};
|
||||
})(this));
|
||||
};
|
||||
|
||||
MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
|
||||
this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
|
||||
if (!this.readyForCIEnvironmentCheck) return;
|
||||
this.getCIEnvironmentsStatus();
|
||||
this.readyForCIEnvironmentCheck = false;
|
||||
}, 300000);
|
||||
};
|
||||
|
||||
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
|
||||
$.getJSON(this.opts.ci_environments_status_url, (environments) => {
|
||||
if (this.cancel) return;
|
||||
this.readyForCIEnvironmentCheck = true;
|
||||
if (environments && environments.length) this.renderEnvironments(environments);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
* @param { function } callback Function to be called on each iteration (required)
|
||||
* @param { milliseconds } startingInterval `currentInterval` is set to this initially
|
||||
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this
|
||||
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
|
||||
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
|
||||
* @param { function } opts.callback Function to be called on each iteration (required)
|
||||
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
|
||||
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
|
||||
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
|
||||
* 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 = {
|
||||
callback,
|
||||
startingInterval,
|
||||
maxInterval,
|
||||
incrementByFactorOf,
|
||||
lazyStart,
|
||||
callback: opts.callback,
|
||||
startingInterval: opts.startingInterval,
|
||||
maxInterval: opts.maxInterval,
|
||||
hiddenInterval: opts.hiddenInterval,
|
||||
incrementByFactorOf: opts.incrementByFactorOf,
|
||||
lazyStart: opts.lazyStart,
|
||||
immediateExecution: opts.immediateExecution,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
intervalId: null,
|
||||
currentInterval: startingInterval,
|
||||
currentInterval: this.cfg.startingInterval,
|
||||
pageVisibility: 'visible',
|
||||
};
|
||||
|
||||
|
@ -36,6 +43,11 @@
|
|||
const cfg = this.cfg;
|
||||
const state = this.state;
|
||||
|
||||
if (cfg.immediateExecution) {
|
||||
cfg.immediateExecution = false;
|
||||
cfg.callback();
|
||||
}
|
||||
|
||||
state.intervalId = window.setInterval(() => {
|
||||
cfg.callback();
|
||||
|
||||
|
@ -54,14 +66,29 @@
|
|||
this.stopTimer();
|
||||
}
|
||||
|
||||
onVisibilityHidden() {
|
||||
if (this.cfg.hiddenInterval) {
|
||||
this.setCurrentInterval(this.cfg.hiddenInterval);
|
||||
this.resume();
|
||||
} else {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// start a timer, using the existing interval
|
||||
resume() {
|
||||
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
|
||||
this.start();
|
||||
}
|
||||
|
||||
onVisibilityVisible() {
|
||||
this.cancel();
|
||||
this.start();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.cancel();
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
$(document).off('visibilitychange').off('page:before-unload');
|
||||
}
|
||||
|
||||
|
@ -80,11 +107,7 @@
|
|||
|
||||
initVisibilityChangeHandling() {
|
||||
// cancel interval when tab no longer shown (prevents cached pages from polling)
|
||||
$(document)
|
||||
.off('visibilitychange').on('visibilitychange', (e) => {
|
||||
this.state.pageVisibility = e.target.visibilityState;
|
||||
this.handleVisibilityChange();
|
||||
});
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
|
||||
}
|
||||
|
||||
initPageUnloadHandling() {
|
||||
|
@ -92,10 +115,11 @@
|
|||
$(document).on('page:before-unload', () => this.cancel());
|
||||
}
|
||||
|
||||
handleVisibilityChange() {
|
||||
const state = this.state;
|
||||
|
||||
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;
|
||||
handleVisibilityChange(e) {
|
||||
this.state.pageVisibility = e.target.visibilityState;
|
||||
const intervalAction = this.isPageVisible() ?
|
||||
this.onVisibilityVisible :
|
||||
this.onVisibilityHidden;
|
||||
|
||||
intervalAction.apply(this);
|
||||
}
|
||||
|
@ -111,6 +135,7 @@
|
|||
incrementInterval() {
|
||||
const cfg = this.cfg;
|
||||
const currentInterval = this.getCurrentInterval();
|
||||
if (cfg.hiddenInterval && !this.isPageVisible()) return;
|
||||
let nextInterval = currentInterval * cfg.incrementByFactorOf;
|
||||
|
||||
if (nextInterval > cfg.maxInterval) {
|
||||
|
@ -120,6 +145,8 @@
|
|||
this.setCurrentInterval(nextInterval);
|
||||
}
|
||||
|
||||
isPageVisible() { return this.state.pageVisibility === 'visible'; }
|
||||
|
||||
stopTimer() {
|
||||
const state = this.state;
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
* name
|
||||
* ref_url
|
||||
*/
|
||||
commit_ref: {
|
||||
commitRef: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
|
@ -32,16 +32,16 @@
|
|||
/**
|
||||
* Used to link to the commit sha.
|
||||
*/
|
||||
commit_url: {
|
||||
commitUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
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,
|
||||
required: false,
|
||||
default: '',
|
||||
|
@ -68,6 +68,11 @@
|
|||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
commitIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -80,7 +85,7 @@
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
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: `
|
||||
<div class="branch-commit">
|
||||
|
||||
|
@ -138,15 +125,15 @@
|
|||
|
||||
<a v-if="hasCommitRef"
|
||||
class="monospace branch-name"
|
||||
:href="commit_ref.ref_url">
|
||||
{{commit_ref.name}}
|
||||
:href="commitRef.ref_url">
|
||||
{{commitRef.name}}
|
||||
</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"
|
||||
:href="commit_url">
|
||||
{{short_sha}}
|
||||
:href="commitUrl">
|
||||
{{shortSha}}
|
||||
</a>
|
||||
|
||||
<p class="commit-title">
|
||||
|
@ -162,7 +149,7 @@
|
|||
</a>
|
||||
|
||||
<a class="commit-row-message"
|
||||
:href="commit_url">
|
||||
:href="commitUrl">
|
||||
{{title}}
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
@import "framework/animations.scss";
|
||||
@import "framework/avatar.scss";
|
||||
@import "framework/asciidoctor.scss";
|
||||
@import "framework/blocks.scss";
|
||||
@import "framework/buttons.scss";
|
||||
@import "framework/calendar.scss";
|
||||
|
@ -40,3 +41,6 @@
|
|||
@import "framework/blank";
|
||||
@import "framework/wells.scss";
|
||||
@import "framework/page-header.scss";
|
||||
@import "framework/awards.scss";
|
||||
@import "framework/images.scss";
|
||||
@import "framework/broadcast-messages";
|
||||
|
|
|
@ -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 {
|
||||
.emoji-icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,5 +136,6 @@
|
|||
|
||||
.award-control-icon {
|
||||
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 {
|
||||
text-align: center;
|
||||
color: $gl-gray;
|
||||
|
@ -274,6 +269,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media(max-width: $screen-xs-max) {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
|
|
|
@ -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 {
|
||||
border-radius: 3px;
|
||||
font-size: $gl-font-size;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
padding: $gl-vert-padding $gl-btn-padding;
|
||||
|
||||
&:focus,
|
||||
|
|
|
@ -255,6 +255,7 @@ img.emoji {
|
|||
height: 20px;
|
||||
vertical-align: top;
|
||||
width: 20px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
|
@ -379,7 +380,9 @@ table {
|
|||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.hide-bottom-border { border-bottom: none !important; }
|
||||
.hide-bottom-border {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.gl-accessibility {
|
||||
&:focus {
|
||||
|
@ -396,3 +399,13 @@ table {
|
|||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.str-truncated {
|
||||
&-60 {
|
||||
@include str-truncated(60%);
|
||||
}
|
||||
|
||||
&-100 {
|
||||
@include str-truncated(100%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
border-radius: $border-radius-base;
|
||||
white-space: nowrap;
|
||||
|
||||
&[disabled] {
|
||||
background-color: $input-bg-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.no-outline {
|
||||
outline: 0;
|
||||
}
|
||||
|
|
|
@ -106,13 +106,13 @@ ul.task-list {
|
|||
}
|
||||
}
|
||||
|
||||
// Generic content list
|
||||
ul.content-list {
|
||||
@include basic-list;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
li {
|
||||
border-color: $table-border-color;
|
||||
font-size: $list-font-size;
|
||||
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 {
|
||||
padding: $gl-padding-top $gl-padding;
|
||||
|
||||
|
|
|
@ -71,6 +71,10 @@
|
|||
border-bottom: 2px solid $link-underline-blue;
|
||||
color: $black;
|
||||
font-weight: 600;
|
||||
|
||||
.badge {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
|
@ -268,6 +272,16 @@
|
|||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.multi-line {
|
||||
.nav-text {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.nav-controls {
|
||||
padding: 17px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-nav {
|
||||
|
|
|
@ -34,6 +34,10 @@ table {
|
|||
background-color: $background-color;
|
||||
font-weight: normal;
|
||||
border-bottom: none;
|
||||
|
||||
&.wide {
|
||||
width: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
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-green: $gl-text-green;
|
||||
|
||||
|
||||
/*
|
||||
* Dashboard
|
||||
*/
|
||||
$dashboard-project-access-icon-color: #888;
|
||||
|
||||
/*
|
||||
* Editor
|
||||
*/
|
||||
|
|
|
@ -43,3 +43,16 @@
|
|||
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;
|
||||
}
|
|
@ -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) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.member-access-text {
|
||||
|
|
|
@ -124,7 +124,7 @@ ul.notes {
|
|||
position: absolute;
|
||||
left: 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 {
|
||||
|
@ -413,7 +413,6 @@ ul.notes {
|
|||
.fa {
|
||||
color: $notes-action-color;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
.notification-list-item {
|
||||
line-height: 34px;
|
||||
|
||||
.dropdown-menu {
|
||||
@extend .dropdown-menu-align-right;
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
|
|
|
@ -280,6 +280,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.admin-builds-table {
|
||||
.ci-table td:last-child {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline visualization
|
||||
|
||||
.toggle-pipeline-btn {
|
||||
|
|
|
@ -188,6 +188,10 @@
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.notification-dropdown .dropdown-menu {
|
||||
@extend .dropdown-menu-align-right;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
@media (max-width: $screen-md-max) {
|
||||
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
|
||||
|
||||
def group
|
||||
@group ||= Group.find_by(path: params[:id])
|
||||
@group ||= Group.find_by_full_path(params[:id])
|
||||
end
|
||||
|
||||
def group_params
|
||||
|
|
|
@ -81,10 +81,8 @@ module CreatesCommit
|
|||
def merge_request_exists?
|
||||
return @merge_request if defined?(@merge_request)
|
||||
|
||||
@merge_request = @mr_target_project.merge_requests.opened.find_by(
|
||||
source_branch: @mr_source_branch,
|
||||
target_branch: @mr_target_branch
|
||||
)
|
||||
@merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
|
||||
find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch)
|
||||
end
|
||||
|
||||
def different_project?
|
||||
|
|
|
@ -6,7 +6,12 @@ module MergeRequestsAction
|
|||
@label = merge_requests_finder.labels.first
|
||||
|
||||
@merge_requests = merge_requests_collection
|
||||
.non_archived
|
||||
.page(params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_params
|
||||
super.merge(non_archived: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController
|
|||
def group
|
||||
unless @group
|
||||
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)
|
||||
@group = nil
|
||||
|
|
|
@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
|
||||
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)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def successful_change_path
|
||||
return referenced_merge_request_url if @commit.merged_merge_request
|
||||
|
||||
namespace_project_commits_url(@project.namespace, @project, @target_branch)
|
||||
referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch)
|
||||
end
|
||||
|
||||
def failed_change_path
|
||||
return referenced_merge_request_url if @commit.merged_merge_request
|
||||
|
||||
namespace_project_commit_url(@project.namespace, @project, params[:id])
|
||||
referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id])
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def commit
|
||||
|
|
|
@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController
|
|||
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
|
||||
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)
|
||||
|
||||
respond_to do |format|
|
||||
|
|
|
@ -53,7 +53,7 @@ class Projects::CompareController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
|
|||
before_action :authorize_resolve_discussion!
|
||||
|
||||
def resolve
|
||||
discussion.resolve!(current_user)
|
||||
|
||||
MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
|
||||
Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
|
||||
|
||||
render json: {
|
||||
resolved_by: discussion.resolved_by.try(:name),
|
||||
|
@ -26,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
|
|||
private
|
||||
|
||||
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
|
||||
|
||||
def discussion
|
||||
|
|
|
@ -46,8 +46,9 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
params[:issue] ||= ActionController::Parameters.new(
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -75,7 +76,9 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
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|
|
||||
format.html do
|
||||
|
@ -169,6 +172,14 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
alias_method :awardable, :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!
|
||||
return render_404 unless can?(current_user, :read_issue, @issue)
|
||||
end
|
||||
|
|
|
@ -10,14 +10,37 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
@project_members = @project.project_members
|
||||
@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?
|
||||
users = @project.users.search(params[:search]).to_a
|
||||
@project_members = @project_members.where(user_id: users)
|
||||
user_ids = @project.users.search(params[:search]).select(:id)
|
||||
@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))
|
||||
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)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class Projects::TodosController < Projects::ApplicationController
|
|||
when "issue"
|
||||
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
|
||||
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
|
||||
|
|
|
@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
DeleteUserService.new(current_user).execute(current_user)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -31,10 +31,18 @@ class SessionsController < Devise::SessionsController
|
|||
resource.update_attributes(reset_password_token: nil,
|
||||
reset_password_sent_at: nil)
|
||||
end
|
||||
# hide the signed-in notification
|
||||
flash[:notice] = nil
|
||||
log_audit_event(current_user, with: authentication_method)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
super
|
||||
# hide the signed_out notice
|
||||
flash[:notice] = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Handle an "initial setup" state, where there's only one user, it's an admin,
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# search: string
|
||||
# label_name: string
|
||||
# sort: string
|
||||
# non_archived: boolean
|
||||
#
|
||||
class IssuableFinder
|
||||
NONE = '0'
|
||||
|
@ -38,6 +39,7 @@ class IssuableFinder
|
|||
items = by_author(items)
|
||||
items = by_label(items)
|
||||
items = by_due_date(items)
|
||||
items = by_non_archived(items)
|
||||
sort(items)
|
||||
end
|
||||
|
||||
|
@ -75,6 +77,10 @@ class IssuableFinder
|
|||
counts
|
||||
end
|
||||
|
||||
def find_by!(*params)
|
||||
execute.find_by!(*params)
|
||||
end
|
||||
|
||||
def group
|
||||
return @group if defined?(@group)
|
||||
|
||||
|
@ -356,6 +362,10 @@ class IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def by_non_archived(items)
|
||||
params[:non_archived].present? ? items.non_archived : items
|
||||
end
|
||||
|
||||
def current_user_related?
|
||||
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
|
||||
end
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# search: string
|
||||
# label_name: string
|
||||
# sort: string
|
||||
# non_archived: boolean
|
||||
#
|
||||
class MergeRequestsFinder < IssuableFinder
|
||||
def klass
|
||||
|
|
|
@ -14,7 +14,7 @@ class NotesFinder
|
|||
when "issue"
|
||||
IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
|
||||
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"
|
||||
project.snippets.find(target_id).notes
|
||||
else
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
class SnippetsFinder
|
||||
def execute(current_user, params = {})
|
||||
filter = params[:filter]
|
||||
user = params.fetch(:user, current_user)
|
||||
|
||||
case filter
|
||||
when :all then
|
||||
snippets(current_user).fresh
|
||||
when :public then
|
||||
Snippet.are_public.fresh
|
||||
when :by_user then
|
||||
by_user(current_user, params[:user], params[:scope])
|
||||
by_user(current_user, user, params[:scope])
|
||||
when :by_project
|
||||
by_project(current_user, params[:project])
|
||||
end
|
||||
|
|
|
@ -5,8 +5,9 @@ module CiStatusHelper
|
|||
end
|
||||
|
||||
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}"
|
||||
|
||||
if target
|
||||
link_to content, target, class: klass
|
||||
else
|
||||
|
@ -14,7 +15,19 @@ module CiStatusHelper
|
|||
end
|
||||
end
|
||||
|
||||
def ci_text_for_status(status)
|
||||
if detailed_status?(status)
|
||||
status.text
|
||||
else
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
def ci_label_for_status(status)
|
||||
if detailed_status?(status)
|
||||
return status.label
|
||||
end
|
||||
|
||||
case status
|
||||
when 'success'
|
||||
'passed'
|
||||
|
@ -31,6 +44,10 @@ module CiStatusHelper
|
|||
end
|
||||
|
||||
def ci_icon_for_status(status)
|
||||
if detailed_status?(status)
|
||||
return custom_icon(status.icon)
|
||||
end
|
||||
|
||||
icon_name =
|
||||
case status
|
||||
when 'success'
|
||||
|
@ -94,4 +111,10 @@ module CiStatusHelper
|
|||
class: klass, title: title, data: data
|
||||
end
|
||||
end
|
||||
|
||||
def detailed_status?(status)
|
||||
status.respond_to?(:text) &&
|
||||
status.respond_to?(:label) &&
|
||||
status.respond_to?(:icon)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -130,7 +130,7 @@ module CommitsHelper
|
|||
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
|
||||
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?
|
||||
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)
|
||||
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?
|
||||
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
|
||||
|
|
|
@ -55,7 +55,9 @@ module DiffHelper
|
|||
if line.blank?
|
||||
" ".html_safe
|
||||
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
|
||||
|
||||
|
|
|
@ -45,6 +45,12 @@ module EventsHelper
|
|||
@project.feature_available?(feature_key, current_user)
|
||||
end
|
||||
|
||||
def comments_visible?
|
||||
event_filter_visible(:repository) ||
|
||||
event_filter_visible(:merge_requests) ||
|
||||
event_filter_visible(:issues)
|
||||
end
|
||||
|
||||
def event_preposition(event)
|
||||
if event.push? || event.commented? || event.target
|
||||
"at"
|
||||
|
|
|
@ -159,6 +159,11 @@ module GitlabRoutingHelper
|
|||
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
|
||||
end
|
||||
|
||||
# Snippets
|
||||
def personal_snippet_url(snippet, *args)
|
||||
snippet_url(snippet)
|
||||
end
|
||||
|
||||
# Groups
|
||||
|
||||
## Members
|
||||
|
|
|
@ -5,7 +5,7 @@ module GroupsHelper
|
|||
|
||||
def group_icon(group)
|
||||
if group.is_a?(String)
|
||||
group = Group.find_by(path: group)
|
||||
group = Group.find_by_full_path(group)
|
||||
end
|
||||
|
||||
group.try(:avatar_url) || image_path('no_group_avatar.png')
|
||||
|
|
|
@ -21,8 +21,6 @@ module Ci
|
|||
|
||||
after_create :keep_around_commits, unless: :importing?
|
||||
|
||||
delegate :stages, to: :statuses
|
||||
|
||||
state_machine :status, initial: :created do
|
||||
event :enqueue do
|
||||
transition created: :pending
|
||||
|
@ -98,17 +96,35 @@ module Ci
|
|||
sha[0...8]
|
||||
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
|
||||
where.not(duration: nil).sum(:duration)
|
||||
end
|
||||
|
||||
def stages_with_latest_statuses
|
||||
statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
|
||||
def stages_count
|
||||
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
|
||||
|
||||
def project_id
|
||||
|
@ -320,6 +336,10 @@ module Ci
|
|||
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
|
||||
end
|
||||
|
||||
def detailed_status
|
||||
Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pipeline_data
|
||||
|
|
|
@ -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
|
||||
|
||||
validates_uniqueness_of :key, scope: :gl_project_id
|
||||
validates :key,
|
||||
presence: true,
|
||||
length: { within: 0..255 },
|
||||
uniqueness: { scope: :gl_project_id },
|
||||
length: { maximum: 255 },
|
||||
format: { with: /\A[a-zA-Z0-9_]+\z/,
|
||||
message: "can contain only letters, digits and '_'." }
|
||||
|
||||
|
|
|
@ -245,44 +245,47 @@ class Commit
|
|||
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
|
||||
end
|
||||
|
||||
def revert_description
|
||||
if merged_merge_request
|
||||
"This reverts merge request #{merged_merge_request.to_reference}"
|
||||
def revert_description(user)
|
||||
if merged_merge_request?(user)
|
||||
"This reverts merge request #{merged_merge_request(user).to_reference}"
|
||||
else
|
||||
"This reverts commit #{sha}"
|
||||
end
|
||||
end
|
||||
|
||||
def revert_message
|
||||
%Q{Revert "#{title.strip}"\n\n#{revert_description}}
|
||||
def revert_message(user)
|
||||
%Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
|
||||
end
|
||||
|
||||
def reverts_commit?(commit)
|
||||
description? && description.include?(commit.revert_description)
|
||||
def reverts_commit?(commit, user)
|
||||
description? && description.include?(commit.revert_description(user))
|
||||
end
|
||||
|
||||
def merge_commit?
|
||||
parents.size > 1
|
||||
end
|
||||
|
||||
def merged_merge_request
|
||||
return @merged_merge_request if defined?(@merged_merge_request)
|
||||
|
||||
@merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
|
||||
def merged_merge_request(current_user)
|
||||
# Memoize with per-user access check
|
||||
@merged_merge_request_hash ||= Hash.new do |hash, user|
|
||||
hash[user] = merged_merge_request_no_cache(user)
|
||||
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)
|
||||
|
||||
noteable.notes_with_associations.system.each do |note|
|
||||
note.all_references(current_user, extractor: ext)
|
||||
end
|
||||
|
||||
ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
|
||||
ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
|
||||
end
|
||||
|
||||
def change_type_title
|
||||
merged_merge_request ? 'merge request' : 'commit'
|
||||
def change_type_title(user)
|
||||
merged_merge_request?(user) ? 'merge request' : 'commit'
|
||||
end
|
||||
|
||||
# Get the URI type of the given path
|
||||
|
@ -350,4 +353,12 @@ class Commit
|
|||
|
||||
changes
|
||||
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
|
||||
|
|
|
@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base
|
|||
end
|
||||
|
||||
scope :exclude_ignored, -> do
|
||||
quoted_when = connection.quote_column_name('when')
|
||||
# We want to ignore failed_but_allowed jobs
|
||||
where("allow_failure = ? OR status IN (?)",
|
||||
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')
|
||||
false, all_state_names - [:failed, :canceled])
|
||||
end
|
||||
|
||||
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
|
||||
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
|
||||
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
|
||||
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
|
||||
|
||||
state_machine :status do
|
||||
event :enqueue do
|
||||
|
@ -117,20 +112,6 @@ class CommitStatus < ActiveRecord::Base
|
|||
name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
|
||||
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?
|
||||
allow_failure? && (failed? || canceled?)
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module HasStatus
|
|||
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
|
||||
STARTED_STATUSES = %w[running success failed skipped]
|
||||
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]
|
||||
|
||||
class_methods do
|
||||
|
@ -23,9 +23,10 @@ module HasStatus
|
|||
canceled = scope.canceled.select('count(*)').to_sql
|
||||
|
||||
"(CASE
|
||||
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
|
||||
WHEN (#{builds})=(#{success}) THEN 'success'
|
||||
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})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
|
||||
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
|
||||
|
|
|
@ -41,7 +41,7 @@ module Issuable
|
|||
has_one :metrics
|
||||
|
||||
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 :assigned_to, ->(u) { where(assignee_id: u.id)}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
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
|
||||
end
|
||||
|
||||
def total_items_count(user = nil)
|
||||
def total_items_count(user)
|
||||
issues_visible_to_user(user).size + merge_requests.size
|
||||
end
|
||||
|
||||
def complete?(user = nil)
|
||||
def complete?(user)
|
||||
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
|
||||
end
|
||||
|
||||
def percent_complete(user = nil)
|
||||
def percent_complete(user)
|
||||
((closed_items_count(user) * 100) / total_items_count(user)).abs
|
||||
rescue ZeroDivisionError
|
||||
0
|
||||
|
@ -29,7 +29,7 @@ module Milestoneish
|
|||
(Date.today - start_date).to_i
|
||||
end
|
||||
|
||||
def issues_visible_to_user(user = nil)
|
||||
def issues_visible_to_user(user)
|
||||
issues.visible_to_user(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
end
|
||||
|
||||
def first_note_to_resolve
|
||||
@first_note_to_resolve ||= notes.detect(&:to_be_resolved?)
|
||||
end
|
||||
|
||||
def last_note
|
||||
@last_note ||= @notes.last
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ class Environment < ActiveRecord::Base
|
|||
validates :name,
|
||||
presence: true,
|
||||
uniqueness: { scope: :project_id },
|
||||
length: { within: 0..255 },
|
||||
length: { maximum: 255 },
|
||||
format: { with: Gitlab::Regex.environment_name_regex,
|
||||
message: Gitlab::Regex.environment_name_regex_message }
|
||||
|
||||
|
|
|
@ -8,10 +8,18 @@ class Key < ActiveRecord::Base
|
|||
|
||||
before_validation :generate_fingerprint
|
||||
|
||||
validates :title, presence: true, length: { within: 0..255 }
|
||||
validates :key, presence: true, length: { within: 0..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' }
|
||||
validates :title,
|
||||
presence: true,
|
||||
length: { maximum: 255 }
|
||||
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
|
||||
|
||||
|
|
|
@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base
|
|||
validate :validate_branches, unless: [:allow_broken, :importing?, :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 :by_milestone, ->(milestone) { where(milestone_id: milestone) }
|
||||
scope :of_projects, ->(ids) { where(target_project_id: ids) }
|
||||
|
@ -476,6 +478,14 @@ class MergeRequest < ActiveRecord::Base
|
|||
@diff_discussions ||= self.notes.diff_notes.discussions
|
||||
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)
|
||||
notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
|
||||
return if notes.empty?
|
||||
|
@ -797,7 +807,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
|
||||
end
|
||||
|
||||
def can_be_reverted?(current_user = nil)
|
||||
def can_be_reverted?(current_user)
|
||||
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,25 +4,29 @@ class Namespace < ActiveRecord::Base
|
|||
include CacheMarkdownField
|
||||
include Sortable
|
||||
include Gitlab::ShellAdapter
|
||||
include Routable
|
||||
|
||||
cache_markdown_field :description, pipeline: :description
|
||||
|
||||
has_many :projects, dependent: :destroy
|
||||
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 :name,
|
||||
length: { within: 0..255 },
|
||||
namespace_name: 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,
|
||||
length: { within: 1..255 },
|
||||
namespace: 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
|
||||
|
||||
|
@ -86,7 +90,7 @@ class Namespace < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def to_param
|
||||
path
|
||||
full_path
|
||||
end
|
||||
|
||||
def human_name
|
||||
|
@ -150,6 +154,14 @@ class Namespace < ActiveRecord::Base
|
|||
Gitlab.config.lfs.enabled
|
||||
end
|
||||
|
||||
def full_path
|
||||
if parent
|
||||
parent.full_path + '/' + path
|
||||
else
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def repository_storage_paths
|
||||
|
@ -185,4 +197,8 @@ class Namespace < ActiveRecord::Base
|
|||
where(projects: { namespace_id: id }).
|
||||
find_each(&:refresh_members_authorized_projects)
|
||||
end
|
||||
|
||||
def full_path_changed?
|
||||
path_changed? || parent_id_changed?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,7 +99,7 @@ class Note < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def discussions
|
||||
Discussion.for_notes(all)
|
||||
Discussion.for_notes(fresh)
|
||||
end
|
||||
|
||||
def grouped_diff_discussions
|
||||
|
|
|
@ -14,6 +14,7 @@ class Project < ActiveRecord::Base
|
|||
include TokenAuthenticatable
|
||||
include ProjectFeaturesCompatibility
|
||||
include SelectForProjectAuthorization
|
||||
include Routable
|
||||
|
||||
extend Gitlab::ConfigHelper
|
||||
|
||||
|
@ -172,13 +173,13 @@ class Project < ActiveRecord::Base
|
|||
validates :description, length: { maximum: 2000 }, allow_blank: true
|
||||
validates :name,
|
||||
presence: true,
|
||||
length: { within: 0..255 },
|
||||
length: { maximum: 255 },
|
||||
format: { with: Gitlab::Regex.project_name_regex,
|
||||
message: Gitlab::Regex.project_name_regex_message }
|
||||
validates :path,
|
||||
presence: true,
|
||||
project_path: true,
|
||||
length: { within: 0..255 },
|
||||
length: { maximum: 255 },
|
||||
format: { with: Gitlab::Regex.project_path_regex,
|
||||
message: Gitlab::Regex.project_path_regex_message }
|
||||
validates :namespace, presence: true
|
||||
|
@ -324,87 +325,6 @@ class Project < ActiveRecord::Base
|
|||
non_archived.where(table[:name].matches(pattern))
|
||||
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
|
||||
Gitlab::VisibilityLevel.options
|
||||
end
|
||||
|
@ -440,6 +360,10 @@ class Project < ActiveRecord::Base
|
|||
def group_ids
|
||||
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
|
||||
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
|
||||
|
||||
def lfs_enabled?
|
||||
|
@ -879,13 +803,14 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
alias_method :human_name, :name_with_namespace
|
||||
|
||||
def path_with_namespace
|
||||
if namespace
|
||||
namespace.path + '/' + path
|
||||
def full_path
|
||||
if namespace && path
|
||||
namespace.full_path + '/' + path
|
||||
else
|
||||
path
|
||||
end
|
||||
end
|
||||
alias_method :path_with_namespace, :full_path
|
||||
|
||||
def execute_hooks(data, hooks_scope = :push_hooks)
|
||||
hooks.send(hooks_scope).each do |hook|
|
||||
|
@ -1373,4 +1298,8 @@ class Project < ActiveRecord::Base
|
|||
def validate_board_limit(board)
|
||||
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
|
||||
end
|
||||
|
||||
def full_path_changed?
|
||||
path_changed? || namespace_id_changed?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -85,12 +85,8 @@ class Repository
|
|||
# This method return true if repository contains some content visible in project page.
|
||||
#
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def commit(ref = 'HEAD')
|
||||
return nil unless exists?
|
||||
|
@ -374,12 +370,6 @@ class Repository
|
|||
return unless 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
|
||||
|
||||
def lookup_cache
|
||||
|
@ -467,7 +457,6 @@ class Repository
|
|||
# Runs code after a new branch has been created.
|
||||
def after_create_branch
|
||||
expire_branches_cache
|
||||
expire_has_visible_content_cache
|
||||
|
||||
repository_event(:push_branch)
|
||||
end
|
||||
|
@ -481,7 +470,6 @@ class Repository
|
|||
|
||||
# Runs code after an existing branch has been removed.
|
||||
def after_remove_branch
|
||||
expire_has_visible_content_cache
|
||||
expire_branches_cache
|
||||
end
|
||||
|
||||
|
@ -962,7 +950,7 @@ class Repository
|
|||
update_branch_with_hooks(user, base_branch) do
|
||||
committer = user_to_committer(user)
|
||||
source_sha = Rugged::Commit.create(rugged,
|
||||
message: commit.revert_message,
|
||||
message: commit.revert_message(user),
|
||||
author: committer,
|
||||
committer: committer,
|
||||
tree: revert_tree_id,
|
||||
|
|
|
@ -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
|
||||
|
||||
validates :author, presence: true
|
||||
validates :title, presence: true, length: { within: 0..255 }
|
||||
validates :title, presence: true, length: { maximum: 255 }
|
||||
validates :file_name,
|
||||
length: { within: 0..255 },
|
||||
length: { maximum: 255 },
|
||||
format: { with: Gitlab::Regex.file_name_regex,
|
||||
message: Gitlab::Regex.file_name_regex_message }
|
||||
|
||||
|
@ -94,6 +94,10 @@ class Snippet < ActiveRecord::Base
|
|||
0
|
||||
end
|
||||
|
||||
def file_name
|
||||
super.to_s
|
||||
end
|
||||
|
||||
# alias for compatibility with blobs and highlighting
|
||||
def path
|
||||
file_name
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Ci
|
||||
class BuildPolicy < CommitStatusPolicy
|
||||
def rules
|
||||
can! :read_build if @subject.project.public_builds?
|
||||
|
||||
super
|
||||
|
||||
# If we can't read build we should also not have that
|
||||
|
|
|
@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
|
|||
if @subject.author == @user
|
||||
can! :read_personal_snippet
|
||||
can! :update_personal_snippet
|
||||
can! :destroy_personal_snippet
|
||||
can! :admin_personal_snippet
|
||||
end
|
||||
|
||||
unless @user.external?
|
||||
can! :create_personal_snippet
|
||||
end
|
||||
|
||||
if @subject.internal? && !@user.external?
|
||||
can! :read_personal_snippet
|
||||
end
|
||||
|
|
|
@ -12,9 +12,6 @@ class ProjectPolicy < BasePolicy
|
|||
guest_access!
|
||||
public_access!
|
||||
|
||||
# Allow to read builds for internal projects
|
||||
can! :read_build if project.public_builds?
|
||||
|
||||
if project.request_access_enabled &&
|
||||
!(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
|
||||
can! :request_access
|
||||
|
@ -46,6 +43,11 @@ class ProjectPolicy < BasePolicy
|
|||
can! :create_note
|
||||
can! :upload_file
|
||||
can! :read_cycle_analytics
|
||||
|
||||
if project.public_builds?
|
||||
can! :read_pipeline
|
||||
can! :read_build
|
||||
end
|
||||
end
|
||||
|
||||
def reporter_access!
|
||||
|
|
|
@ -44,11 +44,11 @@ module Ci
|
|||
def valid_statuses_for_when(value)
|
||||
case value
|
||||
when 'on_success'
|
||||
%w[success]
|
||||
%w[success skipped]
|
||||
when 'on_failure'
|
||||
%w[failed]
|
||||
when 'always'
|
||||
%w[success failed]
|
||||
%w[success failed skipped]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
@ -34,7 +34,7 @@ module Commits
|
|||
repository.public_send(action, current_user, @commit, into, tree_id)
|
||||
success
|
||||
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."
|
||||
raise ChangeError, error_msg
|
||||
end
|
||||
|
|
|
@ -20,6 +20,10 @@ class DestroyGroupService
|
|||
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
|
||||
end
|
||||
|
||||
group.children.each do |group|
|
||||
DestroyGroupService.new(group, current_user).async_execute
|
||||
end
|
||||
|
||||
group.really_destroy!
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
module Discussions
|
||||
class BaseService < ::BaseService
|
||||
end
|
||||
end
|
|
@ -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).
|
||||
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)
|
||||
end
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
module Issues
|
||||
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)
|
||||
issue_data = issue.to_hook_data(current_user)
|
||||
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 New Issue