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