Merge branch 'master' into ide-pending-tab
|
@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb
|
||||||
lib/gitlab/gitaly_client/operation_service.rb
|
lib/gitlab/gitaly_client/operation_service.rb
|
||||||
lib/gitlab/background_migration/*
|
lib/gitlab/background_migration/*
|
||||||
app/models/project_services/kubernetes_service.rb
|
app/models/project_services/kubernetes_service.rb
|
||||||
|
lib/gitlab/workhorse.rb
|
||||||
|
|
|
@ -264,8 +264,17 @@ package-and-qa:
|
||||||
stage: build
|
stage: build
|
||||||
cache: {}
|
cache: {}
|
||||||
when: manual
|
when: manual
|
||||||
|
variables:
|
||||||
|
GIT_STRATEGY: none
|
||||||
|
before_script:
|
||||||
|
# We need to download the script rather than clone the repo since the
|
||||||
|
# package-and-qa job will not be able to run when the branch gets
|
||||||
|
# deleted (when merging the MR).
|
||||||
|
- apk add --update openssl
|
||||||
|
- wget https://gitlab.com/gitlab-org/gitlab-ce/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
|
||||||
|
- chmod 755 trigger-build-omnibus
|
||||||
script:
|
script:
|
||||||
- scripts/trigger-build-omnibus
|
- ./trigger-build-omnibus
|
||||||
only:
|
only:
|
||||||
- //@gitlab-org/gitlab-ce
|
- //@gitlab-org/gitlab-ce
|
||||||
- //@gitlab-org/gitlab-ee
|
- //@gitlab-org/gitlab-ee
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 10.6.2 (2018-03-29)
|
||||||
|
|
||||||
|
### Fixed (2 changes, 1 of them is from the community)
|
||||||
|
|
||||||
|
- Don't capture trailing punctuation when autolinking. !17965
|
||||||
|
- Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied. (Horatiu Eugen Vlad)
|
||||||
|
|
||||||
|
|
||||||
## 10.6.1 (2018-03-27)
|
## 10.6.1 (2018-03-27)
|
||||||
|
|
||||||
### Security (1 change)
|
### Security (1 change)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.91.0
|
0.92.0
|
||||||
|
|
14
Gemfile
|
@ -6,7 +6,6 @@ end
|
||||||
gem_versions = {}
|
gem_versions = {}
|
||||||
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
|
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
|
||||||
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
|
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
|
||||||
gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0'
|
|
||||||
gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
|
gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
|
||||||
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
|
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
|
||||||
# --- The end of special code for migrating to Rails 5.0 ---
|
# --- The end of special code for migrating to Rails 5.0 ---
|
||||||
|
@ -28,7 +27,7 @@ gem 'default_value_for', gem_versions['default_value_for']
|
||||||
gem 'mysql2', '~> 0.4.10', group: :mysql
|
gem 'mysql2', '~> 0.4.10', group: :mysql
|
||||||
gem 'pg', '~> 0.18.2', group: :postgres
|
gem 'pg', '~> 0.18.2', group: :postgres
|
||||||
|
|
||||||
gem 'rugged', '~> 0.26.0'
|
gem 'rugged', '~> 0.27'
|
||||||
gem 'grape-route-helpers', '~> 2.1.0'
|
gem 'grape-route-helpers', '~> 2.1.0'
|
||||||
|
|
||||||
gem 'faraday', '~> 0.12'
|
gem 'faraday', '~> 0.12'
|
||||||
|
@ -44,7 +43,7 @@ gem 'omniauth-cas3', '~> 1.1.4'
|
||||||
gem 'omniauth-facebook', '~> 4.0.0'
|
gem 'omniauth-facebook', '~> 4.0.0'
|
||||||
gem 'omniauth-github', '~> 1.1.1'
|
gem 'omniauth-github', '~> 1.1.1'
|
||||||
gem 'omniauth-gitlab', '~> 1.0.2'
|
gem 'omniauth-gitlab', '~> 1.0.2'
|
||||||
gem 'omniauth-google-oauth2', '~> 0.5.2'
|
gem 'omniauth-google-oauth2', '~> 0.5.3'
|
||||||
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
||||||
gem 'omniauth-oauth2-generic', '~> 0.2.2'
|
gem 'omniauth-oauth2-generic', '~> 0.2.2'
|
||||||
gem 'omniauth-saml', '~> 1.10'
|
gem 'omniauth-saml', '~> 1.10'
|
||||||
|
@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4'
|
||||||
gem 'seed-fu', '~> 2.3.7'
|
gem 'seed-fu', '~> 2.3.7'
|
||||||
|
|
||||||
# Markdown and HTML processing
|
# Markdown and HTML processing
|
||||||
gem 'html-pipeline', gem_versions['html-pipeline']
|
gem 'html-pipeline', '~> 2.7.1'
|
||||||
gem 'deckar01-task_list', '2.0.0'
|
gem 'deckar01-task_list', '2.0.0'
|
||||||
gem 'gitlab-markup', '~> 1.6.2'
|
gem 'gitlab-markup', '~> 1.6.2'
|
||||||
gem 'redcarpet', '~> 3.4'
|
gem 'redcarpet', '~> 3.4'
|
||||||
|
@ -310,7 +309,7 @@ end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'foreman', '~> 0.84.0'
|
gem 'foreman', '~> 0.84.0'
|
||||||
gem 'brakeman', '~> 3.6.0', require: false
|
gem 'brakeman', '~> 4.2', require: false
|
||||||
|
|
||||||
gem 'letter_opener_web', '~> 1.3.0'
|
gem 'letter_opener_web', '~> 1.3.0'
|
||||||
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
|
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
|
||||||
|
@ -376,6 +375,8 @@ group :development, :test do
|
||||||
gem 'stackprof', '~> 0.2.10', require: false
|
gem 'stackprof', '~> 0.2.10', require: false
|
||||||
|
|
||||||
gem 'simple_po_parser', '~> 1.1.2', require: false
|
gem 'simple_po_parser', '~> 1.1.2', require: false
|
||||||
|
|
||||||
|
gem 'timecop', '~> 0.8.0'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
@ -385,7 +386,6 @@ group :test do
|
||||||
gem 'webmock', '~> 2.3.2'
|
gem 'webmock', '~> 2.3.2'
|
||||||
gem 'test_after_commit', '~> 1.1'
|
gem 'test_after_commit', '~> 1.1'
|
||||||
gem 'sham_rack', '~> 1.3.6'
|
gem 'sham_rack', '~> 1.3.6'
|
||||||
gem 'timecop', '~> 0.8.0'
|
|
||||||
gem 'concurrent-ruby', '~> 1.0.5'
|
gem 'concurrent-ruby', '~> 1.0.5'
|
||||||
gem 'test-prof', '~> 0.2.5'
|
gem 'test-prof', '~> 0.2.5'
|
||||||
end
|
end
|
||||||
|
@ -421,7 +421,7 @@ group :ed25519 do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gitaly GRPC client
|
# Gitaly GRPC client
|
||||||
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
|
gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
|
||||||
gem 'grpc', '~> 1.10.0'
|
gem 'grpc', '~> 1.10.0'
|
||||||
|
|
||||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||||
|
|
33
Gemfile.lock
|
@ -95,7 +95,7 @@ GEM
|
||||||
autoprefixer-rails (>= 5.2.1)
|
autoprefixer-rails (>= 5.2.1)
|
||||||
sass (>= 3.3.4)
|
sass (>= 3.3.4)
|
||||||
bootstrap_form (2.7.0)
|
bootstrap_form (2.7.0)
|
||||||
brakeman (3.6.1)
|
brakeman (4.2.1)
|
||||||
browser (2.2.0)
|
browser (2.2.0)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.5.1)
|
bullet (5.5.1)
|
||||||
|
@ -120,7 +120,7 @@ GEM
|
||||||
activesupport (>= 4.0.0)
|
activesupport (>= 4.0.0)
|
||||||
mime-types (>= 1.16)
|
mime-types (>= 1.16)
|
||||||
cause (0.1)
|
cause (0.1)
|
||||||
charlock_holmes (0.7.5)
|
charlock_holmes (0.7.6)
|
||||||
childprocess (0.7.0)
|
childprocess (0.7.0)
|
||||||
ffi (~> 1.0, >= 1.0.11)
|
ffi (~> 1.0, >= 1.0.11)
|
||||||
chronic (0.10.2)
|
chronic (0.10.2)
|
||||||
|
@ -290,7 +290,7 @@ GEM
|
||||||
po_to_json (>= 1.0.0)
|
po_to_json (>= 1.0.0)
|
||||||
rails (>= 3.2.0)
|
rails (>= 3.2.0)
|
||||||
gherkin-ruby (0.3.2)
|
gherkin-ruby (0.3.2)
|
||||||
gitaly-proto (0.88.0)
|
gitaly-proto (0.91.0)
|
||||||
google-protobuf (~> 3.1)
|
google-protobuf (~> 3.1)
|
||||||
grpc (~> 1.0)
|
grpc (~> 1.0)
|
||||||
github-linguist (5.3.3)
|
github-linguist (5.3.3)
|
||||||
|
@ -399,9 +399,9 @@ GEM
|
||||||
hipchat (1.5.2)
|
hipchat (1.5.2)
|
||||||
httparty
|
httparty
|
||||||
mimemagic
|
mimemagic
|
||||||
html-pipeline (1.11.0)
|
html-pipeline (2.7.1)
|
||||||
activesupport (>= 2)
|
activesupport (>= 2)
|
||||||
nokogiri (~> 1.4)
|
nokogiri (>= 1.4)
|
||||||
html2text (0.2.0)
|
html2text (0.2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
|
@ -550,11 +550,10 @@ GEM
|
||||||
omniauth-gitlab (1.0.2)
|
omniauth-gitlab (1.0.2)
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.0)
|
||||||
omniauth-oauth2 (~> 1.0)
|
omniauth-oauth2 (~> 1.0)
|
||||||
omniauth-google-oauth2 (0.5.2)
|
omniauth-google-oauth2 (0.5.3)
|
||||||
jwt (~> 1.5)
|
jwt (>= 1.5)
|
||||||
multi_json (~> 1.3)
|
|
||||||
omniauth (>= 1.1.1)
|
omniauth (>= 1.1.1)
|
||||||
omniauth-oauth2 (>= 1.3.1)
|
omniauth-oauth2 (>= 1.5)
|
||||||
omniauth-jwt (0.0.2)
|
omniauth-jwt (0.0.2)
|
||||||
jwt
|
jwt
|
||||||
omniauth (~> 1.1)
|
omniauth (~> 1.1)
|
||||||
|
@ -566,8 +565,8 @@ GEM
|
||||||
omniauth-oauth (1.1.0)
|
omniauth-oauth (1.1.0)
|
||||||
oauth
|
oauth
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.0)
|
||||||
omniauth-oauth2 (1.4.0)
|
omniauth-oauth2 (1.5.0)
|
||||||
oauth2 (~> 1.0)
|
oauth2 (~> 1.1)
|
||||||
omniauth (~> 1.2)
|
omniauth (~> 1.2)
|
||||||
omniauth-oauth2-generic (0.2.2)
|
omniauth-oauth2-generic (0.2.2)
|
||||||
omniauth-oauth2 (~> 1.0)
|
omniauth-oauth2 (~> 1.0)
|
||||||
|
@ -814,7 +813,7 @@ GEM
|
||||||
rubyzip (1.2.1)
|
rubyzip (1.2.1)
|
||||||
rufus-scheduler (3.4.0)
|
rufus-scheduler (3.4.0)
|
||||||
et-orbi (~> 1.0)
|
et-orbi (~> 1.0)
|
||||||
rugged (0.26.0)
|
rugged (0.27.0)
|
||||||
safe_yaml (1.0.4)
|
safe_yaml (1.0.4)
|
||||||
sanitize (2.1.0)
|
sanitize (2.1.0)
|
||||||
nokogiri (>= 1.4.4)
|
nokogiri (>= 1.4.4)
|
||||||
|
@ -1013,7 +1012,7 @@ DEPENDENCIES
|
||||||
binding_of_caller (~> 0.7.2)
|
binding_of_caller (~> 0.7.2)
|
||||||
bootstrap-sass (~> 3.3.0)
|
bootstrap-sass (~> 3.3.0)
|
||||||
bootstrap_form (~> 2.7.0)
|
bootstrap_form (~> 2.7.0)
|
||||||
brakeman (~> 3.6.0)
|
brakeman (~> 4.2)
|
||||||
browser (~> 2.2)
|
browser (~> 2.2)
|
||||||
bullet (~> 5.5.0)
|
bullet (~> 5.5.0)
|
||||||
bundler-audit (~> 0.5.0)
|
bundler-audit (~> 0.5.0)
|
||||||
|
@ -1062,7 +1061,7 @@ DEPENDENCIES
|
||||||
gettext (~> 3.2.2)
|
gettext (~> 3.2.2)
|
||||||
gettext_i18n_rails (~> 1.8.0)
|
gettext_i18n_rails (~> 1.8.0)
|
||||||
gettext_i18n_rails_js (~> 1.3)
|
gettext_i18n_rails_js (~> 1.3)
|
||||||
gitaly-proto (~> 0.88.0)
|
gitaly-proto (~> 0.91.0)
|
||||||
github-linguist (~> 5.3.3)
|
github-linguist (~> 5.3.3)
|
||||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||||
gitlab-markup (~> 1.6.2)
|
gitlab-markup (~> 1.6.2)
|
||||||
|
@ -1084,7 +1083,7 @@ DEPENDENCIES
|
||||||
hashie-forbidden_attributes
|
hashie-forbidden_attributes
|
||||||
health_check (~> 2.6.0)
|
health_check (~> 2.6.0)
|
||||||
hipchat (~> 1.5.0)
|
hipchat (~> 1.5.0)
|
||||||
html-pipeline (~> 1.11.0)
|
html-pipeline (~> 2.7.1)
|
||||||
html2text
|
html2text
|
||||||
httparty (~> 0.13.3)
|
httparty (~> 0.13.3)
|
||||||
influxdb (~> 0.2)
|
influxdb (~> 0.2)
|
||||||
|
@ -1118,7 +1117,7 @@ DEPENDENCIES
|
||||||
omniauth-facebook (~> 4.0.0)
|
omniauth-facebook (~> 4.0.0)
|
||||||
omniauth-github (~> 1.1.1)
|
omniauth-github (~> 1.1.1)
|
||||||
omniauth-gitlab (~> 1.0.2)
|
omniauth-gitlab (~> 1.0.2)
|
||||||
omniauth-google-oauth2 (~> 0.5.2)
|
omniauth-google-oauth2 (~> 0.5.3)
|
||||||
omniauth-jwt (~> 0.0.2)
|
omniauth-jwt (~> 0.0.2)
|
||||||
omniauth-kerberos (~> 0.3.0)
|
omniauth-kerberos (~> 0.3.0)
|
||||||
omniauth-oauth2-generic (~> 0.2.2)
|
omniauth-oauth2-generic (~> 0.2.2)
|
||||||
|
@ -1174,7 +1173,7 @@ DEPENDENCIES
|
||||||
ruby-prof (~> 0.17.0)
|
ruby-prof (~> 0.17.0)
|
||||||
ruby_parser (~> 3.8)
|
ruby_parser (~> 3.8)
|
||||||
rufus-scheduler (~> 3.4)
|
rufus-scheduler (~> 3.4)
|
||||||
rugged (~> 0.26.0)
|
rugged (~> 0.27)
|
||||||
sanitize (~> 2.0)
|
sanitize (~> 2.0)
|
||||||
sass-rails (~> 5.0.6)
|
sass-rails (~> 5.0.6)
|
||||||
scss_lint (~> 0.56.0)
|
scss_lint (~> 0.56.0)
|
||||||
|
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.3 KiB |
|
@ -10,6 +10,9 @@ const Api = {
|
||||||
projectsPath: '/api/:version/projects.json',
|
projectsPath: '/api/:version/projects.json',
|
||||||
projectPath: '/api/:version/projects/:id',
|
projectPath: '/api/:version/projects/:id',
|
||||||
projectLabelsPath: '/:namespace_path/:project_path/labels',
|
projectLabelsPath: '/:namespace_path/:project_path/labels',
|
||||||
|
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
|
||||||
|
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
|
||||||
|
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
|
||||||
groupLabelsPath: '/groups/:namespace_path/-/labels',
|
groupLabelsPath: '/groups/:namespace_path/-/labels',
|
||||||
licensePath: '/api/:version/templates/licenses/:key',
|
licensePath: '/api/:version/templates/licenses/:key',
|
||||||
gitignorePath: '/api/:version/templates/gitignores/:key',
|
gitignorePath: '/api/:version/templates/gitignores/:key',
|
||||||
|
@ -22,10 +25,8 @@ const Api = {
|
||||||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||||
|
|
||||||
group(groupId, callback) {
|
group(groupId, callback) {
|
||||||
const url = Api.buildUrl(Api.groupPath)
|
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||||
.replace(':id', groupId);
|
return axios.get(url).then(({ data }) => {
|
||||||
return axios.get(url)
|
|
||||||
.then(({ data }) => {
|
|
||||||
callback(data);
|
callback(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -35,11 +36,15 @@ const Api = {
|
||||||
// Return groups list. Filtered by query
|
// Return groups list. Filtered by query
|
||||||
groups(query, options, callback = $.noop) {
|
groups(query, options, callback = $.noop) {
|
||||||
const url = Api.buildUrl(Api.groupsPath);
|
const url = Api.buildUrl(Api.groupsPath);
|
||||||
return axios.get(url, {
|
return axios
|
||||||
params: Object.assign({
|
.get(url, {
|
||||||
|
params: Object.assign(
|
||||||
|
{
|
||||||
search: query,
|
search: query,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
}, options),
|
},
|
||||||
|
options,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
callback(data);
|
callback(data);
|
||||||
|
@ -51,7 +56,8 @@ const Api = {
|
||||||
// Return namespaces list. Filtered by query
|
// Return namespaces list. Filtered by query
|
||||||
namespaces(query, callback) {
|
namespaces(query, callback) {
|
||||||
const url = Api.buildUrl(Api.namespacesPath);
|
const url = Api.buildUrl(Api.namespacesPath);
|
||||||
return axios.get(url, {
|
return axios
|
||||||
|
.get(url, {
|
||||||
params: {
|
params: {
|
||||||
search: query,
|
search: query,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
|
@ -73,7 +79,8 @@ const Api = {
|
||||||
defaults.membership = true;
|
defaults.membership = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.get(url, {
|
return axios
|
||||||
|
.get(url, {
|
||||||
params: Object.assign(defaults, options),
|
params: Object.assign(defaults, options),
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
|
@ -85,8 +92,32 @@ const Api = {
|
||||||
|
|
||||||
// Return single project
|
// Return single project
|
||||||
project(projectPath) {
|
project(projectPath) {
|
||||||
const url = Api.buildUrl(Api.projectPath)
|
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
|
||||||
.replace(':id', encodeURIComponent(projectPath));
|
|
||||||
|
return axios.get(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Return Merge Request for project
|
||||||
|
mergeRequest(projectPath, mergeRequestId) {
|
||||||
|
const url = Api.buildUrl(Api.mergeRequestPath)
|
||||||
|
.replace(':id', encodeURIComponent(projectPath))
|
||||||
|
.replace(':mrid', mergeRequestId);
|
||||||
|
|
||||||
|
return axios.get(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeRequestChanges(projectPath, mergeRequestId) {
|
||||||
|
const url = Api.buildUrl(Api.mergeRequestChangesPath)
|
||||||
|
.replace(':id', encodeURIComponent(projectPath))
|
||||||
|
.replace(':mrid', mergeRequestId);
|
||||||
|
|
||||||
|
return axios.get(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeRequestVersions(projectPath, mergeRequestId) {
|
||||||
|
const url = Api.buildUrl(Api.mergeRequestVersionsPath)
|
||||||
|
.replace(':id', encodeURIComponent(projectPath))
|
||||||
|
.replace(':mrid', mergeRequestId);
|
||||||
|
|
||||||
return axios.get(url);
|
return axios.get(url);
|
||||||
},
|
},
|
||||||
|
@ -102,7 +133,8 @@ const Api = {
|
||||||
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
|
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.post(url, {
|
return axios
|
||||||
|
.post(url, {
|
||||||
label: data,
|
label: data,
|
||||||
})
|
})
|
||||||
.then(res => callback(res.data))
|
.then(res => callback(res.data))
|
||||||
|
@ -111,9 +143,9 @@ const Api = {
|
||||||
|
|
||||||
// Return group projects list. Filtered by query
|
// Return group projects list. Filtered by query
|
||||||
groupProjects(groupId, query, callback) {
|
groupProjects(groupId, query, callback) {
|
||||||
const url = Api.buildUrl(Api.groupProjectsPath)
|
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
|
||||||
.replace(':id', groupId);
|
return axios
|
||||||
return axios.get(url, {
|
.get(url, {
|
||||||
params: {
|
params: {
|
||||||
search: query,
|
search: query,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
|
@ -124,8 +156,7 @@ const Api = {
|
||||||
|
|
||||||
commitMultiple(id, data) {
|
commitMultiple(id, data) {
|
||||||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||||
const url = Api.buildUrl(Api.commitPath)
|
const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
|
||||||
.replace(':id', encodeURIComponent(id));
|
|
||||||
return axios.post(url, JSON.stringify(data), {
|
return axios.post(url, JSON.stringify(data), {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
@ -136,39 +167,34 @@ const Api = {
|
||||||
branchSingle(id, branch) {
|
branchSingle(id, branch) {
|
||||||
const url = Api.buildUrl(Api.branchSinglePath)
|
const url = Api.buildUrl(Api.branchSinglePath)
|
||||||
.replace(':id', encodeURIComponent(id))
|
.replace(':id', encodeURIComponent(id))
|
||||||
.replace(':branch', branch);
|
.replace(':branch', encodeURIComponent(branch));
|
||||||
|
|
||||||
return axios.get(url);
|
return axios.get(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Return text for a specific license
|
// Return text for a specific license
|
||||||
licenseText(key, data, callback) {
|
licenseText(key, data, callback) {
|
||||||
const url = Api.buildUrl(Api.licensePath)
|
const url = Api.buildUrl(Api.licensePath).replace(':key', key);
|
||||||
.replace(':key', key);
|
return axios
|
||||||
return axios.get(url, {
|
.get(url, {
|
||||||
params: data,
|
params: data,
|
||||||
})
|
})
|
||||||
.then(res => callback(res.data));
|
.then(res => callback(res.data));
|
||||||
},
|
},
|
||||||
|
|
||||||
gitignoreText(key, callback) {
|
gitignoreText(key, callback) {
|
||||||
const url = Api.buildUrl(Api.gitignorePath)
|
const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
|
||||||
.replace(':key', key);
|
return axios.get(url).then(({ data }) => callback(data));
|
||||||
return axios.get(url)
|
|
||||||
.then(({ data }) => callback(data));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
gitlabCiYml(key, callback) {
|
gitlabCiYml(key, callback) {
|
||||||
const url = Api.buildUrl(Api.gitlabCiYmlPath)
|
const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
|
||||||
.replace(':key', key);
|
return axios.get(url).then(({ data }) => callback(data));
|
||||||
return axios.get(url)
|
|
||||||
.then(({ data }) => callback(data));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
dockerfileYml(key, callback) {
|
dockerfileYml(key, callback) {
|
||||||
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
|
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
|
||||||
return axios.get(url)
|
return axios.get(url).then(({ data }) => callback(data));
|
||||||
.then(({ data }) => callback(data));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
issueTemplate(namespacePath, projectPath, key, type, callback) {
|
issueTemplate(namespacePath, projectPath, key, type, callback) {
|
||||||
|
@ -177,7 +203,8 @@ const Api = {
|
||||||
.replace(':type', type)
|
.replace(':type', type)
|
||||||
.replace(':project_path', projectPath)
|
.replace(':project_path', projectPath)
|
||||||
.replace(':namespace_path', namespacePath);
|
.replace(':namespace_path', namespacePath);
|
||||||
return axios.get(url)
|
return axios
|
||||||
|
.get(url)
|
||||||
.then(({ data }) => callback(null, data))
|
.then(({ data }) => callback(null, data))
|
||||||
.catch(callback);
|
.catch(callback);
|
||||||
},
|
},
|
||||||
|
@ -185,10 +212,13 @@ const Api = {
|
||||||
users(query, options) {
|
users(query, options) {
|
||||||
const url = Api.buildUrl(this.usersPath);
|
const url = Api.buildUrl(this.usersPath);
|
||||||
return axios.get(url, {
|
return axios.get(url, {
|
||||||
params: Object.assign({
|
params: Object.assign(
|
||||||
|
{
|
||||||
search: query,
|
search: query,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
}, options),
|
},
|
||||||
|
options,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default function renderMath($els) {
|
||||||
if (!$els.length) return;
|
if (!$els.length) return;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
import(/* webpackChunkName: 'katex' */ 'katex'),
|
import(/* webpackChunkName: 'katex' */ 'katex'),
|
||||||
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'),
|
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
|
||||||
]).then(([katex]) => {
|
]).then(([katex]) => {
|
||||||
renderWithKaTeX($els, katex);
|
renderWithKaTeX($els, katex);
|
||||||
}).catch(() => flash(__('An error occurred while rendering KaTeX')));
|
}).catch(() => flash(__('An error occurred while rendering KaTeX')));
|
||||||
|
|
|
@ -54,6 +54,7 @@ class GfmAutoComplete {
|
||||||
alias: 'commands',
|
alias: 'commands',
|
||||||
searchKey: 'search',
|
searchKey: 'search',
|
||||||
skipSpecialCharacterTest: true,
|
skipSpecialCharacterTest: true,
|
||||||
|
skipMarkdownCharacterTest: true,
|
||||||
data: GfmAutoComplete.defaultLoadingData,
|
data: GfmAutoComplete.defaultLoadingData,
|
||||||
displayTpl(value) {
|
displayTpl(value) {
|
||||||
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
|
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
|
||||||
|
@ -376,15 +377,23 @@ class GfmAutoComplete {
|
||||||
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
||||||
},
|
},
|
||||||
beforeInsert(value) {
|
beforeInsert(value) {
|
||||||
let resultantValue = value;
|
let withoutAt = value.substring(1);
|
||||||
|
const at = value.charAt();
|
||||||
|
|
||||||
if (value && !this.setting.skipSpecialCharacterTest) {
|
if (value && !this.setting.skipSpecialCharacterTest) {
|
||||||
const withoutAt = value.substring(1);
|
const regex = at === '~' ? /\W|^\d+$/ : /\W/;
|
||||||
const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
|
|
||||||
if (withoutAt && regex.test(withoutAt)) {
|
if (withoutAt && regex.test(withoutAt)) {
|
||||||
resultantValue = `${value.charAt()}"${withoutAt}"`;
|
withoutAt = `"${withoutAt}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resultantValue;
|
|
||||||
|
// We can ignore this for quick actions because they are processed
|
||||||
|
// before Markdown.
|
||||||
|
if (!this.setting.skipMarkdownCharacterTest) {
|
||||||
|
withoutAt = withoutAt.replace(/([~\-_*`])/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${at}${withoutAt}`;
|
||||||
},
|
},
|
||||||
matcher(flag, subtext) {
|
matcher(flag, subtext) {
|
||||||
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
|
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import { __, sprintf } from '~/locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -11,6 +12,11 @@
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
mergeRequestId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
viewer: {
|
viewer: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -20,6 +26,13 @@
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
mergeReviewLine() {
|
||||||
|
return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
|
||||||
|
mergeRequestId: this.mergeRequestId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeMode(mode) {
|
changeMode(mode) {
|
||||||
this.$emit('click', mode);
|
this.$emit('click', mode);
|
||||||
|
@ -43,7 +56,10 @@
|
||||||
}"
|
}"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
>
|
>
|
||||||
<template v-if="viewer === 'editor'">
|
<template v-if="viewer === 'mrdiff' && mergeRequestId">
|
||||||
|
{{ mergeReviewLine }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="viewer === 'editor'">
|
||||||
{{ __('Editing') }}
|
{{ __('Editing') }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -57,6 +73,29 @@
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
|
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
|
||||||
<ul>
|
<ul>
|
||||||
|
<template v-if="mergeRequestId">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
@click.prevent="changeMode('mrdiff')"
|
||||||
|
:class="{
|
||||||
|
'is-active': viewer === 'mrdiff',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<strong class="dropdown-menu-inner-title">
|
||||||
|
{{ mergeReviewLine }}
|
||||||
|
</strong>
|
||||||
|
<span class="dropdown-menu-inner-content">
|
||||||
|
{{ __('Compare changes with the merge request target branch') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
role="separator"
|
||||||
|
class="divider"
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['changedFiles', 'openFiles', 'viewer']),
|
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
|
||||||
...mapGetters(['activeFile', 'hasChanges']),
|
...mapGetters(['activeFile', 'hasChanges']),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -64,6 +64,7 @@ export default {
|
||||||
:files="openFiles"
|
:files="openFiles"
|
||||||
:viewer="viewer"
|
:viewer="viewer"
|
||||||
:has-changes="hasChanges"
|
:has-changes="hasChanges"
|
||||||
|
:merge-request-id="currentMergeRequestId"
|
||||||
/>
|
/>
|
||||||
<repo-editor
|
<repo-editor
|
||||||
class="multi-file-edit-pane-content"
|
class="multi-file-edit-pane-content"
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
import icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import tooltip from '~/vue_shared/directives/tooltip';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
icon,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<icon
|
||||||
|
name="git-merge"
|
||||||
|
v-tooltip
|
||||||
|
title="__('Part of merge request changes')"
|
||||||
|
css-classes="ide-file-changed-icon"
|
||||||
|
:size="12"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
/* global monaco */
|
/* global monaco */
|
||||||
import { mapState, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import flash from '~/flash';
|
import flash from '~/flash';
|
||||||
import monacoLoader from '../monaco_loader';
|
import monacoLoader from '../monaco_loader';
|
||||||
import Editor from '../lib/editor';
|
import Editor from '../lib/editor';
|
||||||
|
@ -13,12 +13,8 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
|
||||||
'leftPanelCollapsed',
|
...mapGetters(['currentMergeRequest']),
|
||||||
'rightPanelCollapsed',
|
|
||||||
'viewer',
|
|
||||||
'delayViewerUpdated',
|
|
||||||
]),
|
|
||||||
shouldHideEditor() {
|
shouldHideEditor() {
|
||||||
return this.file && this.file.binary && !this.file.raw;
|
return this.file && this.file.binary && !this.file.raw;
|
||||||
},
|
},
|
||||||
|
@ -68,7 +64,10 @@ export default {
|
||||||
|
|
||||||
this.editor.clearEditor();
|
this.editor.clearEditor();
|
||||||
|
|
||||||
this.getRawFileData(this.file)
|
this.getRawFileData({
|
||||||
|
path: this.file.path,
|
||||||
|
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const viewerPromise = this.delayViewerUpdated
|
const viewerPromise = this.delayViewerUpdated
|
||||||
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
|
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
|
||||||
|
@ -81,14 +80,7 @@ export default {
|
||||||
this.createEditorInstance();
|
this.createEditorInstance();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
flash(
|
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
|
||||||
'Error setting up monaco. Please try again.',
|
|
||||||
'alert',
|
|
||||||
document,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -110,7 +102,11 @@ export default {
|
||||||
|
|
||||||
this.model = this.editor.createModel(this.file);
|
this.model = this.editor.createModel(this.file);
|
||||||
|
|
||||||
|
if (this.viewer === 'mrdiff') {
|
||||||
|
this.editor.attachMergeRequestModel(this.model);
|
||||||
|
} else {
|
||||||
this.editor.attachModel(this.model);
|
this.editor.attachModel(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
this.model.onChange(model => {
|
this.model.onChange(model => {
|
||||||
const { file } = model;
|
const { file } = model;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import router from '../ide_router';
|
||||||
import newDropdown from './new_dropdown/index.vue';
|
import newDropdown from './new_dropdown/index.vue';
|
||||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||||
import changedFileIcon from './changed_file_icon.vue';
|
import changedFileIcon from './changed_file_icon.vue';
|
||||||
|
import mrFileIcon from './mr_file_icon.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RepoFile',
|
name: 'RepoFile',
|
||||||
|
@ -15,6 +16,7 @@ export default {
|
||||||
fileStatusIcon,
|
fileStatusIcon,
|
||||||
fileIcon,
|
fileIcon,
|
||||||
changedFileIcon,
|
changedFileIcon,
|
||||||
|
mrFileIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
file: {
|
file: {
|
||||||
|
@ -56,10 +58,7 @@ export default {
|
||||||
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
|
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
|
||||||
clickFile() {
|
clickFile() {
|
||||||
// Manual Action if a tree is selected/opened
|
// Manual Action if a tree is selected/opened
|
||||||
if (
|
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
|
||||||
this.isTree &&
|
|
||||||
this.$router.currentRoute.path === `/project${this.file.url}`
|
|
||||||
) {
|
|
||||||
this.toggleTreeOpen(this.file.path);
|
this.toggleTreeOpen(this.file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,11 +97,15 @@ export default {
|
||||||
:file="file"
|
:file="file"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="pull-right">
|
||||||
|
<mr-file-icon
|
||||||
|
v-if="file.mrChange"
|
||||||
|
/>
|
||||||
<changed-file-icon
|
<changed-file-icon
|
||||||
:file="file"
|
:file="file"
|
||||||
v-if="file.changed || file.tempFile"
|
v-if="file.changed || file.tempFile"
|
||||||
class="prepend-top-5 pull-right"
|
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<new-dropdown
|
<new-dropdown
|
||||||
v-if="isTree"
|
v-if="isTree"
|
||||||
:project-id="file.projectId"
|
:project-id="file.projectId"
|
||||||
|
|
|
@ -26,6 +26,11 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
mergeRequestId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -70,6 +75,7 @@ export default {
|
||||||
:viewer="viewer"
|
:viewer="viewer"
|
||||||
:show-shadow="showShadow"
|
:show-shadow="showShadow"
|
||||||
:has-changes="hasChanges"
|
:has-changes="hasChanges"
|
||||||
|
:merge-request-id="mergeRequestId"
|
||||||
@click="openFileViewer"
|
@click="openFileViewer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const router = new VueRouter({
|
||||||
component: EmptyRouterComponent,
|
component: EmptyRouterComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mr/:mrid',
|
path: 'merge_requests/:mrid',
|
||||||
component: EmptyRouterComponent,
|
component: EmptyRouterComponent,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -98,6 +98,60 @@ router.beforeEach((to, from, next) => {
|
||||||
);
|
);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
} else if (to.params.mrid) {
|
||||||
|
store.dispatch('updateViewer', 'mrdiff');
|
||||||
|
|
||||||
|
store
|
||||||
|
.dispatch('getMergeRequestData', {
|
||||||
|
projectId: fullProjectId,
|
||||||
|
mergeRequestId: to.params.mrid,
|
||||||
|
})
|
||||||
|
.then(mr => {
|
||||||
|
store.dispatch('getBranchData', {
|
||||||
|
projectId: fullProjectId,
|
||||||
|
branchId: mr.source_branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
return store.dispatch('getFiles', {
|
||||||
|
projectId: fullProjectId,
|
||||||
|
branchId: mr.source_branch,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
store.dispatch('getMergeRequestVersions', {
|
||||||
|
projectId: fullProjectId,
|
||||||
|
mergeRequestId: to.params.mrid,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(() =>
|
||||||
|
store.dispatch('getMergeRequestChanges', {
|
||||||
|
projectId: fullProjectId,
|
||||||
|
mergeRequestId: to.params.mrid,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(mrChanges => {
|
||||||
|
mrChanges.changes.forEach((change, ind) => {
|
||||||
|
const changeTreeEntry = store.state.entries[change.new_path];
|
||||||
|
|
||||||
|
if (changeTreeEntry) {
|
||||||
|
store.dispatch('setFileMrChange', {
|
||||||
|
file: changeTreeEntry,
|
||||||
|
mrChange: change,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ind < 10) {
|
||||||
|
store.dispatch('getFileData', {
|
||||||
|
path: change.new_path,
|
||||||
|
makeFileActive: ind === 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
flash('Error while loading the merge request. Please try again.');
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
|
|
@ -21,6 +21,15 @@ export default class Model {
|
||||||
new this.monaco.Uri(null, null, this.file.key),
|
new this.monaco.Uri(null, null, this.file.key),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
if (this.file.mrChange) {
|
||||||
|
this.disposable.add(
|
||||||
|
(this.baseModel = this.monaco.editor.createModel(
|
||||||
|
this.file.baseRaw,
|
||||||
|
undefined,
|
||||||
|
new this.monaco.Uri(null, null, `target/${this.file.path}`),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.events = new Map();
|
this.events = new Map();
|
||||||
|
|
||||||
|
@ -55,6 +64,10 @@ export default class Model {
|
||||||
return this.originalModel;
|
return this.originalModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBaseModel() {
|
||||||
|
return this.baseModel;
|
||||||
|
}
|
||||||
|
|
||||||
setValue(value) {
|
setValue(value) {
|
||||||
this.getModel().setValue(value);
|
this.getModel().setValue(value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,11 +109,19 @@ export default class Editor {
|
||||||
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
|
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachMergeRequestModel(model) {
|
||||||
|
this.instance.setModel({
|
||||||
|
original: model.getBaseModel(),
|
||||||
|
modified: model.getModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.monaco.editor.createDiffNavigator(this.instance, {
|
||||||
|
alwaysRevealFirst: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setupMonacoTheme() {
|
setupMonacoTheme() {
|
||||||
this.monaco.editor.defineTheme(
|
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
|
||||||
gitlabTheme.themeName,
|
|
||||||
gitlabTheme.monacoTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.monaco.editor.setTheme('gitlab');
|
this.monaco.editor.setTheme('gitlab');
|
||||||
}
|
}
|
||||||
|
@ -161,8 +169,6 @@ export default class Editor {
|
||||||
onPositionChange(cb) {
|
onPositionChange(cb) {
|
||||||
if (!this.instance.onDidChangeCursorPosition) return;
|
if (!this.instance.onDidChangeCursorPosition) return;
|
||||||
|
|
||||||
this.disposable.add(
|
this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
|
||||||
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,35 @@ export default {
|
||||||
return Promise.resolve(file.raw);
|
return Promise.resolve(file.raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
|
return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
|
||||||
|
},
|
||||||
|
getBaseRawFileData(file, sha) {
|
||||||
|
if (file.tempFile) {
|
||||||
|
return Promise.resolve(file.baseRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.baseRaw) {
|
||||||
|
return Promise.resolve(file.baseRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Vue.http
|
||||||
|
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
|
||||||
|
params: { format: 'json' },
|
||||||
|
})
|
||||||
.then(res => res.text());
|
.then(res => res.text());
|
||||||
},
|
},
|
||||||
getProjectData(namespace, project) {
|
getProjectData(namespace, project) {
|
||||||
return Api.project(`${namespace}/${project}`);
|
return Api.project(`${namespace}/${project}`);
|
||||||
},
|
},
|
||||||
|
getProjectMergeRequestData(projectId, mergeRequestId) {
|
||||||
|
return Api.mergeRequest(projectId, mergeRequestId);
|
||||||
|
},
|
||||||
|
getProjectMergeRequestChanges(projectId, mergeRequestId) {
|
||||||
|
return Api.mergeRequestChanges(projectId, mergeRequestId);
|
||||||
|
},
|
||||||
|
getProjectMergeRequestVersions(projectId, mergeRequestId) {
|
||||||
|
return Api.mergeRequestVersions(projectId, mergeRequestId);
|
||||||
|
},
|
||||||
getBranchData(projectId, currentBranchId) {
|
getBranchData(projectId, currentBranchId) {
|
||||||
return Api.branchSingle(projectId, currentBranchId);
|
return Api.branchSingle(projectId, currentBranchId);
|
||||||
},
|
},
|
||||||
|
|
|
@ -115,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
|
||||||
export * from './actions/tree';
|
export * from './actions/tree';
|
||||||
export * from './actions/file';
|
export * from './actions/file';
|
||||||
export * from './actions/project';
|
export * from './actions/project';
|
||||||
|
export * from './actions/merge_request';
|
||||||
|
|
|
@ -56,22 +56,21 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
|
||||||
commit(types.SET_CURRENT_BRANCH, file.branchId);
|
commit(types.SET_CURRENT_BRANCH, file.branchId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFileData = ({ state, commit, dispatch }, file) => {
|
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
|
||||||
|
const file = state.entries[path];
|
||||||
commit(types.TOGGLE_LOADING, { entry: file });
|
commit(types.TOGGLE_LOADING, { entry: file });
|
||||||
|
|
||||||
return service
|
return service
|
||||||
.getFileData(file.url)
|
.getFileData(file.url)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||||
|
|
||||||
setPageTitle(pageTitle);
|
setPageTitle(pageTitle);
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
commit(types.SET_FILE_DATA, { data, file });
|
commit(types.SET_FILE_DATA, { data, file });
|
||||||
commit(types.TOGGLE_FILE_OPEN, file.path);
|
commit(types.TOGGLE_FILE_OPEN, path);
|
||||||
dispatch('setFileActive', file.path);
|
if (makeFileActive) dispatch('setFileActive', path);
|
||||||
commit(types.TOGGLE_LOADING, { entry: file });
|
commit(types.TOGGLE_LOADING, { entry: file });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -80,15 +79,40 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRawFileData = ({ commit, dispatch }, file) =>
|
export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
|
||||||
|
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
|
||||||
|
const file = state.entries[path];
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
service
|
service
|
||||||
.getRawFileData(file)
|
.getRawFileData(file)
|
||||||
.then(raw => {
|
.then(raw => {
|
||||||
commit(types.SET_FILE_RAW_DATA, { file, raw });
|
commit(types.SET_FILE_RAW_DATA, { file, raw });
|
||||||
|
if (file.mrChange && file.mrChange.new_file === false) {
|
||||||
|
service
|
||||||
|
.getBaseRawFileData(file, baseSha)
|
||||||
|
.then(baseRaw => {
|
||||||
|
commit(types.SET_FILE_BASE_RAW_DATA, {
|
||||||
|
file,
|
||||||
|
baseRaw,
|
||||||
|
});
|
||||||
|
resolve(raw);
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(e => {
|
||||||
flash('Error loading file content. Please try again.', 'alert', document, null, false, true),
|
reject(e);
|
||||||
);
|
});
|
||||||
|
} else {
|
||||||
|
resolve(raw);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
flash('Error loading file content. Please try again.');
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const changeFileContent = ({ state, commit }, { path, content }) => {
|
export const changeFileContent = ({ state, commit }, { path, content }) => {
|
||||||
const file = state.entries[path];
|
const file = state.entries[path];
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import flash from '~/flash';
|
||||||
|
import service from '../../services';
|
||||||
|
import * as types from '../mutation_types';
|
||||||
|
|
||||||
|
export const getMergeRequestData = (
|
||||||
|
{ commit, state, dispatch },
|
||||||
|
{ projectId, mergeRequestId, force = false } = {},
|
||||||
|
) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
|
||||||
|
service
|
||||||
|
.getProjectMergeRequestData(projectId, mergeRequestId)
|
||||||
|
.then(res => res.data)
|
||||||
|
.then(data => {
|
||||||
|
commit(types.SET_MERGE_REQUEST, {
|
||||||
|
projectPath: projectId,
|
||||||
|
mergeRequestId,
|
||||||
|
mergeRequest: data,
|
||||||
|
});
|
||||||
|
if (!state.currentMergeRequestId) {
|
||||||
|
commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
flash('Error loading merge request data. Please try again.');
|
||||||
|
reject(new Error(`Merge Request not loaded ${projectId}`));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMergeRequestChanges = (
|
||||||
|
{ commit, state, dispatch },
|
||||||
|
{ projectId, mergeRequestId, force = false } = {},
|
||||||
|
) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
|
||||||
|
service
|
||||||
|
.getProjectMergeRequestChanges(projectId, mergeRequestId)
|
||||||
|
.then(res => res.data)
|
||||||
|
.then(data => {
|
||||||
|
commit(types.SET_MERGE_REQUEST_CHANGES, {
|
||||||
|
projectPath: projectId,
|
||||||
|
mergeRequestId,
|
||||||
|
changes: data,
|
||||||
|
});
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
flash('Error loading merge request changes. Please try again.');
|
||||||
|
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getMergeRequestVersions = (
|
||||||
|
{ commit, state, dispatch },
|
||||||
|
{ projectId, mergeRequestId, force = false } = {},
|
||||||
|
) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
|
||||||
|
service
|
||||||
|
.getProjectMergeRequestVersions(projectId, mergeRequestId)
|
||||||
|
.then(res => res.data)
|
||||||
|
.then(data => {
|
||||||
|
commit(types.SET_MERGE_REQUEST_VERSIONS, {
|
||||||
|
projectPath: projectId,
|
||||||
|
mergeRequestId,
|
||||||
|
versions: data,
|
||||||
|
});
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
flash('Error loading merge request versions. Please try again.');
|
||||||
|
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
|
||||||
|
}
|
||||||
|
});
|
|
@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||||
import flash from '~/flash';
|
import flash from '~/flash';
|
||||||
import service from '../../services';
|
import service from '../../services';
|
||||||
import * as types from '../mutation_types';
|
import * as types from '../mutation_types';
|
||||||
import {
|
import { findEntry } from '../utils';
|
||||||
findEntry,
|
|
||||||
} from '../utils';
|
|
||||||
import FilesDecoratorWorker from '../workers/files_decorator_worker';
|
import FilesDecoratorWorker from '../workers/files_decorator_worker';
|
||||||
|
|
||||||
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
|
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
|
||||||
|
@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
|
||||||
|
|
||||||
dispatch('setFileActive', row.path);
|
dispatch('setFileActive', row.path);
|
||||||
} else {
|
} else {
|
||||||
dispatch('getFileData', row);
|
dispatch('getFileData', { path: row.path });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
||||||
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
|
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
|
||||||
|
|
||||||
service.getTreeLastCommit(tree.lastCommitPath)
|
service
|
||||||
.then((res) => {
|
.getTreeLastCommit(tree.lastCommitPath)
|
||||||
|
.then(res => {
|
||||||
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
||||||
|
|
||||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
data.forEach((lastCommit) => {
|
data.forEach(lastCommit => {
|
||||||
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
|
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
@ -50,10 +49,8 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
|
||||||
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
|
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFiles = (
|
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
|
||||||
{ state, commit, dispatch },
|
new Promise((resolve, reject) => {
|
||||||
{ projectId, branchId } = {},
|
|
||||||
) => new Promise((resolve, reject) => {
|
|
||||||
if (!state.trees[`${projectId}/${branchId}`]) {
|
if (!state.trees[`${projectId}/${branchId}`]) {
|
||||||
const selectedProject = state.projects[projectId];
|
const selectedProject = state.projects[projectId];
|
||||||
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
|
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
|
||||||
|
@ -61,15 +58,21 @@ export const getFiles = (
|
||||||
service
|
service
|
||||||
.getFiles(selectedProject.web_url, branchId)
|
.getFiles(selectedProject.web_url, branchId)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
const worker = new FilesDecoratorWorker();
|
const worker = new FilesDecoratorWorker();
|
||||||
worker.addEventListener('message', (e) => {
|
worker.addEventListener('message', e => {
|
||||||
const { entries, treeList } = e.data;
|
const { entries, treeList } = e.data;
|
||||||
const selectedTree = state.trees[`${projectId}/${branchId}`];
|
const selectedTree = state.trees[`${projectId}/${branchId}`];
|
||||||
|
|
||||||
commit(types.SET_ENTRIES, entries);
|
commit(types.SET_ENTRIES, entries);
|
||||||
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
|
commit(types.SET_DIRECTORY_DATA, {
|
||||||
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
|
treePath: `${projectId}/${branchId}`,
|
||||||
|
data: treeList,
|
||||||
|
});
|
||||||
|
commit(types.TOGGLE_LOADING, {
|
||||||
|
entry: selectedTree,
|
||||||
|
forceValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
|
|
||||||
|
@ -82,7 +85,7 @@ export const getFiles = (
|
||||||
branchId,
|
branchId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(e => {
|
||||||
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
|
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
|
||||||
reject(e);
|
reject(e);
|
||||||
});
|
});
|
||||||
|
@ -90,4 +93,3 @@ export const getFiles = (
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
export const activeFile = state =>
|
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||||
state.openFiles.find(file => file.active) || null;
|
|
||||||
|
|
||||||
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
|
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
|
||||||
|
|
||||||
export const modifiedFiles = state =>
|
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
|
||||||
state.changedFiles.filter(f => !f.tempFile);
|
|
||||||
|
|
||||||
export const projectsWithTrees = state =>
|
export const projectsWithTrees = state =>
|
||||||
Object.keys(state.projects).map(projectId => {
|
Object.keys(state.projects).map(projectId => {
|
||||||
|
@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const currentMergeRequest = state => {
|
||||||
|
if (state.projects[state.currentProjectId]) {
|
||||||
|
return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-confusing-arrow
|
// eslint-disable-next-line no-confusing-arrow
|
||||||
export const currentIcon = state =>
|
export const currentIcon = state =>
|
||||||
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
|
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
|
||||||
|
|
||||||
export const hasChanges = state => !!state.changedFiles.length;
|
export const hasChanges = state => !!state.changedFiles.length;
|
||||||
|
|
||||||
|
export const hasMergeRequest = state => !!state.currentMergeRequestId;
|
||||||
|
|
|
@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
|
||||||
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
|
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
|
||||||
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
|
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
|
||||||
|
|
||||||
|
// Merge Request Mutation Types
|
||||||
|
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
|
||||||
|
export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
|
||||||
|
export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
|
||||||
|
export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
|
||||||
|
|
||||||
// Branch Mutation Types
|
// Branch Mutation Types
|
||||||
export const SET_BRANCH = 'SET_BRANCH';
|
export const SET_BRANCH = 'SET_BRANCH';
|
||||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||||
|
@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
|
||||||
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
|
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
|
||||||
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
||||||
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
||||||
|
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
|
||||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
||||||
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
|
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
|
||||||
|
@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
|
||||||
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
|
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
|
||||||
export const SET_ENTRIES = 'SET_ENTRIES';
|
export const SET_ENTRIES = 'SET_ENTRIES';
|
||||||
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
|
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
|
||||||
|
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
|
||||||
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
|
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
|
||||||
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
|
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import projectMutations from './mutations/project';
|
import projectMutations from './mutations/project';
|
||||||
|
import mergeRequestMutation from './mutations/merge_request';
|
||||||
import fileMutations from './mutations/file';
|
import fileMutations from './mutations/file';
|
||||||
import treeMutations from './mutations/tree';
|
import treeMutations from './mutations/tree';
|
||||||
import branchMutations from './mutations/branch';
|
import branchMutations from './mutations/branch';
|
||||||
|
@ -11,10 +12,7 @@ export default {
|
||||||
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
|
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
|
||||||
if (entry.path) {
|
if (entry.path) {
|
||||||
Object.assign(state.entries[entry.path], {
|
Object.assign(state.entries[entry.path], {
|
||||||
loading:
|
loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
|
||||||
forceValue !== undefined
|
|
||||||
? forceValue
|
|
||||||
: !state.entries[entry.path].loading,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(entry, {
|
Object.assign(entry, {
|
||||||
|
@ -83,9 +81,7 @@ export default {
|
||||||
|
|
||||||
if (!foundEntry) {
|
if (!foundEntry) {
|
||||||
Object.assign(state.trees[`${projectId}/${branchId}`], {
|
Object.assign(state.trees[`${projectId}/${branchId}`], {
|
||||||
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
|
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
|
||||||
data.treeList,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -100,6 +96,7 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
...projectMutations,
|
...projectMutations,
|
||||||
|
...mergeRequestMutation,
|
||||||
...fileMutations,
|
...fileMutations,
|
||||||
...treeMutations,
|
...treeMutations,
|
||||||
...branchMutations,
|
...branchMutations,
|
||||||
|
|
|
@ -40,6 +40,8 @@ export default {
|
||||||
rawPath: data.raw_path,
|
rawPath: data.raw_path,
|
||||||
binary: data.binary,
|
binary: data.binary,
|
||||||
renderError: data.render_error,
|
renderError: data.render_error,
|
||||||
|
raw: null,
|
||||||
|
baseRaw: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
|
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
|
||||||
|
@ -47,6 +49,11 @@ export default {
|
||||||
raw,
|
raw,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
|
||||||
|
Object.assign(state.entries[file.path], {
|
||||||
|
baseRaw,
|
||||||
|
});
|
||||||
|
},
|
||||||
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
|
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
|
||||||
const changed = content !== state.entries[path].raw;
|
const changed = content !== state.entries[path].raw;
|
||||||
|
|
||||||
|
@ -71,6 +78,11 @@ export default {
|
||||||
editorColumn,
|
editorColumn,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
|
||||||
|
Object.assign(state.entries[file.path], {
|
||||||
|
mrChange,
|
||||||
|
});
|
||||||
|
},
|
||||||
[types.DISCARD_FILE_CHANGES](state, path) {
|
[types.DISCARD_FILE_CHANGES](state, path) {
|
||||||
Object.assign(state.entries[path], {
|
Object.assign(state.entries[path], {
|
||||||
content: state.entries[path].raw,
|
content: state.entries[path].raw,
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import * as types from '../mutation_types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
[types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
|
||||||
|
Object.assign(state, {
|
||||||
|
currentMergeRequestId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
|
||||||
|
Object.assign(state.projects[projectPath], {
|
||||||
|
mergeRequests: {
|
||||||
|
[mergeRequestId]: {
|
||||||
|
...mergeRequest,
|
||||||
|
active: true,
|
||||||
|
changes: [],
|
||||||
|
versions: [],
|
||||||
|
baseCommitSha: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
|
||||||
|
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
|
||||||
|
changes,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
|
||||||
|
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
|
||||||
|
versions,
|
||||||
|
baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -11,6 +11,7 @@ export default {
|
||||||
Object.assign(project, {
|
Object.assign(project, {
|
||||||
tree: [],
|
tree: [],
|
||||||
branches: {},
|
branches: {},
|
||||||
|
mergeRequests: {},
|
||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default () => ({
|
export default () => ({
|
||||||
currentProjectId: '',
|
currentProjectId: '',
|
||||||
currentBranchId: '',
|
currentBranchId: '',
|
||||||
|
currentMergeRequestId: '',
|
||||||
changedFiles: [],
|
changedFiles: [],
|
||||||
endpoints: {},
|
endpoints: {},
|
||||||
lastCommitMsg: '',
|
lastCommitMsg: '',
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const dataStructure = () => ({
|
||||||
eol: '',
|
eol: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const decorateData = (entity) => {
|
export const decorateData = entity => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -57,7 +57,6 @@ export const decorateData = (entity) => {
|
||||||
base64 = false,
|
base64 = false,
|
||||||
|
|
||||||
file_lock,
|
file_lock,
|
||||||
|
|
||||||
} = entity;
|
} = entity;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -80,17 +79,15 @@ export const decorateData = (entity) => {
|
||||||
base64,
|
base64,
|
||||||
|
|
||||||
file_lock,
|
file_lock,
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
|
export const findEntry = (tree, type, name, prop = 'name') =>
|
||||||
f => f.type === type && f[prop] === name,
|
tree.find(f => f.type === type && f[prop] === name);
|
||||||
);
|
|
||||||
|
|
||||||
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
|
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
|
||||||
|
|
||||||
export const setPageTitle = (title) => {
|
export const setPageTitle = title => {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
|
export const sortTree = sortedTree =>
|
||||||
|
sortedTree
|
||||||
|
.map(entity =>
|
||||||
|
Object.assign(entity, {
|
||||||
tree: entity.tree.length ? sortTree(entity.tree) : [],
|
tree: entity.tree.length ? sortTree(entity.tree) : [],
|
||||||
})).sort(sortTreesByTypeAndName);
|
}),
|
||||||
|
)
|
||||||
|
.sort(sortTreesByTypeAndName);
|
||||||
|
|
|
@ -11,11 +11,19 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
helpUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasTitle() {
|
hasTitle() {
|
||||||
return this.title.length > 0;
|
return this.title.length > 0;
|
||||||
},
|
},
|
||||||
|
hasHelpURL() {
|
||||||
|
return this.helpUrl.length > 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -28,5 +36,21 @@
|
||||||
{{ title }}:
|
{{ title }}:
|
||||||
</span>
|
</span>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="hasHelpURL"
|
||||||
|
class="help-button pull-right"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="helpUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-question-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -22,6 +22,11 @@
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
runnerHelpUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
shouldRenderContent() {
|
shouldRenderContent() {
|
||||||
|
@ -39,6 +44,21 @@
|
||||||
runnerId() {
|
runnerId() {
|
||||||
return `#${this.job.runner.id}`;
|
return `#${this.job.runner.id}`;
|
||||||
},
|
},
|
||||||
|
hasTimeout() {
|
||||||
|
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
|
||||||
|
},
|
||||||
|
timeout() {
|
||||||
|
if (this.job.metadata == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = this.job.metadata.timeout_human_readable;
|
||||||
|
if (this.job.metadata.timeout_source !== '') {
|
||||||
|
t += ` (from ${this.job.metadata.timeout_source})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
|
},
|
||||||
renderBlock() {
|
renderBlock() {
|
||||||
return this.job.merge_request ||
|
return this.job.merge_request ||
|
||||||
this.job.duration ||
|
this.job.duration ||
|
||||||
|
@ -114,6 +134,13 @@
|
||||||
title="Queued"
|
title="Queued"
|
||||||
:value="queued"
|
:value="queued"
|
||||||
/>
|
/>
|
||||||
|
<detail-row
|
||||||
|
class="js-job-timeout"
|
||||||
|
v-if="hasTimeout"
|
||||||
|
title="Timeout"
|
||||||
|
:help-url="runnerHelpUrl"
|
||||||
|
:value="timeout"
|
||||||
|
/>
|
||||||
<detail-row
|
<detail-row
|
||||||
class="js-job-runner"
|
class="js-job-runner"
|
||||||
v-if="job.runner"
|
v-if="job.runner"
|
||||||
|
|
|
@ -51,6 +51,7 @@ export default () => {
|
||||||
props: {
|
props: {
|
||||||
isLoading: this.mediator.state.isLoading,
|
isLoading: this.mediator.state.isLoading,
|
||||||
job: this.mediator.store.state.job,
|
job: this.mediator.store.state.job,
|
||||||
|
runnerHelpUrl: dataset.runnerHelpUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -51,7 +51,7 @@ export function removeParams(params) {
|
||||||
const url = document.createElement('a');
|
const url = document.createElement('a');
|
||||||
url.href = window.location.href;
|
url.href = window.location.href;
|
||||||
|
|
||||||
params.forEach((param) => {
|
params.forEach(param => {
|
||||||
url.search = removeParamQueryString(url.search, param);
|
url.search = removeParamQueryString(url.search, param);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,3 +83,11 @@ export function refreshCurrentPage() {
|
||||||
export function redirectTo(url) {
|
export function redirectTo(url) {
|
||||||
return window.location.assign(url);
|
return window.location.assign(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function webIDEUrl(route = undefined) {
|
||||||
|
let returnUrl = `${gon.relative_url_root}/-/ide/`;
|
||||||
|
if (route) {
|
||||||
|
returnUrl += `project${route}`;
|
||||||
|
}
|
||||||
|
return returnUrl;
|
||||||
|
}
|
||||||
|
|
|
@ -292,10 +292,12 @@ Please check your network connection and try again.`;
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="note.resolvable"
|
||||||
class="btn-group discussion-actions"
|
class="btn-group discussion-actions"
|
||||||
role="group">
|
role="group"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="note.resolvable && !discussionResolved"
|
v-if="!discussionResolved"
|
||||||
class="btn-group"
|
class="btn-group"
|
||||||
role="group">
|
role="group">
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -19,15 +19,19 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
groupName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
title() {
|
||||||
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
|
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
|
||||||
},
|
},
|
||||||
text() {
|
text() {
|
||||||
return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
|
return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
|
||||||
Existing project milestones with the same title will be merged.
|
Existing project milestones with the same title will be merged.
|
||||||
This action cannot be reversed.`);
|
This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default () => {
|
||||||
const modalProps = {
|
const modalProps = {
|
||||||
milestoneTitle: button.dataset.milestoneTitle,
|
milestoneTitle: button.dataset.milestoneTitle,
|
||||||
url: button.dataset.url,
|
url: button.dataset.url,
|
||||||
|
groupName: button.dataset.groupName,
|
||||||
};
|
};
|
||||||
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
|
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
|
||||||
eventHub.$emit('promoteMilestoneModal.props', modalProps);
|
eventHub.$emit('promoteMilestoneModal.props', modalProps);
|
||||||
|
@ -54,6 +55,7 @@ export default () => {
|
||||||
return {
|
return {
|
||||||
modalProps: {
|
modalProps: {
|
||||||
milestoneTitle: '',
|
milestoneTitle: '',
|
||||||
|
groupName: '',
|
||||||
url: '',
|
url: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import _ from 'underscore';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||||
|
@ -27,19 +28,26 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
groupName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
text() {
|
text() {
|
||||||
return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
|
return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
|
||||||
Existing project labels with the same title will be merged. This action cannot be reversed.`);
|
Existing project labels with the same title will be merged. This action cannot be reversed.`), {
|
||||||
|
labelTitle: this.labelTitle,
|
||||||
|
groupName: this.groupName,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
const label = `<span
|
const label = `<span
|
||||||
class="label color-label"
|
class="label color-label"
|
||||||
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
|
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
|
||||||
>${this.labelTitle}</span>`;
|
>${_.escape(this.labelTitle)}</span>`;
|
||||||
|
|
||||||
return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
|
return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
|
||||||
labelTitle: label,
|
labelTitle: label,
|
||||||
}, false);
|
}, false);
|
||||||
},
|
},
|
||||||
|
@ -69,6 +77,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
slot="title"
|
slot="title"
|
||||||
|
class="modal-title-with-label"
|
||||||
v-html="title"
|
v-html="title"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
|
@ -30,6 +30,7 @@ const initLabelIndex = () => {
|
||||||
labelColor: button.dataset.labelColor,
|
labelColor: button.dataset.labelColor,
|
||||||
labelTextColor: button.dataset.labelTextColor,
|
labelTextColor: button.dataset.labelTextColor,
|
||||||
url: button.dataset.url,
|
url: button.dataset.url,
|
||||||
|
groupName: button.dataset.groupName,
|
||||||
};
|
};
|
||||||
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
|
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
|
||||||
eventHub.$emit('promoteLabelModal.props', modalProps);
|
eventHub.$emit('promoteLabelModal.props', modalProps);
|
||||||
|
@ -62,6 +63,7 @@ const initLabelIndex = () => {
|
||||||
labelColor: '',
|
labelColor: '',
|
||||||
labelTextColor: '',
|
labelTextColor: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
groupName: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import editForm from './edit_form.vue';
|
import editForm from './edit_form.vue';
|
||||||
import Icon from '../../../vue_shared/components/icon.vue';
|
import Icon from '../../../vue_shared/components/icon.vue';
|
||||||
import { __ } from '../../../locale';
|
import { __ } from '../../../locale';
|
||||||
|
import eventHub from '../../event_hub';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -33,15 +34,26 @@
|
||||||
return this.isConfidential ? 'eye-slash' : 'eye';
|
return this.isConfidential ? 'eye-slash' : 'eye';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
eventHub.$on('closeConfidentialityForm', this.toggleForm);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
eventHub.$off('closeConfidentialityForm', this.toggleForm);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleForm() {
|
toggleForm() {
|
||||||
this.edit = !this.edit;
|
this.edit = !this.edit;
|
||||||
},
|
},
|
||||||
updateConfidentialAttribute(confidential) {
|
updateConfidentialAttribute(confidential) {
|
||||||
this.service.update('issue', { confidential })
|
this.service
|
||||||
|
.update('issue', { confidential })
|
||||||
.then(() => location.reload())
|
.then(() => location.reload())
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
Flash(__('Something went wrong trying to change the confidentiality of this issue'));
|
Flash(
|
||||||
|
__(
|
||||||
|
'Something went wrong trying to change the confidentiality of this issue',
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -50,10 +62,12 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="block issuable-sidebar-item confidentiality">
|
<div class="block issuable-sidebar-item confidentiality">
|
||||||
<div class="sidebar-collapsed-icon">
|
<div
|
||||||
|
class="sidebar-collapsed-icon"
|
||||||
|
@click="toggleForm"
|
||||||
|
>
|
||||||
<icon
|
<icon
|
||||||
:name="confidentialityIcon"
|
:name="confidentialityIcon"
|
||||||
:size="16"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,7 +85,6 @@
|
||||||
<div class="value sidebar-item-value hide-collapsed">
|
<div class="value sidebar-item-value hide-collapsed">
|
||||||
<editForm
|
<editForm
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
:toggle-form="toggleForm"
|
|
||||||
:is-confidential="isConfidential"
|
:is-confidential="isConfidential"
|
||||||
:update-confidential-attribute="updateConfidentialAttribute"
|
:update-confidential-attribute="updateConfidentialAttribute"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,10 +11,6 @@
|
||||||
required: true,
|
required: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
toggleForm: {
|
|
||||||
required: true,
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
updateConfidentialAttribute: {
|
updateConfidentialAttribute: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -22,10 +18,14 @@
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
confidentialityOnWarning() {
|
confidentialityOnWarning() {
|
||||||
return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
|
return s__(
|
||||||
|
'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
confidentialityOffWarning() {
|
confidentialityOffWarning() {
|
||||||
return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
|
return s__(
|
||||||
|
'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -45,7 +45,6 @@
|
||||||
</p>
|
</p>
|
||||||
<edit-form-buttons
|
<edit-form-buttons
|
||||||
:is-confidential="isConfidential"
|
:is-confidential="isConfidential"
|
||||||
:toggle-form="toggleForm"
|
|
||||||
:update-confidential-attribute="updateConfidentialAttribute"
|
:update-confidential-attribute="updateConfidentialAttribute"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
|
import eventHub from '../../event_hub';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
isConfidential: {
|
isConfidential: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
toggleForm: {
|
|
||||||
required: true,
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
updateConfidentialAttribute: {
|
updateConfidentialAttribute: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -22,6 +21,16 @@ export default {
|
||||||
return !this.isConfidential;
|
return !this.isConfidential;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
closeForm() {
|
||||||
|
eventHub.$emit('closeConfidentialityForm');
|
||||||
|
$(this.$el).trigger('hidden.gl.dropdown');
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
this.closeForm();
|
||||||
|
this.updateConfidentialAttribute(this.updateConfidentialBool);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -30,14 +39,14 @@ export default {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-default append-right-10"
|
class="btn btn-default append-right-10"
|
||||||
@click="toggleForm"
|
@click="closeForm"
|
||||||
>
|
>
|
||||||
{{ __('Cancel') }}
|
{{ __('Cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-close"
|
class="btn btn-close"
|
||||||
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
|
@click.prevent="submitForm"
|
||||||
>
|
>
|
||||||
{{ toggleButtonText }}
|
{{ toggleButtonText }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -7,20 +7,13 @@
|
||||||
components: {
|
components: {
|
||||||
editFormButtons,
|
editFormButtons,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [issuableMixin],
|
||||||
issuableMixin,
|
|
||||||
],
|
|
||||||
props: {
|
props: {
|
||||||
isLocked: {
|
isLocked: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleForm: {
|
|
||||||
required: true,
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
|
|
||||||
updateLockedAttribute: {
|
updateLockedAttribute: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -28,10 +21,20 @@
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lockWarning() {
|
lockWarning() {
|
||||||
return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
|
return sprintf(
|
||||||
|
__(
|
||||||
|
'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
|
||||||
|
),
|
||||||
|
{ issuableDisplayName: this.issuableDisplayName },
|
||||||
|
);
|
||||||
},
|
},
|
||||||
unlockWarning() {
|
unlockWarning() {
|
||||||
return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
|
return sprintf(
|
||||||
|
__(
|
||||||
|
'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
|
||||||
|
),
|
||||||
|
{ issuableDisplayName: this.issuableDisplayName },
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -54,7 +57,6 @@
|
||||||
|
|
||||||
<edit-form-buttons
|
<edit-form-buttons
|
||||||
:is-locked="isLocked"
|
:is-locked="isLocked"
|
||||||
:toggle-form="toggleForm"
|
|
||||||
:update-locked-attribute="updateLockedAttribute"
|
:update-locked-attribute="updateLockedAttribute"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
|
import eventHub from '../../event_hub';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
isLocked: {
|
isLocked: {
|
||||||
|
@ -6,11 +9,6 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleForm: {
|
|
||||||
required: true,
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
|
|
||||||
updateLockedAttribute: {
|
updateLockedAttribute: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -26,6 +24,17 @@ export default {
|
||||||
return !this.isLocked;
|
return !this.isLocked;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
closeForm() {
|
||||||
|
eventHub.$emit('closeLockForm');
|
||||||
|
$(this.$el).trigger('hidden.gl.dropdown');
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
this.closeForm();
|
||||||
|
this.updateLockedAttribute(this.toggleLock);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -34,7 +43,7 @@ export default {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-default append-right-10"
|
class="btn btn-default append-right-10"
|
||||||
@click="toggleForm"
|
@click="closeForm"
|
||||||
>
|
>
|
||||||
{{ __('Cancel') }}
|
{{ __('Cancel') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -42,7 +51,7 @@ export default {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-close"
|
class="btn btn-close"
|
||||||
@click.prevent="updateLockedAttribute(toggleLock)"
|
@click.prevent="submitForm"
|
||||||
>
|
>
|
||||||
{{ buttonText }}
|
{{ buttonText }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -3,15 +3,14 @@
|
||||||
import editForm from './edit_form.vue';
|
import editForm from './edit_form.vue';
|
||||||
import issuableMixin from '../../../vue_shared/mixins/issuable';
|
import issuableMixin from '../../../vue_shared/mixins/issuable';
|
||||||
import Icon from '../../../vue_shared/components/icon.vue';
|
import Icon from '../../../vue_shared/components/icon.vue';
|
||||||
|
import eventHub from '../../event_hub';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
editForm,
|
editForm,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [issuableMixin],
|
||||||
issuableMixin,
|
|
||||||
],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
isLocked: {
|
isLocked: {
|
||||||
|
@ -28,7 +27,11 @@
|
||||||
required: true,
|
required: true,
|
||||||
type: Object,
|
type: Object,
|
||||||
validator(mediatorObject) {
|
validator(mediatorObject) {
|
||||||
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
|
return (
|
||||||
|
mediatorObject.service &&
|
||||||
|
mediatorObject.service.update &&
|
||||||
|
mediatorObject.store
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -43,17 +46,35 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
eventHub.$on('closeLockForm', this.toggleForm);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
eventHub.$off('closeLockForm', this.toggleForm);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleForm() {
|
toggleForm() {
|
||||||
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
|
this.mediator.store.isLockDialogOpen = !this.mediator.store
|
||||||
|
.isLockDialogOpen;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateLockedAttribute(locked) {
|
updateLockedAttribute(locked) {
|
||||||
this.mediator.service.update(this.issuableType, {
|
this.mediator.service
|
||||||
|
.update(this.issuableType, {
|
||||||
discussion_locked: locked,
|
discussion_locked: locked,
|
||||||
})
|
})
|
||||||
.then(() => location.reload())
|
.then(() => location.reload())
|
||||||
.catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
|
.catch(() =>
|
||||||
|
Flash(
|
||||||
|
this.__(
|
||||||
|
`Something went wrong trying to change the locked state of this ${
|
||||||
|
this.issuableDisplayName
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -61,10 +82,12 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="block issuable-sidebar-item lock">
|
<div class="block issuable-sidebar-item lock">
|
||||||
<div class="sidebar-collapsed-icon">
|
<div
|
||||||
|
class="sidebar-collapsed-icon"
|
||||||
|
@click="toggleForm"
|
||||||
|
>
|
||||||
<icon
|
<icon
|
||||||
:name="lockIcon"
|
:name="lockIcon"
|
||||||
:size="16"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="sidebar-item-icon is-active"
|
class="sidebar-item-icon is-active"
|
||||||
/>
|
/>
|
||||||
|
@ -85,7 +108,6 @@
|
||||||
<div class="value sidebar-item-value hide-collapsed">
|
<div class="value sidebar-item-value hide-collapsed">
|
||||||
<edit-form
|
<edit-form
|
||||||
v-if="isLockDialogOpen"
|
v-if="isLockDialogOpen"
|
||||||
:toggle-form="toggleForm"
|
|
||||||
:is-locked="isLocked"
|
:is-locked="isLocked"
|
||||||
:update-locked-attribute="updateLockedAttribute"
|
:update-locked-attribute="updateLockedAttribute"
|
||||||
:issuable-type="issuableType"
|
:issuable-type="issuableType"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import tooltip from '~/vue_shared/directives/tooltip';
|
import tooltip from '~/vue_shared/directives/tooltip';
|
||||||
import { n__ } from '~/locale';
|
import { n__ } from '~/locale';
|
||||||
|
import { webIDEUrl } from '~/lib/utils/url_utility';
|
||||||
import icon from '~/vue_shared/components/icon.vue';
|
import icon from '~/vue_shared/components/icon.vue';
|
||||||
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||||
|
|
||||||
|
@ -41,6 +42,9 @@
|
||||||
isTargetBranchLong() {
|
isTargetBranchLong() {
|
||||||
return this.isBranchTitleLong(this.mr.targetBranch);
|
return this.isBranchTitleLong(this.mr.targetBranch);
|
||||||
},
|
},
|
||||||
|
webIdePath() {
|
||||||
|
return webIDEUrl(this.mr.statusPath.replace('.json', ''));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isBranchTitleLong(branchTitle) {
|
isBranchTitleLong(branchTitle) {
|
||||||
|
@ -96,6 +100,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="mr.isOpen">
|
<div v-if="mr.isOpen">
|
||||||
|
<a
|
||||||
|
v-if="!mr.sourceBranchRemoved"
|
||||||
|
:href="webIdePath"
|
||||||
|
class="btn btn-sm btn-default inline js-web-ide"
|
||||||
|
>
|
||||||
|
{{ s__("mrWidget|Web IDE") }}
|
||||||
|
</a>
|
||||||
<button
|
<button
|
||||||
data-target="#modal_merge_info"
|
data-target="#modal_merge_info"
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
|
|
|
@ -199,6 +199,10 @@
|
||||||
.branch-header-title {
|
.branch-header-title {
|
||||||
color: $color-700;
|
color: $color-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ide-file-list .file.file-active {
|
||||||
|
color: $color-700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -4,9 +4,15 @@
|
||||||
|
|
||||||
.page-title,
|
.page-title,
|
||||||
.modal-title {
|
.modal-title {
|
||||||
|
.modal-title-with-label span {
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.color-label {
|
.color-label {
|
||||||
font-size: $gl-font-size;
|
font-size: $gl-font-size;
|
||||||
padding: $gl-vert-padding $label-padding-modal;
|
padding: $gl-vert-padding $label-padding-modal;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,6 @@
|
||||||
|
|
||||||
.right-sidebar {
|
.right-sidebar {
|
||||||
border-left: 1px solid $border-color;
|
border-left: 1px solid $border-color;
|
||||||
height: calc(100% - #{$header-height});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-performance-bar .right-sidebar.affix {
|
.with-performance-bar .right-sidebar.affix {
|
||||||
|
|
|
@ -522,10 +522,6 @@
|
||||||
|
|
||||||
.with-performance-bar .right-sidebar {
|
.with-performance-bar .right-sidebar {
|
||||||
top: $header-height + $performance-bar-height;
|
top: $header-height + $performance-bar-height;
|
||||||
|
|
||||||
.issuable-sidebar {
|
|
||||||
height: calc(100% - #{$performance-bar-height});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-move-issue-confirmation-button {
|
.sidebar-move-issue-confirmation-button {
|
||||||
|
|
|
@ -19,8 +19,7 @@
|
||||||
.ide-view {
|
.ide-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - #{$header-height});
|
height: calc(100vh - #{$header-height});
|
||||||
margin-top: 40px;
|
margin-top: 0;
|
||||||
color: $almost-black;
|
|
||||||
border-top: 1px solid $white-dark;
|
border-top: 1px solid $white-dark;
|
||||||
border-bottom: 1px solid $white-dark;
|
border-bottom: 1px solid $white-dark;
|
||||||
|
|
||||||
|
@ -43,13 +42,18 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.file-open {
|
&.file-open {
|
||||||
background: $white-normal;
|
background: $link-active-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.file-active {
|
||||||
|
font-weight: $gl-font-weight-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-file-name {
|
.ide-file-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
max-width: inherit;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -72,7 +76,10 @@
|
||||||
margin-right: -8px;
|
margin-right: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: $link-active-background;
|
||||||
|
|
||||||
.ide-new-btn {
|
.ide-new-btn {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -450,6 +457,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-file-commit-empty-state-container {
|
.multi-file-commit-empty-state-container {
|
||||||
|
@ -460,7 +469,7 @@
|
||||||
.multi-file-commit-panel-header {
|
.multi-file-commit-panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 0;
|
||||||
border-bottom: 1px solid $white-dark;
|
border-bottom: 1px solid $white-dark;
|
||||||
padding: $gl-btn-padding 0;
|
padding: $gl-btn-padding 0;
|
||||||
|
|
||||||
|
@ -667,8 +676,14 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.nav-only {
|
&.nav-only {
|
||||||
|
padding-top: $header-height;
|
||||||
|
|
||||||
|
.with-performance-bar & {
|
||||||
|
padding-top: $header-height + $performance-bar-height;
|
||||||
|
}
|
||||||
|
|
||||||
.flash-container {
|
.flash-container {
|
||||||
margin-top: $header-height;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -678,7 +693,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
margin-top: $header-height;
|
margin-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -702,11 +717,11 @@
|
||||||
|
|
||||||
.with-performance-bar .ide.nav-only {
|
.with-performance-bar .ide.nav-only {
|
||||||
.flash-container {
|
.flash-container {
|
||||||
margin-top: #{$header-height + $performance-bar-height};
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
margin-top: #{$header-height + $performance-bar-height};
|
margin-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -715,14 +730,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.flash-shown {
|
&.flash-shown {
|
||||||
.content-wrapper {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ide-view {
|
.ide-view {
|
||||||
height: calc(
|
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
|
||||||
100vh - #{$header-height + $performance-bar-height + $flash-height}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,786 @@
|
||||||
|
.project-refs-form,
|
||||||
|
.project-refs-target-form {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message {
|
||||||
|
@include str-truncated(250px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-mode {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-view {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - #{$header-height});
|
||||||
|
margin-top: 40px;
|
||||||
|
color: $almost-black;
|
||||||
|
border-top: 1px solid $white-dark;
|
||||||
|
border-bottom: 1px solid $white-dark;
|
||||||
|
|
||||||
|
&.is-collapsed {
|
||||||
|
.ide-file-list {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status-icon {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-file-list {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.file-open {
|
||||||
|
background: $white-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-file-name {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
margin-right: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-file-changed-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-new-btn {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.ide-new-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.folder {
|
||||||
|
svg {
|
||||||
|
fill: $gl-text-color-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $gl-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name,
|
||||||
|
.file-col-commit-message {
|
||||||
|
display: flex;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-loading-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.animation-container {
|
||||||
|
background: $gray-light;
|
||||||
|
|
||||||
|
div {
|
||||||
|
background: $gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-table-col-commit-message {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-edit-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
border-left: 1px solid $white-dark;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-tabs {
|
||||||
|
display: flex;
|
||||||
|
background-color: $white-normal;
|
||||||
|
box-shadow: inset 0 -1px $white-dark;
|
||||||
|
|
||||||
|
> ul {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
padding: 0 $grid-size;
|
||||||
|
border-left: 1px solid $white-dark;
|
||||||
|
background-color: $white-light;
|
||||||
|
|
||||||
|
&.shadow {
|
||||||
|
box-shadow: 0 0 10px $dropdown-shadow-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-tab {
|
||||||
|
@include str-truncated(150px);
|
||||||
|
padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
|
||||||
|
background-color: $gray-normal;
|
||||||
|
border-right: 1px solid $white-dark;
|
||||||
|
border-bottom: 1px solid $white-dark;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $white-light;
|
||||||
|
border-bottom-color: $white-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-tab-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
border-radius: $border-radius-default;
|
||||||
|
color: $theme-gray-900;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $theme-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: $blue-500;
|
||||||
|
color: $white-light;
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-edit-pane-content {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-editor-container {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.vertical-center {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor .lines-content .cigr {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-diff-editor.vs {
|
||||||
|
.editor.modified {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagonal-fill {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffOverview {
|
||||||
|
background-color: $white-light;
|
||||||
|
border-left: 1px solid $white-dark;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffViewport {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-insert {
|
||||||
|
background-color: $line-added-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-delete {
|
||||||
|
background-color: $line-removed-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
color: $black-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-overlays {
|
||||||
|
.line-insert {
|
||||||
|
background-color: $line-added;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-delete {
|
||||||
|
background-color: $line-removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin {
|
||||||
|
background-color: $gray-light;
|
||||||
|
border-right: 1px solid $white-normal;
|
||||||
|
|
||||||
|
.line-insert {
|
||||||
|
border-right: 1px solid $line-added-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-delete {
|
||||||
|
border-right: 1px solid $line-removed-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-view-overlays .insert-sign,
|
||||||
|
.margin-view-overlays .delete-sign {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursors-layer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-editor-holder {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-editor-btn-group {
|
||||||
|
padding: $gl-bar-padding $gl-padding;
|
||||||
|
border-top: 1px solid $white-dark;
|
||||||
|
border-bottom: 1px solid $white-dark;
|
||||||
|
background: $white-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-status-bar {
|
||||||
|
padding: $gl-bar-padding $gl-padding;
|
||||||
|
background: $white-light;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not great, but this is to deal with our current output
|
||||||
|
.multi-file-preview-holder {
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
.file-content.code {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content,
|
||||||
|
.line-numbers,
|
||||||
|
.blob-content,
|
||||||
|
.code {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-content.blob-no-preview {
|
||||||
|
a {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 340px;
|
||||||
|
padding: 0;
|
||||||
|
background-color: $gray-light;
|
||||||
|
padding-right: 3px;
|
||||||
|
|
||||||
|
.projects-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.context-header {
|
||||||
|
width: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-inner {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-inner-scroll {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-collapsed {
|
||||||
|
width: 60px;
|
||||||
|
|
||||||
|
.multi-file-commit-list {
|
||||||
|
padding-top: $gl-padding;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-context-bar-icon {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
float: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-container {
|
||||||
|
border-left: 4px solid $indigo-700;
|
||||||
|
margin-bottom: $gl-bar-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-header {
|
||||||
|
background: $white-dark;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-header-title {
|
||||||
|
flex: 1;
|
||||||
|
padding: $grid-size $gl-padding;
|
||||||
|
color: $indigo-700;
|
||||||
|
font-weight: $gl-font-weight-bold;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-header-btns {
|
||||||
|
padding: $gl-vert-padding $gl-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-collapse-btn {
|
||||||
|
display: none;
|
||||||
|
background: $gray-light;
|
||||||
|
text-align: left;
|
||||||
|
border-top: 1px solid $white-dark;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-context-bar-icon {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: 10px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-empty-state-container {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-bottom: 1px solid $white-dark;
|
||||||
|
padding: $gl-btn-padding 0;
|
||||||
|
|
||||||
|
&.is-collapsed {
|
||||||
|
border-bottom: 1px solid $white-dark;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-collapse-btn {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-header-title {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 $gl-btn-padding;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: $gl-btn-padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-panel-collapse-btn {
|
||||||
|
border-left: 1px solid $white-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: $gl-padding 0;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-list-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.multi-file-discard-btn {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
color: $gl-link-color;
|
||||||
|
padding: 0 2px;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $white-normal;
|
||||||
|
|
||||||
|
.multi-file-discard-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-addition {
|
||||||
|
fill: $green-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-modified {
|
||||||
|
fill: $orange-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-list-collapsed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-status-icon {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-list-path {
|
||||||
|
padding: $grid-size / 2;
|
||||||
|
padding-left: $gl-padding;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
min-width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-list-file-path {
|
||||||
|
@include str-truncated(100%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-form {
|
||||||
|
padding: $gl-padding;
|
||||||
|
border-top: 1px solid $white-dark;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: $gl-font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-commit-message.form-control {
|
||||||
|
height: 160px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dirty-diff {
|
||||||
|
// !important need to override monaco inline style
|
||||||
|
width: 4px !important;
|
||||||
|
left: 0 !important;
|
||||||
|
|
||||||
|
&-modified {
|
||||||
|
background-color: $blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-added {
|
||||||
|
background-color: $green-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-removed {
|
||||||
|
height: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
bottom: -2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px;
|
||||||
|
border-color: transparent transparent transparent $red-500;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: rgba($red-500, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-loading {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-empty-state {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-new-btn {
|
||||||
|
.dropdown-toggle svg {
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: $gl-font-weight-normal;
|
||||||
|
padding: 5px 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.nav-only {
|
||||||
|
.flash-container {
|
||||||
|
margin-top: $header-height;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-wrapper .flash-container .flash-alert:last-child,
|
||||||
|
.alert-wrapper .flash-container .flash-notice:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
margin-top: $header-height;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flash-shown {
|
||||||
|
.content-wrapper {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-view {
|
||||||
|
height: calc(100vh - #{$header-height + $flash-height});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-sidebar {
|
||||||
|
.multi-file-commit-panel-inner-scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.with-performance-bar .ide.nav-only {
|
||||||
|
.flash-container {
|
||||||
|
margin-top: #{$header-height + $performance-bar-height};
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
margin-top: #{$header-height + $performance-bar-height};
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-view {
|
||||||
|
height: calc(100vh - #{$header-height + $performance-bar-height});
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flash-shown {
|
||||||
|
.content-wrapper {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-view {
|
||||||
|
height: calc(
|
||||||
|
100vh - #{$header-height + $performance-bar-height + $flash-height}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background-color: $white-dark;
|
||||||
|
|
||||||
|
&.dragright {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragleft {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-commit-radios {
|
||||||
|
label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-block {
|
||||||
|
margin-top: 0;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-commit-new-branch {
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-external-links {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-sidebar-link {
|
||||||
|
padding: $gl-padding-8 $gl-padding;
|
||||||
|
background: $indigo-700;
|
||||||
|
color: $white-light;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: $white-light;
|
||||||
|
text-decoration: underline;
|
||||||
|
background: $indigo-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: $indigo-800;
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
|
||||||
|
|
||||||
# Only allow a trusted parameter "white list" through.
|
# Only allow a trusted parameter "white list" through.
|
||||||
def appearance_params
|
def appearance_params
|
||||||
params.require(:appearance).permit(
|
params.require(:appearance).permit(allowed_appearance_params)
|
||||||
:title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
|
end
|
||||||
:new_project_guidelines, :updated_by
|
|
||||||
)
|
def allowed_appearance_params
|
||||||
|
%i[
|
||||||
|
title
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
logo_cache
|
||||||
|
header_logo
|
||||||
|
header_logo_cache
|
||||||
|
new_project_guidelines
|
||||||
|
updated_by
|
||||||
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,10 +21,7 @@ class Projects::BranchesController < Projects::ApplicationController
|
||||||
fetch_branches_by_mode
|
fetch_branches_by_mode
|
||||||
|
|
||||||
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
|
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
|
||||||
@merged_branch_names =
|
@merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
|
||||||
repository.merged_branch_names(@branches.map(&:name))
|
|
||||||
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
|
|
||||||
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
|
||||||
@max_commits = @branches.reduce(0) do |memo, branch|
|
@max_commits = @branches.reduce(0) do |memo, branch|
|
||||||
diverging_commit_counts = repository.diverging_commit_counts(branch)
|
diverging_commit_counts = repository.diverging_commit_counts(branch)
|
||||||
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
|
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
|
||||||
|
@ -32,7 +29,6 @@ class Projects::BranchesController < Projects::ApplicationController
|
||||||
|
|
||||||
render
|
render
|
||||||
end
|
end
|
||||||
end
|
|
||||||
format.json do
|
format.json do
|
||||||
branches = BranchesFinder.new(@repository, params).execute
|
branches = BranchesFinder.new(@repository, params).execute
|
||||||
branches = Kaminari.paginate_array(branches).page(params[:page])
|
branches = Kaminari.paginate_array(branches).page(params[:page])
|
||||||
|
|
|
@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
|
||||||
begin
|
begin
|
||||||
return render_404 unless promote_service.execute(@label)
|
return render_404 unless promote_service.execute(@label)
|
||||||
|
|
||||||
flash[:notice] = "#{@label.title} promoted to group label."
|
flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
redirect_to(project_labels_path(@project), status: 303)
|
redirect_to(project_labels_path(@project), status: 303)
|
||||||
|
|
|
@ -74,9 +74,9 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def promote
|
def promote
|
||||||
Milestones::PromoteService.new(project, current_user).execute(milestone)
|
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
|
||||||
|
|
||||||
flash[:notice] = "#{milestone.title} promoted to group milestone"
|
flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
redirect_to project_milestones_path(project)
|
redirect_to project_milestones_path(project)
|
||||||
|
|
|
@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
|
||||||
else
|
else
|
||||||
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
|
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
|
||||||
end
|
end
|
||||||
|
rescue Gitlab::HTTP::BlockedUrlError => e
|
||||||
|
{ error: true, message: 'Test failed.', service_response: e.message }
|
||||||
end
|
end
|
||||||
|
|
||||||
def success_message
|
def success_message
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
module AppearancesHelper
|
module AppearancesHelper
|
||||||
def brand_title
|
def brand_title
|
||||||
brand_item&.title.presence || 'GitLab Community Edition'
|
current_appearance&.title.presence || 'GitLab Community Edition'
|
||||||
end
|
end
|
||||||
|
|
||||||
def brand_image
|
def brand_image
|
||||||
image_tag(brand_item.logo) if brand_item&.logo?
|
image_tag(current_appearance.logo) if current_appearance&.logo?
|
||||||
end
|
end
|
||||||
|
|
||||||
def brand_text
|
def brand_text
|
||||||
markdown_field(brand_item, :description)
|
markdown_field(current_appearance, :description)
|
||||||
end
|
end
|
||||||
|
|
||||||
def brand_new_project_guidelines
|
def brand_new_project_guidelines
|
||||||
markdown_field(brand_item, :new_project_guidelines)
|
markdown_field(current_appearance, :new_project_guidelines)
|
||||||
end
|
end
|
||||||
|
|
||||||
def brand_item
|
def current_appearance
|
||||||
@appearance ||= Appearance.current
|
@appearance ||= Appearance.current
|
||||||
end
|
end
|
||||||
|
|
||||||
def brand_header_logo
|
def brand_header_logo
|
||||||
if brand_item&.header_logo?
|
if current_appearance&.header_logo?
|
||||||
image_tag brand_item.header_logo
|
image_tag current_appearance.header_logo
|
||||||
else
|
else
|
||||||
render 'shared/logo.svg'
|
render 'shared/logo.svg'
|
||||||
end
|
end
|
||||||
|
@ -29,7 +29,7 @@ module AppearancesHelper
|
||||||
|
|
||||||
# Skip the 'GitLab' type logo when custom brand logo is set
|
# Skip the 'GitLab' type logo when custom brand logo is set
|
||||||
def brand_header_logo_type
|
def brand_header_logo_type
|
||||||
unless brand_item&.header_logo?
|
unless current_appearance&.header_logo?
|
||||||
render 'shared/logo_type.svg'
|
render 'shared/logo_type.svg'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -285,6 +285,10 @@ module ApplicationHelper
|
||||||
class_names
|
class_names
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# EE feature: System header and footer, unavailable in CE
|
||||||
|
def system_message_class
|
||||||
|
end
|
||||||
|
|
||||||
# Returns active css class when condition returns true
|
# Returns active css class when condition returns true
|
||||||
# otherwise returns nil.
|
# otherwise returns nil.
|
||||||
#
|
#
|
||||||
|
|
|
@ -54,9 +54,9 @@ module EmailsHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def header_logo
|
def header_logo
|
||||||
if brand_item && brand_item.header_logo?
|
if current_appearance&.header_logo?
|
||||||
image_tag(
|
image_tag(
|
||||||
brand_item.header_logo,
|
current_appearance.header_logo,
|
||||||
style: 'height: 50px'
|
style: 'height: 50px'
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
|
@ -31,7 +31,7 @@ module NamespacesHelper
|
||||||
|
|
||||||
def namespace_icon(namespace, size = 40)
|
def namespace_icon(namespace, size = 40)
|
||||||
if namespace.is_a?(Group)
|
if namespace.is_a?(Group)
|
||||||
group_icon(namespace)
|
group_icon_url(namespace)
|
||||||
else
|
else
|
||||||
avatar_icon_for_user(namespace.owner, size)
|
avatar_icon_for_user(namespace.owner, size)
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,7 +39,10 @@ module PageLayoutHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def favicon
|
def favicon
|
||||||
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
|
return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
|
||||||
|
return 'favicon-blue.ico' if Rails.env.development?
|
||||||
|
|
||||||
|
'favicon.ico'
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_image
|
def page_image
|
||||||
|
|
|
@ -36,16 +36,15 @@ module Ci
|
||||||
def external_url(project, job)
|
def external_url(project, job)
|
||||||
return unless external_link?(job)
|
return unless external_link?(job)
|
||||||
|
|
||||||
full_path_parts = project.full_path_components
|
url_project_path = project.full_path.partition('/').last
|
||||||
top_level_group = full_path_parts.shift
|
|
||||||
|
|
||||||
artifact_path = [
|
artifact_path = [
|
||||||
'-', *full_path_parts, '-',
|
'-', url_project_path, '-',
|
||||||
'jobs', job.id,
|
'jobs', job.id,
|
||||||
'artifacts', path
|
'artifacts', path
|
||||||
].join('/')
|
].join('/')
|
||||||
|
|
||||||
"#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
|
"#{project.pages_group_url}/#{artifact_path}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def external_link?(job)
|
def external_link?(job)
|
||||||
|
|
|
@ -6,6 +6,7 @@ module Ci
|
||||||
include ObjectStorage::BackgroundMove
|
include ObjectStorage::BackgroundMove
|
||||||
include Presentable
|
include Presentable
|
||||||
include Importable
|
include Importable
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
MissingDependenciesError = Class.new(StandardError)
|
MissingDependenciesError = Class.new(StandardError)
|
||||||
|
|
||||||
|
@ -24,12 +25,18 @@ module Ci
|
||||||
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
|
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
|
||||||
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
|
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
|
||||||
|
|
||||||
# The "environment" field for builds is a String, and is the unexpanded name
|
has_one :metadata, class_name: 'Ci::BuildMetadata'
|
||||||
|
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
|
||||||
|
|
||||||
|
##
|
||||||
|
# The "environment" field for builds is a String, and is the unexpanded name!
|
||||||
|
#
|
||||||
def persisted_environment
|
def persisted_environment
|
||||||
@persisted_environment ||= Environment.find_by(
|
return unless has_environment?
|
||||||
name: expanded_environment_name,
|
|
||||||
project: project
|
strong_memoize(:persisted_environment) do
|
||||||
)
|
Environment.find_by(name: expanded_environment_name, project: project)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
|
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
|
||||||
|
@ -153,6 +160,14 @@ module Ci
|
||||||
before_transition any => [:running] do |build|
|
before_transition any => [:running] do |build|
|
||||||
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_transition pending: :running do |build|
|
||||||
|
build.ensure_metadata.update_timeout_state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_metadata
|
||||||
|
metadata || build_metadata(project: project)
|
||||||
end
|
end
|
||||||
|
|
||||||
def detailed_status(current_user)
|
def detailed_status(current_user)
|
||||||
|
@ -200,7 +215,11 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def expanded_environment_name
|
def expanded_environment_name
|
||||||
ExpandVariables.expand(environment, simple_variables) if environment
|
return unless has_environment?
|
||||||
|
|
||||||
|
strong_memoize(:expanded_environment_name) do
|
||||||
|
ExpandVariables.expand(environment, simple_variables)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_environment?
|
def has_environment?
|
||||||
|
@ -231,10 +250,6 @@ module Ci
|
||||||
latest_builds.where('stage_idx < ?', stage_idx)
|
latest_builds.where('stage_idx < ?', stage_idx)
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
|
||||||
project.build_timeout
|
|
||||||
end
|
|
||||||
|
|
||||||
def triggered_by?(current_user)
|
def triggered_by?(current_user)
|
||||||
user == current_user
|
user == current_user
|
||||||
end
|
end
|
||||||
|
@ -250,31 +265,52 @@ module Ci
|
||||||
Gitlab::Utils.slugify(ref.to_s)
|
Gitlab::Utils.slugify(ref.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Variables whose value does not depend on environment
|
##
|
||||||
def simple_variables
|
# Variables in the environment name scope.
|
||||||
variables(environment: nil)
|
#
|
||||||
end
|
def scoped_variables(environment: expanded_environment_name)
|
||||||
|
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||||
# All variables, including those dependent on environment, which could
|
|
||||||
# contain unexpanded variables.
|
|
||||||
def variables(environment: persisted_environment)
|
|
||||||
collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
|
||||||
variables.concat(predefined_variables)
|
variables.concat(predefined_variables)
|
||||||
variables.concat(project.predefined_variables)
|
variables.concat(project.predefined_variables)
|
||||||
variables.concat(pipeline.predefined_variables)
|
variables.concat(pipeline.predefined_variables)
|
||||||
variables.concat(runner.predefined_variables) if runner
|
variables.concat(runner.predefined_variables) if runner
|
||||||
variables.concat(project.deployment_variables(environment: environment)) if has_environment?
|
variables.concat(project.deployment_variables(environment: environment)) if environment
|
||||||
variables.concat(yaml_variables)
|
variables.concat(yaml_variables)
|
||||||
variables.concat(user_variables)
|
variables.concat(user_variables)
|
||||||
variables.concat(project.group.secret_variables_for(ref, project)) if project.group
|
variables.concat(secret_group_variables)
|
||||||
variables.concat(secret_variables(environment: environment))
|
variables.concat(secret_project_variables(environment: environment))
|
||||||
variables.concat(trigger_request.user_variables) if trigger_request
|
variables.concat(trigger_request.user_variables) if trigger_request
|
||||||
variables.concat(pipeline.variables)
|
variables.concat(pipeline.variables)
|
||||||
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
|
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
|
||||||
variables.concat(persisted_environment_variables) if environment
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
collection.to_runner_variables
|
##
|
||||||
|
# Variables that do not depend on the environment name.
|
||||||
|
#
|
||||||
|
def simple_variables
|
||||||
|
strong_memoize(:simple_variables) do
|
||||||
|
scoped_variables(environment: nil).to_runner_variables
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# All variables, including persisted environment variables.
|
||||||
|
#
|
||||||
|
def variables
|
||||||
|
Gitlab::Ci::Variables::Collection.new
|
||||||
|
.concat(persisted_variables)
|
||||||
|
.concat(scoped_variables)
|
||||||
|
.concat(persisted_environment_variables)
|
||||||
|
.to_runner_variables
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Regular Ruby hash of scoped variables, without duplicates that are
|
||||||
|
# possible to be present in an array of hashes returned from `variables`.
|
||||||
|
#
|
||||||
|
def scoped_variables_hash
|
||||||
|
scoped_variables.to_hash
|
||||||
end
|
end
|
||||||
|
|
||||||
def features
|
def features
|
||||||
|
@ -451,9 +487,14 @@ module Ci
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def secret_variables(environment: persisted_environment)
|
def secret_group_variables
|
||||||
|
return [] unless project.group
|
||||||
|
|
||||||
|
project.group.secret_variables_for(ref, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def secret_project_variables(environment: persisted_environment)
|
||||||
project.secret_variables_for(ref: ref, environment: environment)
|
project.secret_variables_for(ref: ref, environment: environment)
|
||||||
.map(&:to_runner_variable)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def steps
|
def steps
|
||||||
|
@ -550,6 +591,21 @@ module Ci
|
||||||
|
|
||||||
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
|
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
|
||||||
|
|
||||||
|
def persisted_variables
|
||||||
|
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||||
|
return variables unless persisted?
|
||||||
|
|
||||||
|
variables
|
||||||
|
.append(key: 'CI_JOB_ID', value: id.to_s)
|
||||||
|
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
|
||||||
|
.append(key: 'CI_BUILD_ID', value: id.to_s)
|
||||||
|
.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
|
||||||
|
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
|
||||||
|
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
|
||||||
|
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def predefined_variables
|
def predefined_variables
|
||||||
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||||
variables.append(key: 'CI', value: 'true')
|
variables.append(key: 'CI', value: 'true')
|
||||||
|
@ -558,16 +614,11 @@ module Ci
|
||||||
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
|
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
|
||||||
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
|
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
|
||||||
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
|
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
|
||||||
variables.append(key: 'CI_JOB_ID', value: id.to_s)
|
|
||||||
variables.append(key: 'CI_JOB_NAME', value: name)
|
variables.append(key: 'CI_JOB_NAME', value: name)
|
||||||
variables.append(key: 'CI_JOB_STAGE', value: stage)
|
variables.append(key: 'CI_JOB_STAGE', value: stage)
|
||||||
variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
|
|
||||||
variables.append(key: 'CI_COMMIT_SHA', value: sha)
|
variables.append(key: 'CI_COMMIT_SHA', value: sha)
|
||||||
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
|
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
|
||||||
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
|
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
|
||||||
variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
|
|
||||||
variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
|
|
||||||
variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
|
|
||||||
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
|
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
|
||||||
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
|
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
|
||||||
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
|
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
|
||||||
|
@ -575,23 +626,8 @@ module Ci
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def persisted_environment_variables
|
|
||||||
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
|
||||||
return variables unless persisted_environment
|
|
||||||
|
|
||||||
variables.concat(persisted_environment.predefined_variables)
|
|
||||||
|
|
||||||
# Here we're passing unexpanded environment_url for runner to expand,
|
|
||||||
# and we need to make sure that CI_ENVIRONMENT_NAME and
|
|
||||||
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
|
|
||||||
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def legacy_variables
|
def legacy_variables
|
||||||
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||||
variables.append(key: 'CI_BUILD_ID', value: id.to_s)
|
|
||||||
variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
|
|
||||||
variables.append(key: 'CI_BUILD_REF', value: sha)
|
variables.append(key: 'CI_BUILD_REF', value: sha)
|
||||||
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
|
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
|
||||||
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
|
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
|
||||||
|
@ -604,6 +640,19 @@ module Ci
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def persisted_environment_variables
|
||||||
|
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||||
|
return variables unless persisted? && persisted_environment.present?
|
||||||
|
|
||||||
|
variables.concat(persisted_environment.predefined_variables)
|
||||||
|
|
||||||
|
# Here we're passing unexpanded environment_url for runner to expand,
|
||||||
|
# and we need to make sure that CI_ENVIRONMENT_NAME and
|
||||||
|
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
|
||||||
|
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def environment_url
|
def environment_url
|
||||||
options&.dig(:environment, :url) || persisted_environment&.external_url
|
options&.dig(:environment, :url) || persisted_environment&.external_url
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
module Ci
|
||||||
|
# The purpose of this class is to store Build related data that can be disposed.
|
||||||
|
# Data that should be persisted forever, should be stored with Ci::Build model.
|
||||||
|
class BuildMetadata < ActiveRecord::Base
|
||||||
|
extend Gitlab::Ci::Model
|
||||||
|
include Presentable
|
||||||
|
include ChronicDurationAttribute
|
||||||
|
|
||||||
|
self.table_name = 'ci_builds_metadata'
|
||||||
|
|
||||||
|
belongs_to :build, class_name: 'Ci::Build'
|
||||||
|
belongs_to :project
|
||||||
|
|
||||||
|
validates :build, presence: true
|
||||||
|
validates :project, presence: true
|
||||||
|
|
||||||
|
chronic_duration_attr_reader :timeout_human_readable, :timeout
|
||||||
|
|
||||||
|
enum timeout_source: {
|
||||||
|
unknown_timeout_source: 1,
|
||||||
|
project_timeout_source: 2,
|
||||||
|
runner_timeout_source: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_timeout_state
|
||||||
|
return unless build.runner.present?
|
||||||
|
|
||||||
|
project_timeout = project&.build_timeout
|
||||||
|
timeout = [project_timeout, build.runner.maximum_timeout].compact.min
|
||||||
|
timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source
|
||||||
|
|
||||||
|
update(timeout: timeout, timeout_source: timeout_source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,12 +3,13 @@ module Ci
|
||||||
extend Gitlab::Ci::Model
|
extend Gitlab::Ci::Model
|
||||||
include Gitlab::SQL::Pattern
|
include Gitlab::SQL::Pattern
|
||||||
include RedisCacheable
|
include RedisCacheable
|
||||||
|
include ChronicDurationAttribute
|
||||||
|
|
||||||
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
|
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
|
||||||
ONLINE_CONTACT_TIMEOUT = 1.hour
|
ONLINE_CONTACT_TIMEOUT = 1.hour
|
||||||
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
|
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
|
||||||
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
|
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
|
||||||
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
|
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
|
||||||
|
|
||||||
has_many :builds
|
has_many :builds
|
||||||
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
@ -51,6 +52,12 @@ module Ci
|
||||||
|
|
||||||
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
|
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
|
||||||
|
|
||||||
|
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
|
||||||
|
|
||||||
|
validates :maximum_timeout, allow_nil: true,
|
||||||
|
numericality: { greater_than_or_equal_to: 600,
|
||||||
|
message: 'needs to be at least 10 minutes' }
|
||||||
|
|
||||||
# Searches for runners matching the given query.
|
# Searches for runners matching the given query.
|
||||||
#
|
#
|
||||||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||||
|
|
|
@ -51,6 +51,10 @@ module Clusters
|
||||||
|
|
||||||
scope :enabled, -> { where(enabled: true) }
|
scope :enabled, -> { where(enabled: true) }
|
||||||
scope :disabled, -> { where(enabled: false) }
|
scope :disabled, -> { where(enabled: false) }
|
||||||
|
scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
|
||||||
|
scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
|
||||||
|
scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
|
||||||
|
|
||||||
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
|
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
|
||||||
|
|
||||||
def status_name
|
def status_name
|
||||||
|
|
|
@ -4,6 +4,8 @@ module Clusters
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
|
||||||
|
|
||||||
state_machine :status, initial: :not_installable do
|
state_machine :status, initial: :not_installable do
|
||||||
state :not_installable, value: -2
|
state :not_installable, value: -2
|
||||||
state :errored, value: -1
|
state :errored, value: -1
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
module ChronicDurationAttribute
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def chronic_duration_attr_reader(virtual_attribute, source_attribute)
|
||||||
|
define_method(virtual_attribute) do
|
||||||
|
chronic_duration_attributes[virtual_attribute] || output_chronic_duration_attribute(source_attribute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def chronic_duration_attr_writer(virtual_attribute, source_attribute)
|
||||||
|
chronic_duration_attr_reader(virtual_attribute, source_attribute)
|
||||||
|
|
||||||
|
define_method("#{virtual_attribute}=") do |value|
|
||||||
|
chronic_duration_attributes[virtual_attribute] = value.presence || ''
|
||||||
|
|
||||||
|
begin
|
||||||
|
new_value = ChronicDuration.parse(value).to_i if value.present?
|
||||||
|
assign_attributes(source_attribute => new_value)
|
||||||
|
rescue ChronicDuration::DurationParseError
|
||||||
|
# ignore error as it will be caught by validation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validates virtual_attribute, allow_nil: true, duration: true
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :chronic_duration_attr, :chronic_duration_attr_writer
|
||||||
|
end
|
||||||
|
|
||||||
|
def chronic_duration_attributes
|
||||||
|
@chronic_duration_attributes ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def output_chronic_duration_attribute(source_attribute)
|
||||||
|
value = attributes[source_attribute.to_s]
|
||||||
|
ChronicDuration.output(value, format: :short) if value
|
||||||
|
end
|
||||||
|
end
|
|
@ -27,6 +27,10 @@ class DeployKey < Key
|
||||||
self.private?
|
self.private?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
super || User.ghost
|
||||||
|
end
|
||||||
|
|
||||||
def has_access_to?(project)
|
def has_access_to?(project)
|
||||||
deploy_keys_project_for(project).present?
|
deploy_keys_project_for(project).present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :moved_to, class_name: 'Issue'
|
belongs_to :moved_to, class_name: 'Issue'
|
||||||
|
belongs_to :closed_by, class_name: 'User'
|
||||||
|
|
||||||
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
|
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
|
||||||
|
|
||||||
|
@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
|
||||||
before_transition any => :closed do |issue|
|
before_transition any => :closed do |issue|
|
||||||
issue.closed_at = Time.zone.now
|
issue.closed_at = Time.zone.now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_transition closed: :opened do |issue|
|
||||||
|
issue.closed_at = nil
|
||||||
|
issue.closed_by = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
|
|
@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base
|
||||||
Dir.exist?(public_pages_path)
|
Dir.exist?(public_pages_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pages_url
|
def pages_group_url
|
||||||
subdomain, _, url_path = full_path.partition('/')
|
|
||||||
|
|
||||||
# The hostname always needs to be in downcased
|
|
||||||
# All web servers convert hostname to lowercase
|
|
||||||
host = "#{subdomain}.#{Settings.pages.host}".downcase
|
|
||||||
|
|
||||||
# The host in URL always needs to be downcased
|
# The host in URL always needs to be downcased
|
||||||
url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
|
Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
|
||||||
"#{prefix}#{subdomain}."
|
"#{prefix}#{pages_subdomain}."
|
||||||
end.downcase
|
end.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def pages_url
|
||||||
|
url = pages_group_url
|
||||||
|
url_path = full_path.partition('/').last
|
||||||
|
|
||||||
# If the project path is the same as host, we serve it as group page
|
# If the project path is the same as host, we serve it as group page
|
||||||
return url if host == url_path
|
return url if url == "#{Settings.pages.protocol}://#{url_path}"
|
||||||
|
|
||||||
"#{url}/#{url_path}"
|
"#{url}/#{url_path}"
|
||||||
end
|
end
|
||||||
|
@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base
|
||||||
@errors = original_errors
|
@errors = original_errors
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_export_job(current_user:, params: {})
|
def add_export_job(current_user:, after_export_strategy: nil, params: {})
|
||||||
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
|
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
|
||||||
|
|
||||||
if job_id
|
if job_id
|
||||||
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
|
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
|
||||||
|
@ -1572,6 +1571,8 @@ class Project < ActiveRecord::Base
|
||||||
def export_status
|
def export_status
|
||||||
if export_in_progress?
|
if export_in_progress?
|
||||||
:started
|
:started
|
||||||
|
elsif after_export_in_progress?
|
||||||
|
:after_export_action
|
||||||
elsif export_project_path
|
elsif export_project_path
|
||||||
:finished
|
:finished
|
||||||
else
|
else
|
||||||
|
@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base
|
||||||
import_export_shared.active_export_count > 0
|
import_export_shared.active_export_count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def after_export_in_progress?
|
||||||
|
import_export_shared.after_export_in_progress?
|
||||||
|
end
|
||||||
|
|
||||||
def remove_exports
|
def remove_exports
|
||||||
return nil unless export_path.present?
|
return nil unless export_path.present?
|
||||||
|
|
||||||
FileUtils.rm_rf(export_path)
|
FileUtils.rm_rf(export_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_exported_project_file
|
||||||
|
return unless export_project_path.present?
|
||||||
|
|
||||||
|
FileUtils.rm_f(export_project_path)
|
||||||
|
end
|
||||||
|
|
||||||
def full_path_slug
|
def full_path_slug
|
||||||
Gitlab::Utils.slugify(full_path.to_s)
|
Gitlab::Utils.slugify(full_path.to_s)
|
||||||
end
|
end
|
||||||
|
|
|
@ -249,13 +249,13 @@ class Repository
|
||||||
end
|
end
|
||||||
|
|
||||||
def diverging_commit_counts(branch)
|
def diverging_commit_counts(branch)
|
||||||
root_ref_hash = raw_repository.commit(root_ref).id
|
@root_ref_hash ||= raw_repository.commit(root_ref).id
|
||||||
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
|
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
|
||||||
# Rugged seems to throw a `ReferenceError` when given branch_names rather
|
# Rugged seems to throw a `ReferenceError` when given branch_names rather
|
||||||
# than SHA-1 hashes
|
# than SHA-1 hashes
|
||||||
number_commits_behind, number_commits_ahead =
|
number_commits_behind, number_commits_ahead =
|
||||||
raw_repository.count_commits_between(
|
raw_repository.count_commits_between(
|
||||||
root_ref_hash,
|
@root_ref_hash,
|
||||||
branch.dereferenced_target.sha,
|
branch.dereferenced_target.sha,
|
||||||
left_right: true,
|
left_right: true,
|
||||||
max_count: MAX_DIVERGING_COUNT)
|
max_count: MAX_DIVERGING_COUNT)
|
||||||
|
|
|
@ -273,6 +273,7 @@ class Service < ActiveRecord::Base
|
||||||
|
|
||||||
def self.build_from_template(project_id, template)
|
def self.build_from_template(project_id, template)
|
||||||
service = template.dup
|
service = template.dup
|
||||||
|
service.active = false unless service.valid?
|
||||||
service.template = false
|
service.template = false
|
||||||
service.project_id = project_id
|
service.project_id = project_id
|
||||||
service
|
service
|
||||||
|
|
|
@ -82,11 +82,8 @@ class User < ActiveRecord::Base
|
||||||
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
|
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
|
||||||
# Profile
|
# Profile
|
||||||
has_many :keys, -> do
|
has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
type = Key.arel_table[:type]
|
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
|
||||||
where(type.not_eq('DeployKey').or(type.eq(nil)))
|
|
||||||
end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
||||||
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
||||||
has_many :gpg_keys
|
has_many :gpg_keys
|
||||||
|
|
||||||
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
module Ci
|
||||||
|
class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
|
||||||
|
TIMEOUT_SOURCES = {
|
||||||
|
unknown_timeout_source: nil,
|
||||||
|
project_timeout_source: 'project',
|
||||||
|
runner_timeout_source: 'runner'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
presents :metadata
|
||||||
|
|
||||||
|
def timeout_source
|
||||||
|
return unless metadata.timeout_source?
|
||||||
|
|
||||||
|
TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
|
||||||
|
metadata.timeout_source
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
|
||||||
expose :runner, using: RunnerEntity
|
expose :runner, using: RunnerEntity
|
||||||
expose :pipeline, using: PipelineEntity
|
expose :pipeline, using: PipelineEntity
|
||||||
|
|
||||||
|
expose :metadata, using: BuildMetadataEntity
|
||||||
|
|
||||||
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
|
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
|
||||||
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
|
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
|
||||||
erase_project_job_path(project, build)
|
erase_project_job_path(project, build)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class BuildMetadataEntity < Grape::Entity
|
||||||
|
expose :timeout_human_readable do |metadata|
|
||||||
|
metadata.timeout_human_readable unless metadata.timeout.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :timeout_source do |metadata|
|
||||||
|
metadata.present.timeout_source
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity
|
||||||
expose :details_path
|
expose :details_path
|
||||||
|
|
||||||
expose :favicon do |status|
|
expose :favicon do |status|
|
||||||
dir = 'ci_favicons'
|
dir =
|
||||||
dir = File.join(dir, 'dev') if Rails.env.development?
|
if Gitlab::Utils.to_boolean(ENV['CANARY'])
|
||||||
|
File.join('ci_favicons', 'canary')
|
||||||
|
elsif Rails.env.development?
|
||||||
|
File.join('ci_favicons', 'dev')
|
||||||
|
else
|
||||||
|
'ci_favicons'
|
||||||
|
end
|
||||||
|
|
||||||
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
|
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,11 +2,15 @@ module Boards
|
||||||
class ListService < Boards::BaseService
|
class ListService < Boards::BaseService
|
||||||
def execute
|
def execute
|
||||||
create_board! if parent.boards.empty?
|
create_board! if parent.boards.empty?
|
||||||
parent.boards
|
boards
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def boards
|
||||||
|
parent.boards
|
||||||
|
end
|
||||||
|
|
||||||
def create_board!
|
def create_board!
|
||||||
Boards::CreateService.new(parent, current_user).execute
|
Boards::CreateService.new(parent, current_user).execute
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,7 @@ module Issues
|
||||||
end
|
end
|
||||||
|
|
||||||
if project.issues_enabled? && issue.close
|
if project.issues_enabled? && issue.close
|
||||||
|
issue.update(closed_by: current_user)
|
||||||
event_service.close_issue(issue, current_user)
|
event_service.close_issue(issue, current_user)
|
||||||
create_note(issue, commit) if system_note
|
create_note(issue, commit) if system_note
|
||||||
notification_service.close_issue(issue, current_user) if notifications
|
notification_service.close_issue(issue, current_user) if notifications
|
||||||
|
|
|
@ -90,9 +90,6 @@ module Projects
|
||||||
unless @project.gitlab_project_import?
|
unless @project.gitlab_project_import?
|
||||||
@project.write_repository_config
|
@project.write_repository_config
|
||||||
@project.create_wiki unless skip_wiki?
|
@project.create_wiki unless skip_wiki?
|
||||||
create_services_from_active_templates(@project)
|
|
||||||
|
|
||||||
@project.create_labels
|
|
||||||
end
|
end
|
||||||
|
|
||||||
event_service.create_project(@project, current_user)
|
event_service.create_project(@project, current_user)
|
||||||
|
@ -121,21 +118,29 @@ module Projects
|
||||||
Project.transaction do
|
Project.transaction do
|
||||||
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
|
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
|
||||||
|
|
||||||
if @project.save && !@project.import?
|
if @project.save
|
||||||
|
unless @project.gitlab_project_import?
|
||||||
|
create_services_from_active_templates(@project)
|
||||||
|
@project.create_labels
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @project.import?
|
||||||
raise 'Failed to create repository' unless @project.create_repository
|
raise 'Failed to create repository' unless @project.create_repository
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fail(error:)
|
def fail(error:)
|
||||||
message = "Unable to save project. Error: #{error}"
|
message = "Unable to save project. Error: #{error}"
|
||||||
message << "Project ID: #{@project.id}" if @project && @project.id
|
log_message = message.dup
|
||||||
|
|
||||||
Rails.logger.error(message)
|
log_message << " Project ID: #{@project.id}" if @project&.id
|
||||||
|
Rails.logger.error(log_message)
|
||||||
|
|
||||||
if @project && @project.import?
|
if @project
|
||||||
@project.errors.add(:base, message)
|
@project.errors.add(:base, message)
|
||||||
@project.mark_import_as_failed(message)
|
@project.mark_import_as_failed(message) if @project.import?
|
||||||
end
|
end
|
||||||
|
|
||||||
@project
|
@project
|
||||||
|
|
|
@ -1,22 +1,36 @@
|
||||||
module Projects
|
module Projects
|
||||||
module ImportExport
|
module ImportExport
|
||||||
class ExportService < BaseService
|
class ExportService < BaseService
|
||||||
def execute(_options = {})
|
def execute(after_export_strategy = nil, options = {})
|
||||||
@shared = project.import_export_shared
|
@shared = project.import_export_shared
|
||||||
save_all
|
|
||||||
|
save_all!
|
||||||
|
execute_after_export_action(after_export_strategy)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def save_all
|
def execute_after_export_action(after_export_strategy)
|
||||||
if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
|
return unless after_export_strategy
|
||||||
|
|
||||||
|
unless after_export_strategy.execute(current_user, project)
|
||||||
|
cleanup_and_notify_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_all!
|
||||||
|
if save_services
|
||||||
Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
|
Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
|
||||||
notify_success
|
notify_success
|
||||||
else
|
else
|
||||||
cleanup_and_notify
|
cleanup_and_notify_error!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def save_services
|
||||||
|
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
|
||||||
|
end
|
||||||
|
|
||||||
def version_saver
|
def version_saver
|
||||||
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
|
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
|
||||||
end
|
end
|
||||||
|
@ -41,19 +55,22 @@ module Projects
|
||||||
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
|
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cleanup_and_notify
|
def cleanup_and_notify_error
|
||||||
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
|
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
|
||||||
|
|
||||||
FileUtils.rm_rf(@shared.export_path)
|
FileUtils.rm_rf(@shared.export_path)
|
||||||
|
|
||||||
notify_error
|
notify_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup_and_notify_error!
|
||||||
|
cleanup_and_notify_error
|
||||||
|
|
||||||
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
|
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_success
|
def notify_success
|
||||||
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
|
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
|
||||||
|
|
||||||
notification_service.project_exported(@project, @current_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_error
|
def notify_error
|
||||||
|
|
|
@ -28,7 +28,11 @@ module Projects
|
||||||
|
|
||||||
def add_repository_to_project
|
def add_repository_to_project
|
||||||
if project.external_import? && !unknown_url?
|
if project.external_import? && !unknown_url?
|
||||||
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
|
begin
|
||||||
|
Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
|
||||||
|
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||||
|
raise Error, "Blocked import URL: #{e.message}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# We should skip the repository for a GitHub import or GitLab project import,
|
# We should skip the repository for a GitHub import or GitLab project import,
|
||||||
|
|
|
@ -178,6 +178,9 @@ module Projects
|
||||||
|
|
||||||
def latest_sha
|
def latest_sha
|
||||||
project.commit(build.ref).try(:sha).to_s
|
project.commit(build.ref).try(:sha).to_s
|
||||||
|
ensure
|
||||||
|
# Close any file descriptors that were opened and free libgit2 buffers
|
||||||
|
project.cleanup
|
||||||
end
|
end
|
||||||
|
|
||||||
def sha
|
def sha
|
||||||
|
|
|
@ -228,16 +228,9 @@ module ObjectStorage
|
||||||
raise 'Failed to update object store' unless updated
|
raise 'Failed to update object store' unless updated
|
||||||
end
|
end
|
||||||
|
|
||||||
def use_file
|
def use_file(&blk)
|
||||||
if file_storage?
|
with_exclusive_lease do
|
||||||
return yield path
|
unsafe_use_file(&blk)
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
cache_stored_file!
|
|
||||||
yield cache_path
|
|
||||||
ensure
|
|
||||||
cache_storage.delete_dir!(cache_path(nil))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -247,12 +240,9 @@ module ObjectStorage
|
||||||
# new_store: Enum (Store::LOCAL, Store::REMOTE)
|
# new_store: Enum (Store::LOCAL, Store::REMOTE)
|
||||||
#
|
#
|
||||||
def migrate!(new_store)
|
def migrate!(new_store)
|
||||||
uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
|
with_exclusive_lease do
|
||||||
raise 'Already running' unless uuid
|
|
||||||
|
|
||||||
unsafe_migrate!(new_store)
|
unsafe_migrate!(new_store)
|
||||||
ensure
|
end
|
||||||
Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def schedule_background_upload(*args)
|
def schedule_background_upload(*args)
|
||||||
|
@ -384,6 +374,15 @@ module ObjectStorage
|
||||||
"object_storage_migrate:#{model.class}:#{model.id}"
|
"object_storage_migrate:#{model.class}:#{model.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_exclusive_lease
|
||||||
|
uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
|
||||||
|
raise 'exclusive lease already taken' unless uuid
|
||||||
|
|
||||||
|
yield uuid
|
||||||
|
ensure
|
||||||
|
Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
|
||||||
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# Move the file to another store
|
# Move the file to another store
|
||||||
#
|
#
|
||||||
|
@ -418,4 +417,18 @@ module ObjectStorage
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsafe_use_file
|
||||||
|
if file_storage?
|
||||||
|
return yield path
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
cache_stored_file!
|
||||||
|
yield cache_path
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_f(cache_path)
|
||||||
|
cache_storage.delete_dir!(cache_path(nil))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class CertificateFingerprintValidator < ActiveModel::EachValidator
|
||||||
|
FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
|
||||||
|
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
unless value.try(:match, FINGERPRINT_PATTERN)
|
||||||
|
record.errors.add(attribute, "must be a hash containing only letters, numbers, spaces, : and -")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,8 +4,8 @@
|
||||||
# protect against Server-side Request Forgery (SSRF).
|
# protect against Server-side Request Forgery (SSRF).
|
||||||
class ImportableUrlValidator < ActiveModel::EachValidator
|
class ImportableUrlValidator < ActiveModel::EachValidator
|
||||||
def validate_each(record, attribute, value)
|
def validate_each(record, attribute, value)
|
||||||
if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS)
|
Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
|
||||||
record.errors.add(attribute, "imports are not allowed from that URL")
|
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||||
end
|
record.errors.add(attribute, "is blocked: #{e.message}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|