Commit c0dddb51 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-pending-tab

parents 6bec91bf 8dca091f
...@@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb ...@@ -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: ...@@ -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 @@ ...@@ -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)
......
...@@ -6,7 +6,6 @@ end ...@@ -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'] ...@@ -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' ...@@ -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' ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -95,7 +95,7 @@ GEM ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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)
......
...@@ -10,6 +10,9 @@ const Api = { ...@@ -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,25 +25,27 @@ const Api = { ...@@ -22,25 +25,27 @@ 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) callback(data);
.then(({ data }) => {
callback(data);
return data; return data;
}); });
}, },
// 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, {
search: query, params: Object.assign(
per_page: 20, {
}, options), search: query,
}) per_page: 20,
},
options,
),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -51,12 +56,13 @@ const Api = { ...@@ -51,12 +56,13 @@ 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
params: { .get(url, {
search: query, params: {
per_page: 20, search: query,
}, per_page: 20,
}) },
})
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
...@@ -73,9 +79,10 @@ const Api = { ...@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true; defaults.membership = true;
} }
return axios.get(url, { return axios
params: Object.assign(defaults, options), .get(url, {
}) params: Object.assign(defaults, options),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -85,8 +92,32 @@ const Api = { ...@@ -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,30 +133,30 @@ const Api = { ...@@ -102,30 +133,30 @@ 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
label: data, .post(url, {
}) label: data,
})
.then(res => callback(res.data)) .then(res => callback(res.data))
.catch(e => callback(e.response.data)); .catch(e => callback(e.response.data));
}, },
// 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,
}, },
}) })
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
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 = { ...@@ -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 = { ...@@ -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 = { ...@@ -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, {
per_page: 20, search: query,
}, options), per_page: 20,
},
options,
),
}); });
}, },
......
...@@ -31,7 +31,7 @@ export default function renderMath($els) { ...@@ -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 { ...@@ -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 { ...@@ -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);
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
icon, icon,
},
props: {
file: {
type: Object,
required: true,
}, },
props: { },
file: { computed: {
type: Object, changedIcon() {
required: true, return this.file.tempFile ? 'file-addition' : 'file-modified';
},
}, },
computed: { changedIconClass() {
changedIcon() { return `multi-${this.changedIcon}`;
return this.file.tempFile ? 'file-addition' : 'file-modified';
},
changedIconClass() {
return `multi-${this.changedIcon}`;
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<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: {
Icon, Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
}, },
props: { mergeRequestId: {
hasChanges: { type: String,
type: Boolean, required: false,
required: false, default: '',
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
}, },
methods: { viewer: {
changeMode(mode) { type: String,
this.$emit('click', mode); required: true,
},
}, },
}; showShadow: {
type: Boolean,
required: true,
},
},
computed: {
mergeReviewLine() {
return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
mergeRequestId: this.mergeRequestId,
});
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script> </script>
<template> <template>
...@@ -43,7 +56,10 @@ ...@@ -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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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"
......
<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>
<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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -110,7 +102,11 @@ export default {
this.model = this.editor.createModel(this.file); this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model); if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
} else {
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'; ...@@ -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 { ...@@ -15,6 +16,7 @@ export default {
fileStatusIcon, fileStatusIcon,
fileIcon, fileIcon,
changedFileIcon, changedFileIcon,
mrFileIcon,
}, },
props: { props: {
file: { file: {
...@@ -56,10 +58,7 @@ export default { ...@@ -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 { ...@@ -98,11 +97,15 @@ export default {
:file="file" :file="file"
/> />
</span> </span>
<changed-file-icon <span class="pull-right">
:file="file" <mr-file-icon
v-if="file.changed || file.tempFile" v-if="file.mrChange"
class="prepend-top-5 pull-right" />
/> <changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
/>
</span>
<new-dropdown <new-dropdown
v-if="isTree" v-if="isTree"
:project-id="file.projectId" :project-id="file.projectId"
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
export default { export default {
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
}, },
directives: { },
tooltip, computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
}, },
props: { },
file: { };
type: Object,
required: true,
},
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
},
},
};
</script> </script>
<template> <template>
......
...@@ -26,6 +26,11 @@ export default { ...@@ -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 { ...@@ -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({ ...@@ -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) => { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) => { ...@@ -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) => { ...@@ -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) => { ...@@ -80,15 +79,40 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
}); });
}; };
export const getRawFileData = ({ commit, dispatch }, file) => export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
service commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
.getRawFileData(file) };
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
}) const file = state.entries[path];
.catch(() => return new Promise((resolve, reject) => {
flash('Error loading file content. Please try again.', 'alert', document, null, false, true), service
); .getRawFileData(file)
.then(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(e => {
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];
......
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'; ...@@ -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) => { ...@@ -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,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -50,44 +49,47 @@ 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 } = {}, if (!state.trees[`${projectId}/${branchId}`]) {
) => new Promise((resolve, reject) => { const selectedProject = state.projects[projectId];
if (!state.trees[`${projectId}/${branchId}`]) { commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); service
.getFiles(selectedProject.web_url, branchId)
service .then(res => res.json())
.getFiles(selectedProject.web_url, branchId) .then(data => {
.then(res => res.json()) const worker = new FilesDecoratorWorker();
.then((data) => { worker.addEventListener('message', e => {
const worker = new FilesDecoratorWorker(); const { entries, treeList } = e.data;
worker.addEventListener('message', (e) => { const selectedTree = state.trees[`${projectId}/${branchId}`];
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`]; commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, {
commit(types.SET_ENTRIES, entries); treePath: `${projectId}/${branchId}`,
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList }); data: treeList,
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false }); });
commit(types.TOGGLE_LOADING, {
worker.terminate(); entry: selectedTree,
forceValue: false,
resolve(); });
});
worker.terminate();
worker.postMessage({
data, resolve();
projectId, });
branchId,
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch(e => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
}); });
}) } else {
.catch((e) => { resolve();
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); }
reject(e); });
});
} else {
resolve();
}
});
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 => ...@@ -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'; ...@@ -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'; ...@@ -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'; ...@@ -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';
......
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 { ...@@ -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 { ...@@ -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 { ...@@ -100,6 +96,7 @@ export default {
}); });
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
...branchMutations, ...branchMutations,
......
...@@ -40,6 +40,8 @@ export default { ...@@ -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 { ...@@ -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 { ...@@ -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,
......
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 { ...@@ -11,6 +11,7 @@ export default {
Object.assign(project, { Object.assign(project, {
tree: [], tree: [],
branches: {}, branches: {},
mergeRequests: {},
active: true, active: true,
}); });
......
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [], changedFiles: [],
endpoints: {}, endpoints: {},
lastCommitMsg: '', lastCommitMsg: '',
......
...@@ -38,7 +38,7 @@ export const dataStructure = () => ({ ...@@ -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) => { ...@@ -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) => { ...@@ -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) => { ...@@ -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 =>
tree: entity.tree.length ? sortTree(entity.tree) : [], sortedTree
})).sort(sortTreesByTypeAndName); .map(entity =>
Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
}),
)
.sort(sortTreesByTypeAndName);
...@@ -11,11 +11,19 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 () => { ...@@ -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) { ...@@ -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() { ...@@ -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.`; ...@@ -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 @@ ...@@ -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 () => { ...@@ -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 () => { ...@@ -54,6 +55,7 @@ export default () => {
return { return {
modalProps: { modalProps: {
milestoneTitle: '', milestoneTitle: '',
groupName: '',
url: '', url: '',
}, },
}; };
......
<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 @@ ...@@ -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 @@ ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -62,6 +63,7 @@ const initLabelIndex = () => {
labelColor: '', labelColor: '',
labelTextColor: '', labelTextColor: '',
url: '', url: '',
groupName: '',
}, },
}; };
}, },
......
<script> <script>
import Flash from '../../../flash'; import Flash from '../../../flash';
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: {
editForm, editForm,
Icon, Icon,
},
props: {
isConfidential: {
required: true,
type: Boolean,
}, },
props: { isEditable: {
isConfidential: { required: true,
required: true, type: Boolean,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
service: {
required: true,
type: Object,
},
}, },
data() { service: {
return { required: true,
edit: false, type: Object,
};
}, },
computed: { },
confidentialityIcon() { data() {
return this.isConfidential ? 'eye-slash' : 'eye'; return {
}, edit: false,
};
},
computed: {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
}, },
methods: { },
toggleForm() { created() {
this.edit = !this.edit; eventHub.$on('closeConfidentialityForm', this.toggleForm);
}, },
updateConfidentialAttribute(confidential) { beforeDestroy() {
this.service.update('issue', { confidential }) eventHub.$off('closeConfidentialityForm', this.toggleForm);
.then(() => location.reload()) },
.catch(() => { methods: {
Flash(__('Something went wrong trying to change the confidentiality of this issue')); toggleForm() {
}); this.edit = !this.edit;
},
}, },
}; updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
.then(() => location.reload())
.catch(() => {
Flash(
__(
'Something went wrong trying to change the confidentiality of this issue',
),
);
});
},
},
};
</script> </script>
<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 @@ ...@@ -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"
/> />
......
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
import { s__ } from '../../../locale'; import { s__ } from '../../../locale';
export default { export default {
components: { components: {
editFormButtons, editFormButtons,
},
props: {
isConfidential: {
required: true,
type: Boolean,
}, },
props: { updateConfidentialAttribute: {
isConfidential: { required: true,
required: true, type: Function,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
}, },
computed: { },
confidentialityOnWarning() { computed: {
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.'); confidentialityOnWarning() {
}, return s__(
confidentialityOffWarning() { '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 off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.'); );
},
}, },
}; 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.',
);
},
},
};
</script> </script>
<template> <template>
...@@ -45,7 +45,6 @@ ...@@ -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>
......
<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 { ...@@ -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 { ...@@ -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>
......
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale'; import { __, sprintf } from '../../../locale';
export default { export default {
components: { components: {
editFormButtons, editFormButtons,
},
mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
}, },
mixins: [
issuableMixin,
],
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: { updateLockedAttribute: {
required: true, required: true,
type: Function, type: Function,
}, },
},
updateLockedAttribute: { computed: {
required: true, lockWarning() {
type: Function, return sprintf(
}, __(
'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
),
{ issuableDisplayName: this.issuableDisplayName },
);
}, },
computed: { unlockWarning() {
lockWarning() { return sprintf(
return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); __(
}, 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
unlockWarning() { ),
return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); { issuableDisplayName: this.issuableDisplayName },
}, );
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -54,7 +57,6 @@ ...@@ -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>
......
<script> <script>
import $ from 'jquery';
import eventHub from '../../event_hub';
export default { export default {
props: { props: {
isLocked: { isLocked: {
...@@ -6,11 +9,6 @@ export default { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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>
......
<script> <script>
import Flash from '~/flash'; import Flash from '~/flash';
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: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
isEditable: { isEditable: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
mediator: { mediator: {
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
);
}, },
}, },
},
computed: { computed: {
lockIcon() { lockIcon() {
return this.isLocked ? 'lock' : 'lock-open'; return this.isLocked ? 'lock' : 'lock-open';
}, },
isLockDialogOpen() { isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen; return this.mediator.store.isLockDialogOpen;
},
}, },
},
methods: { created() {
toggleForm() { eventHub.$on('closeLockForm', this.toggleForm);
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; },
},
beforeDestroy() {
eventHub.$off('closeLockForm', this.toggleForm);
},
updateLockedAttribute(locked) { methods: {
this.mediator.service.update(this.issuableType, { toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store
.isLockDialogOpen;
},
updateLockedAttribute(locked) {
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
}`,
),
),
);
}, },
}; },
};
</script> </script>
<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 @@ ...@@ -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"
......
<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 icon from '~/vue_shared/components/icon.vue'; import { webIDEUrl } from '~/lib/utils/url_utility';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
}, },
components: { },
icon, computed: {
clipboardButton, shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
}, },
props: { commitsText() {
mr: { return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
type: Object,
required: true,
},
}, },
computed: { branchNameClipboardData() {
shouldShowCommitsBehindText() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that
return this.mr.divergedCommitsCount > 0; // works around ClipboardJS limitations to allow the context-specific
}, // copy/pasting of plain text or GFM.
commitsText() { return JSON.stringify({
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); text: this.mr.sourceBranch,
}, gfm: `\`${this.mr.sourceBranch}\``,
branchNameClipboardData() { });
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
}, },
methods: { isSourceBranchLong() {
isBranchTitleLong(branchTitle) { return this.isBranchTitleLong(this.mr.sourceBranch);
return branchTitle.length > 32;
},
}, },
}; isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() {
return webIDEUrl(this.mr.statusPath.replace('.json', ''));
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script> </script>
<template> <template>
<div class="mr-source-target"> <div class="mr-source-target">
...@@ -96,6 +100,13 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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}
);
} }
} }
} }
......
This diff is collapsed.
...@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController ...@@ -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,17 +21,13 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -21,17 +21,13 @@ 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)) @max_commits = @branches.reduce(0) do |memo, branch|
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 diverging_commit_counts = repository.diverging_commit_counts(branch)
Gitlab::GitalyClient.allow_n_plus_1_calls do [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
render
end end
render
end end
format.json do format.json do
branches = BranchesFinder.new(@repository, params).execute branches = BranchesFinder.new(@repository, params).execute
......
...@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -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 ...@@ -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 ...@@ -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
......
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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
##
# 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
collection.to_runner_variables ##
# 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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
......
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 ...@@ -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)
......
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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
raise 'Failed to create repository' unless @project.create_repository 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
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
......
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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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)
end
unsafe_migrate!(new_store)
ensure
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 ...@@ -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 ...@@ -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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment