Commit 5501dfd1 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'feature/gb/variables-expressions-in-only-except'

 Conflicts:
   app/models/ci/build.rb
parents f4d81536 8230b774
...@@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb ...@@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/* lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
...@@ -50,6 +50,7 @@ eslint-report.html ...@@ -50,6 +50,7 @@ eslint-report.html
/db/data.yml /db/data.yml
/doc/code/* /doc/code/*
/dump.rdb /dump.rdb
/jsconfig.json
/log/*.log* /log/*.log*
/node_modules/ /node_modules/
/nohup.out /nohup.out
......
See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html <!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html -->
## What does this MR do? ## What does this MR do?
(briefly describe what this MR is about) <!-- Briefly describe what this MR is about -->
## Related issues
<!-- Mention the issue(s) this MR closes or is related to -->
Closes
## Moving docs to a new location? ## Moving docs to a new location?
See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location Read the guidelines:
https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location
- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location. - [ ] Make sure the old link is not removed and has its contents replaced with
a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken. - [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory. - [ ] Search and replace any links referring to old docs in GitLab Rails app,
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/doc_styleguide.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread. specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] If working on CE, submit an MR to EE with the changes as well. - [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread.
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review. - [ ] Ping one of the technical writers for review.
/label ~Documentation
...@@ -2,6 +2,26 @@ ...@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.6.1 (2018-03-27)
### Security (1 change)
- Bump rails-html-sanitizer to 1.0.4.
### Fixed (2 changes)
- Prevent auto-retry AccessDenied error from stopping transition to failed. !17862
- Fix 500 error when trying to resolve non-ASCII conflicts in the editor. !17962
### Performance (1 change)
- Add indexes for user activity queries. !17890
### Other (1 change)
- Add documentation for runner IP address (#44232). !17837
## 10.6.0 (2018-03-22) ## 10.6.0 (2018-03-22)
### Security (4 changes) ### Security (4 changes)
......
...@@ -28,7 +28,7 @@ gem 'default_value_for', gem_versions['default_value_for'] ...@@ -28,7 +28,7 @@ gem 'default_value_for', gem_versions['default_value_for']
gem 'mysql2', '~> 0.4.10', group: :mysql gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.26.0' gem 'rugged', '~> 0.27'
gem 'grape-route-helpers', '~> 2.1.0' gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
...@@ -44,7 +44,7 @@ gem 'omniauth-cas3', '~> 1.1.4' ...@@ -44,7 +44,7 @@ gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2' gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
...@@ -52,6 +52,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' ...@@ -52,6 +52,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.4' gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.1' gem 'omniauth-authentiq', '~> 0.3.1'
gem 'omniauth-jwt', '~> 0.0.2'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6' gem 'jwt', '~> 1.5.6'
...@@ -117,9 +118,9 @@ gem 'carrierwave', '~> 1.2' ...@@ -117,9 +118,9 @@ gem 'carrierwave', '~> 1.2'
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 2.0' gem 'fog-aws', '~> 2.0.1'
gem 'fog-core', '~> 1.44' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5' gem 'fog-google', '~> 1.3.3'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
...@@ -145,8 +146,8 @@ gem 'rdoc', '~> 4.2' ...@@ -145,8 +146,8 @@ gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.7' gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.9' gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0' gem 'bootstrap_form', '~> 2.7.0'
...@@ -375,6 +376,8 @@ group :development, :test do ...@@ -375,6 +376,8 @@ group :development, :test do
gem 'stackprof', '~> 0.2.10', require: false gem 'stackprof', '~> 0.2.10', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false gem 'simple_po_parser', '~> 1.1.2', require: false
gem 'timecop', '~> 0.8.0'
end end
group :test do group :test do
...@@ -384,7 +387,6 @@ group :test do ...@@ -384,7 +387,6 @@ group :test do
gem 'webmock', '~> 2.3.2' gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5' gem 'concurrent-ruby', '~> 1.0.5'
gem 'test-prof', '~> 0.2.5' gem 'test-prof', '~> 0.2.5'
end end
...@@ -420,7 +422,7 @@ group :ed25519 do ...@@ -420,7 +422,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0' gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -56,8 +56,8 @@ GEM ...@@ -56,8 +56,8 @@ GEM
faraday_middleware (~> 0.9) faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0) faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0) oauth2 (~> 1.0)
asciidoctor (1.5.3) asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.7) asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5) asciidoctor (~> 1.5)
asset_sync (2.2.0) asset_sync (2.2.0)
activemodel (>= 4.1.0) activemodel (>= 4.1.0)
...@@ -244,10 +244,11 @@ GEM ...@@ -244,10 +244,11 @@ GEM
builder builder
excon (~> 0.58) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (0.5.3) fog-google (1.3.3)
fog-core fog-core
fog-json fog-json
fog-xml fog-xml
google-api-client (~> 0.19.1)
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
...@@ -289,7 +290,7 @@ GEM ...@@ -289,7 +290,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.88.0) gitaly-proto (0.91.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -549,11 +550,13 @@ GEM ...@@ -549,11 +550,13 @@ GEM
omniauth-gitlab (1.0.2) omniauth-gitlab (1.0.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.5.2) omniauth-google-oauth2 (0.5.3)
jwt (~> 1.5) jwt (>= 1.5)
multi_json (~> 1.3)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1) omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
omniauth-kerberos (0.3.0) omniauth-kerberos (0.3.0)
omniauth-multipassword omniauth-multipassword
timfel-krb5-auth (~> 0.8) timfel-krb5-auth (~> 0.8)
...@@ -562,8 +565,8 @@ GEM ...@@ -562,8 +565,8 @@ GEM
omniauth-oauth (1.1.0) omniauth-oauth (1.1.0)
oauth oauth
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (1.4.0) omniauth-oauth2 (1.5.0)
oauth2 (~> 1.0) oauth2 (~> 1.1)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2) omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
...@@ -810,7 +813,7 @@ GEM ...@@ -810,7 +813,7 @@ GEM
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.26.0) rugged (0.27.0)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
...@@ -995,8 +998,8 @@ DEPENDENCIES ...@@ -995,8 +998,8 @@ DEPENDENCIES
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
asana (~> 0.6.0) asana (~> 0.6.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.7) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0) asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
...@@ -1044,9 +1047,9 @@ DEPENDENCIES ...@@ -1044,9 +1047,9 @@ DEPENDENCIES
flipper-active_record (~> 0.13.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0) fog-aws (~> 2.0.1)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 1.3.3)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
...@@ -1058,7 +1061,7 @@ DEPENDENCIES ...@@ -1058,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.88.0) gitaly-proto (~> 0.91.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
...@@ -1114,7 +1117,8 @@ DEPENDENCIES ...@@ -1114,7 +1117,8 @@ DEPENDENCIES
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1) omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.2) omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
...@@ -1169,7 +1173,7 @@ DEPENDENCIES ...@@ -1169,7 +1173,7 @@ DEPENDENCIES
ruby-prof (~> 0.17.0) ruby-prof (~> 0.17.0)
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.26.0) rugged (~> 0.27)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
......
...@@ -31,7 +31,7 @@ export default function renderMath($els) { ...@@ -31,7 +31,7 @@ export default function renderMath($els) {
if (!$els.length) return; if (!$els.length) return;
Promise.all([ Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'), import(/* webpackChunkName: 'katex' */ 'katex'),
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'), import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]).then(([katex]) => { ]).then(([katex]) => {
renderWithKaTeX($els, katex); renderWithKaTeX($els, katex);
}).catch(() => flash(__('An error occurred while rendering KaTeX'))); }).catch(() => flash(__('An error occurred while rendering KaTeX')));
......
...@@ -753,7 +753,7 @@ GitLabDropdown = (function() { ...@@ -753,7 +753,7 @@ GitLabDropdown = (function() {
} }
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
return; return [selectedObject];
} }
if (el.hasClass(ACTIVE_CLASS) && value !== 0) { if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
......
...@@ -6,6 +6,7 @@ export const defaultEditorOptions = { ...@@ -6,6 +6,7 @@ export const defaultEditorOptions = {
minimap: { minimap: {
enabled: false, enabled: false,
}, },
wordWrap: 'bounded',
}; };
export default [ export default [
......
...@@ -11,11 +11,19 @@ ...@@ -11,11 +11,19 @@
type: String, type: String,
required: true, required: true,
}, },
helpUrl: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
hasTitle() { hasTitle() {
return this.title.length > 0; return this.title.length > 0;
}, },
hasHelpURL() {
return this.helpUrl.length > 0;
},
}, },
}; };
</script> </script>
...@@ -28,5 +36,21 @@ ...@@ -28,5 +36,21 @@
{{ title }}: {{ title }}:
</span> </span>
{{ value }} {{ value }}
<span
v-if="hasHelpURL"
class="help-button pull-right"
>
<a
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
></i>
</a>
</span>
</p> </p>
</template> </template>
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
type: Boolean, type: Boolean,
required: true, required: true,
}, },
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
shouldRenderContent() { shouldRenderContent() {
...@@ -39,6 +44,21 @@ ...@@ -39,6 +44,21 @@
runnerId() { runnerId() {
return `#${this.job.runner.id}`; return `#${this.job.runner.id}`;
}, },
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() { renderBlock() {
return this.job.merge_request || return this.job.merge_request ||
this.job.duration || this.job.duration ||
...@@ -114,6 +134,13 @@ ...@@ -114,6 +134,13 @@
title="Queued" title="Queued"
:value="queued" :value="queued"
/> />
<detail-row
class="js-job-timeout"
v-if="hasTimeout"
title="Timeout"
:help-url="runnerHelpUrl"
:value="timeout"
/>
<detail-row <detail-row
class="js-job-runner" class="js-job-runner"
v-if="job.runner" v-if="job.runner"
......
...@@ -51,6 +51,7 @@ export default () => { ...@@ -51,6 +51,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job, job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
}, },
}); });
}, },
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import Graph from './graph.vue'; import Graph from './graph.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store'; import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
Graph, Graph,
GraphGroup, GraphGroup,
EmptyState, EmptyState,
},
props: {
hasMetrics: {
type: Boolean,
required: false,
default: true,
}, },
showLegend: {
props: { type: Boolean,
hasMetrics: { required: false,
type: Boolean, default: true,
required: false,
default: true,
},
showLegend: {
type: Boolean,
required: false,
default: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
forceSmallGraph: {
type: Boolean,
required: false,
default: false,
},
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
},
clustersPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
}, },
showPanels: {
data() { type: Boolean,
return { required: false,
store: new MonitoringStore(), default: true,
state: 'gettingStarted',
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {},
};
}, },
forceSmallGraph: {
created() { type: Boolean,
this.service = new MonitoringService({ required: false,
metricsEndpoint: this.metricsEndpoint, default: false,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
}, },
documentationPath: {
beforeDestroy() { type: String,
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); required: true,
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
}, },
settingsPath: {
mounted() { type: String,
this.resizeThrottled = _.throttle(this.resize, 600); required: true,
if (!this.hasMetrics) { },
this.state = 'gettingStarted'; clustersPath: {
} else { type: String,
this.getGraphsData(); required: true,
window.addEventListener('resize', this.resizeThrottled, false); },
tagsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
},
data() {
return {
store: new MonitoringStore(),
state: 'gettingStarted',
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {},
};
},
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
}
},
methods: {
getGraphsData() {
this.state = 'loading';
Promise.all([
this.service.getGraphsData().then(data => this.store.storeMetrics(data)),
this.service
.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')),
])
.then(() => {
if (this.store.groups.length < 1) {
this.state = 'noData';
return;
}
this.showEmptyState = false;
})
.catch(() => {
this.state = 'unableToConnect';
});
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
} }
}, },
hoverChanged(data) {
methods: { this.hoverData = data;
getGraphsData() {
this.state = 'loading';
Promise.all([
this.service.getGraphsData()
.then(data => this.store.storeMetrics(data)),
this.service.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')),
])
.then(() => {
if (this.store.groups.length < 1) {
this.state = 'noData';
return;
}
this.showEmptyState = false;
})
.catch(() => { this.state = 'unableToConnect'; });
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
}
},
hoverChanged(data) {
this.hoverData = data;
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
props: { props: {
documentationPath: { documentationPath: {
type: String, type: String,
required: true, required: true,
}, },
settingsPath: { settingsPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
clustersPath: { clustersPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
selectedState: { selectedState: {
type: String, type: String,
required: true, required: true,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
}, },
data() { emptyGettingStartedSvgPath: {
return { type: String,
states: { required: true,
gettingStarted: { },
svgUrl: this.emptyGettingStartedSvgPath, emptyLoadingSvgPath: {
title: 'Get started with performance monitoring', type: String,
description: `Stay updated about the performance and health required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
},
data() {
return {
states: {
gettingStarted: {
svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`, of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Install Prometheus on clusters', buttonText: 'Install Prometheus on clusters',
buttonPath: this.clustersPath, buttonPath: this.clustersPath,
secondaryButtonText: 'Configure existing Prometheus', secondaryButtonText: 'Configure existing Prometheus',
secondaryButtonPath: this.settingsPath, secondaryButtonPath: this.settingsPath,
}, },
loading: { loading: {
svgUrl: this.emptyLoadingSvgPath, svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data', title: 'Waiting for performance data',
description: `Creating graphs uses the data from the Prometheus server. description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`, If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation', buttonText: 'View documentation',
buttonPath: this.documentationPath, buttonPath: this.documentationPath,
}, },
noData: { noData: {
svgUrl: this.emptyNoDataSvgPath, svgUrl: this.emptyNoDataSvgPath,
title: 'No data found', title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently description: `You are connected to the Prometheus server, but there is currently
no data to display.`, no data to display.`,
buttonText: 'Configure Prometheus', buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath, buttonPath: this.settingsPath,
}, },
unableToConnect: { unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server', title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ', description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation', buttonText: 'View documentation',
buttonPath: this.documentationPath, buttonPath: this.documentationPath,
},
}, },
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
}, },
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
props: { props: {
deploymentData: { deploymentData: {
type: Array, type: Array,
required: true, required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
}, },
graphHeight: {
computed: { type: Number,
calculatedHeight() { required: true,
return this.graphHeight - this.graphHeightOffset;
},
}, },
graphHeightOffset: {
methods: { type: Number,
transformDeploymentGroup(deployment) { required: true,
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
}, },
}; },
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
methods: {
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
},
},
};
</script> </script>
<template> <template>
<g class="deploy-info"> <g class="deploy-info">
......
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils'; import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
components: { components: {
icon, icon,
}, },
props: { props: {
currentXCoordinate: { currentXCoordinate: {
type: Number, type: Number,
required: true, required: true,
},
currentData: {
type: Object,
required: true,
},
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
}, },
currentData: {
computed: { type: Object,
formatTime() { required: true,
return this.deploymentFlagData ?
timeFormat(this.deploymentFlagData.time) :
timeFormat(this.currentData.time);
},
formatDate() {
return this.deploymentFlagData ?
dateFormat(this.deploymentFlagData.time) :
dateFormat(this.currentData.time);
},
cursorStyle() {
const xCoordinate = this.deploymentFlagData ?
this.deploymentFlagData.xPos :
this.currentXCoordinate;
const offsetTop = 20 * this.realPixelRatio;
const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
return {
top: `${offsetTop}px`,
left: `${offsetLeft}px`,
height: `${height}px`,
};
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
}, },
deploymentFlagData: {
type: Object,
required: false,
default: null,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
realPixelRatio: {
type: Number,
required: true,
},
showFlagContent: {
type: Boolean,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
},
computed: {
formatTime() {
return this.deploymentFlagData
? timeFormat(this.deploymentFlagData.time)
: timeFormat(this.currentData.time);
},
formatDate() {
return this.deploymentFlagData
? dateFormat(this.deploymentFlagData.time)
: dateFormat(this.currentData.time);
},
cursorStyle() {
const xCoordinate = this.deploymentFlagData
? this.deploymentFlagData.xPos
: this.currentXCoordinate;
methods: { const offsetTop = 20 * this.realPixelRatio;
seriesMetricValue(series) { const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
const index = this.deploymentFlagData ? const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
this.deploymentFlagData.seriesIndex :
this.currentDataIndex;
const value = series.values[index] &&
series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
},
strokeDashArray(type) { return {
if (type === 'dashed') return '6, 3'; top: `${offsetTop}px`,
if (type === 'dotted') return '3, 3'; left: `${offsetLeft}px`,
return null; height: `${height}px`,
}, };
},
flagOrientation() {
if (this.currentXCoordinate * this.realPixelRatio > 120) {
return 'left';
}
return 'right';
},
},
methods: {
seriesMetricValue(series) {
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
: this.currentDataIndex;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
},
seriesMetricLabel(index, series) {
if (this.timeSeries.length < 2) {
return this.legendTitle;
}
if (series.metricTag) {
return series.metricTag;
}
return `series ${index + 1}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils'; import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
legendTitle: {
type: String,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
};
},
computed: {
textTransform() {
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2) || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2)
+ (this.yLabelWidth / 2) || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- this.margin.right) || 0;
},
yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
},
export default {
props: {
graphWidth: {
type: Number,
required: true,
}, },
mounted() { graphHeight: {
this.$nextTick(() => { type: Number,
const bbox = this.$refs.ylabel.getBBox(); required: true,
this.metricUsageXPosition = 0; },
this.seriesXPosition = 0; margin: {
if (this.$refs.legendTitleSvg != null) { type: Object,
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; required: true,
} },
if (this.$refs.seriesTitleSvg != null) { measurements: {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; type: Object,
} required: true,
this.yLabelWidth = bbox.width + 10; // Added some padding },
this.yLabelHeight = bbox.height + 5; legendTitle: {
}); type: String,
}, required: true,
methods: { },
translateLegendGroup(index) { yAxisLabel: {
return `translate(0, ${12 * (index)})`; type: String,
}, required: true,
},
formatMetricUsage(series) { timeSeries: {
const value = series.values[this.currentDataIndex] && type: Array,
series.values[this.currentDataIndex].value; required: true,
if (isNaN(value)) { },
return '-'; unitOfDisplay: {
} type: String,
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`; required: true,
}, },
currentDataIndex: {
type: Number,
required: true,
},
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
};
},
computed: {
textTransform() {
const yCoordinate =
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
createSeriesString(index, series) { return `translate(15, ${yCoordinate}) rotate(-90)`;
if (series.metricTag) { },
return `${series.metricTag} ${this.formatMetricUsage(series)}`; rectTransform() {
} const yCoordinate =
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`; (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
}, this.yLabelWidth / 2 || 0;
strokeDashArray(type) { return `translate(0, ${yCoordinate}) rotate(-90)`;
if (type === 'dashed') return '6, 3'; },
if (type === 'dotted') return '3, 3'; xPosition() {
return null; return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
}, },
yPosition() {
return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
},
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
},
methods: {
translateLegendGroup(index) {
return `translate(0, ${12 * index})`;
},
formatMetricUsage(series) {
const value =
series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
},
createSeriesString(index, series) {
if (series.metricTag) {
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
}, },
}; },
};
</script> </script>
<template> <template>
<g class="axis-label-container"> <g class="axis-label-container">
......
<script> <script>
export default { export default {
props: { props: {
generatedLinePath: { generatedLinePath: {
type: String, type: String,
required: true, required: true,
},
generatedAreaPath: {
type: String,
required: true,
},
lineStyle: {
type: String,
required: false,
default: '',
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
}, },
computed: { generatedAreaPath: {
strokeDashArray() { type: String,
if (this.lineStyle === 'dashed') return '3, 1'; required: true,
if (this.lineStyle === 'dotted') return '1, 1';
return null;
},
}, },
}; lineStyle: {
type: String,
required: false,
default: '',
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
},
computed: {
strokeDashArray() {
if (this.lineStyle === 'dashed') return '3, 1';
if (this.lineStyle === 'dotted') return '1, 1';
return null;
},
},
};
</script> </script>
<template> <template>
<g> <g>
......
<script> <script>
export default { export default {
props: { props: {
name: { name: {
type: String, type: String,
required: true, required: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
}, },
}; showPanels: {
type: Boolean,
required: false,
default: true,
},
},
};
</script> </script>
<template> <template>
......
...@@ -292,10 +292,12 @@ Please check your network connection and try again.`; ...@@ -292,10 +292,12 @@ Please check your network connection and try again.`;
</button> </button>
</div> </div>
<div <div
v-if="note.resolvable"
class="btn-group discussion-actions" class="btn-group discussion-actions"
role="group"> role="group"
>
<div <div
v-if="note.resolvable && !discussionResolved" v-if="!discussionResolved"
class="btn-group" class="btn-group"
role="group"> role="group">
<a <a
......
...@@ -19,15 +19,19 @@ ...@@ -19,15 +19,19 @@
type: String, type: String,
required: true, required: true,
}, },
groupName: {
type: String,
required: true,
},
}, },
computed: { computed: {
title() { title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle }); return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
}, },
text() { text() {
return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group. return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged. Existing project milestones with the same title will be merged.
This action cannot be reversed.`); This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
}, },
}, },
methods: { methods: {
......
...@@ -25,6 +25,7 @@ export default () => { ...@@ -25,6 +25,7 @@ export default () => {
const modalProps = { const modalProps = {
milestoneTitle: button.dataset.milestoneTitle, milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url, url: button.dataset.url,
groupName: button.dataset.groupName,
}; };
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted); eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps); eventHub.$emit('promoteMilestoneModal.props', modalProps);
...@@ -54,6 +55,7 @@ export default () => { ...@@ -54,6 +55,7 @@ export default () => {
return { return {
modalProps: { modalProps: {
milestoneTitle: '', milestoneTitle: '',
groupName: '',
url: '', url: '',
}, },
}; };
......
<script> <script>
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
...@@ -27,19 +28,26 @@ ...@@ -27,19 +28,26 @@
type: String, type: String,
required: true, required: true,
}, },
groupName: {
type: String,
required: true,
},
}, },
computed: { computed: {
text() { text() {
return s__(`Milestones|Promoting this label will make it available for all projects inside the group. return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
Existing project labels with the same title will be merged. This action cannot be reversed.`); Existing project labels with the same title will be merged. This action cannot be reversed.`), {
labelTitle: this.labelTitle,
groupName: this.groupName,
});
}, },
title() { title() {
const label = `<span const label = `<span
class="label color-label" class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};" style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
>${this.labelTitle}</span>`; >${_.escape(this.labelTitle)}</span>`;
return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), { return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
labelTitle: label, labelTitle: label,
}, false); }, false);
}, },
...@@ -69,6 +77,7 @@ ...@@ -69,6 +77,7 @@
> >
<div <div
slot="title" slot="title"
class="modal-title-with-label"
v-html="title" v-html="title"
> >
{{ title }} {{ title }}
......
...@@ -30,6 +30,7 @@ const initLabelIndex = () => { ...@@ -30,6 +30,7 @@ const initLabelIndex = () => {
labelColor: button.dataset.labelColor, labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor, labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url, url: button.dataset.url,
groupName: button.dataset.groupName,
}; };
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted); eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps); eventHub.$emit('promoteLabelModal.props', modalProps);
...@@ -62,6 +63,7 @@ const initLabelIndex = () => { ...@@ -62,6 +63,7 @@ const initLabelIndex = () => {
labelColor: '', labelColor: '',
labelTextColor: '', labelTextColor: '',
url: '', url: '',
groupName: '',
}, },
}; };
}, },
......
<script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
export default { export default {
name: 'time-tracking-comparison-pane', name: 'TimeTrackingComparisonPane',
props: { props: {
timeSpent: { timeSpent: {
type: Number, type: Number,
...@@ -43,47 +44,50 @@ export default { ...@@ -43,47 +44,50 @@ export default {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
}, },
}, },
template: ` };
<div class="time-tracking-comparison-pane"> </script>
<template>
<div class="time-tracking-comparison-pane">
<div
class="compare-meter"
data-toggle="tooltip"
data-placement="top"
role="timeRemainingDisplay"
:aria-valuenow="timeRemainingTooltip"
:title="timeRemainingTooltip"
:data-original-title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
>
<div <div
class="compare-meter" class="meter-container"
data-toggle="tooltip" role="timeSpentPercent"
data-placement="top" :aria-valuenow="timeRemainingPercent"
role="timeRemainingDisplay"
:aria-valuenow="timeRemainingTooltip"
:title="timeRemainingTooltip"
:data-original-title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
> >
<div <div
class="meter-container" :style="{ width: timeRemainingPercent }"
role="timeSpentPercent" class="meter-fill"
:aria-valuenow="timeRemainingPercent"
> >
<div
:style="{ width: timeRemainingPercent }"
class="meter-fill"
/>
</div> </div>
<div class="compare-display-container"> </div>
<div class="compare-display pull-left"> <div class="compare-display-container">
<span class="compare-label"> <div class="compare-display pull-left">
<span class="compare-label">
{{ s__('TimeTracking|Spent') }} {{ s__('TimeTracking|Spent') }}
</span> </span>
<span class="compare-value spent"> <span class="compare-value spent">
{{ timeSpentHumanReadable }} {{ timeSpentHumanReadable }}
</span> </span>
</div> </div>
<div class="compare-display estimated pull-right"> <div class="compare-display estimated pull-right">
<span class="compare-label"> <span class="compare-label">
{{ s__('TimeTrackingEstimated|Est') }} {{ s__('TimeTrackingEstimated|Est') }}
</span> </span>
<span class="compare-value"> <span class="compare-value">
{{ timeEstimateHumanReadable }} {{ timeEstimateHumanReadable }}
</span> </span>
</div>
</div> </div>
</div> </div>
</div> </div>
`, </div>
}; </template>
...@@ -4,7 +4,7 @@ import TimeTrackingCollapsedState from './collapsed_state.vue'; ...@@ -4,7 +4,7 @@ import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane'; import timeTrackingNoTrackingPane from './no_tracking_pane';
import timeTrackingEstimateOnlyPane from './estimate_only_pane'; import timeTrackingEstimateOnlyPane from './estimate_only_pane';
import timeTrackingComparisonPane from './comparison_pane'; import TimeTrackingComparisonPane from './comparison_pane.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
...@@ -15,7 +15,7 @@ export default { ...@@ -15,7 +15,7 @@ export default {
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane, 'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
'time-tracking-comparison-pane': timeTrackingComparisonPane, TimeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState, 'time-tracking-help-state': timeTrackingHelpState,
}, },
props: { props: {
......
...@@ -17,8 +17,8 @@ export default { ...@@ -17,8 +17,8 @@ export default {
/> />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
The source branch HEAD has recently changed. {{ s__(`mrWidget|The source branch HEAD has recently changed.
Please reload the page and review the changes before merging. Please reload the page and review the changes before merging`) }}
</span> </span>
</div> </div>
</div> </div>
......
...@@ -2,7 +2,15 @@ ...@@ -2,7 +2,15 @@
* Styles the GitLab application with a specific color theme * Styles the GitLab application with a specific color theme
*/ */
@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { @mixin gitlab-theme(
$color-100,
$color-200,
$color-500,
$color-700,
$color-800,
$color-900,
$color-alternate
) {
// Header // Header
.navbar-gitlab { .navbar-gitlab {
...@@ -23,7 +31,7 @@ ...@@ -23,7 +31,7 @@
> li { > li {
> a:hover, > a:hover,
> a:focus { > a:focus {
background-color: rgba($color-200, .2); background-color: rgba($color-200, 0.2);
} }
&.active > a, &.active > a,
...@@ -33,7 +41,7 @@ ...@@ -33,7 +41,7 @@
} }
&.line-separator { &.line-separator {
border-left: 1px solid rgba($color-200, .2); border-left: 1px solid rgba($color-200, 0.2);
} }
} }
} }
...@@ -56,7 +64,7 @@ ...@@ -56,7 +64,7 @@
&:hover, &:hover,
&:focus { &:focus {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
background-color: rgba($color-200, .2); background-color: rgba($color-200, 0.2);
} }
svg { svg {
...@@ -91,34 +99,34 @@ ...@@ -91,34 +99,34 @@
> a { > a {
&:hover, &:hover,
&:focus { &:focus {
background-color: rgba($color-200, .2); background-color: rgba($color-200, 0.2);
} }
} }
} }
.search { .search {
form { form {
background-color: rgba($color-200, .2); background-color: rgba($color-200, 0.2);
&:hover { &:hover {
background-color: rgba($color-200, .3); background-color: rgba($color-200, 0.3);
} }
} }
.location-badge { .location-badge {
color: $color-100; color: $color-100;
background-color: rgba($color-200, .1); background-color: rgba($color-200, 0.1);
border-right: 1px solid $color-800; border-right: 1px solid $color-800;
} }
.search-input::placeholder { .search-input::placeholder {
color: rgba($color-200, .8); color: rgba($color-200, 0.8);
} }
.search-input-wrap { .search-input-wrap {
.search-icon, .search-icon,
.clear-icon { .clear-icon {
fill: rgba($color-200, .8); fill: rgba($color-200, 0.8);
} }
} }
...@@ -133,7 +141,7 @@ ...@@ -133,7 +141,7 @@
.search-input-wrap { .search-input-wrap {
.search-icon { .search-icon {
fill: rgba($color-200, .8); fill: rgba($color-200, 0.8);
} }
} }
} }
...@@ -144,7 +152,6 @@ ...@@ -144,7 +152,6 @@
color: $color-900; color: $color-900;
} }
// Sidebar // Sidebar
.nav-sidebar li.active { .nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700; box-shadow: inset 4px 0 0 $color-700;
...@@ -169,28 +176,94 @@ ...@@ -169,28 +176,94 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
} }
}
// Web IDE
.ide-sidebar-link {
color: $color-200;
background-color: $color-700;
&:hover,
&:focus {
background-color: $color-500;
}
&:active {
background: $color-800;
}
}
.branch-container {
border-left-color: $color-700;
}
.branch-header-title {
color: $color-700;
}
.ide-file-list .file.file-active {
color: $color-700;
}
}
body { body {
&.ui_indigo { &.ui_indigo {
@include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light); @include gitlab-theme(
$indigo-100,
$indigo-200,
$indigo-500,
$indigo-700,
$indigo-800,
$indigo-900,
$white-light
);
} }
&.ui_dark { &.ui_dark {
@include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light); @include gitlab-theme(
$theme-gray-100,
$theme-gray-200,
$theme-gray-500,
$theme-gray-700,
$theme-gray-800,
$theme-gray-900,
$white-light
);
} }
&.ui_blue { &.ui_blue {
@include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light); @include gitlab-theme(
$theme-blue-100,
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
$theme-blue-800,
$theme-blue-900,
$white-light
);
} }
&.ui_green { &.ui_green {
@include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light); @include gitlab-theme(
$theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-700,
$theme-green-800,
$theme-green-900,
$white-light
);
} }
&.ui_light { &.ui_light {
@include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); @include gitlab-theme(
$theme-gray-900,
$theme-gray-700,
$theme-gray-800,
$theme-gray-700,
$theme-gray-700,
$theme-gray-100,
$theme-gray-700
);
.navbar-gitlab { .navbar-gitlab {
background-color: $theme-gray-100; background-color: $theme-gray-100;
...@@ -270,5 +343,9 @@ body { ...@@ -270,5 +343,9 @@ body {
.sidebar-top-level-items > li.active .badge { .sidebar-top-level-items > li.active .badge {
color: $theme-gray-900; color: $theme-gray-900;
} }
.ide-sidebar-link {
color: $white-light;
}
} }
} }
...@@ -4,9 +4,15 @@ ...@@ -4,9 +4,15 @@
.page-title, .page-title,
.modal-title { .modal-title {
.modal-title-with-label span {
vertical-align: middle;
display: inline-block;
}
.color-label { .color-label {
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal; padding: $gl-vert-padding $label-padding-modal;
vertical-align: middle;
} }
} }
......
.ci-body {
.incorrect-syntax {
font-size: 18px;
color: $lint-incorrect-color;
}
.correct-syntax {
font-size: 18px;
color: $lint-correct-color;
}
}
.ci-linter {
.ci-editor {
height: 400px;
}
.ci-template pre {
white-space: pre-wrap;
}
}
...@@ -1121,3 +1121,25 @@ pre.light-well { ...@@ -1121,3 +1121,25 @@ pre.light-well {
padding-top: $gl-padding; padding-top: $gl-padding;
padding-bottom: 37px; padding-bottom: 37px;
} }
.project-ci-body {
.incorrect-syntax {
font-size: 18px;
color: $lint-incorrect-color;
}
.correct-syntax {
font-size: 18px;
color: $lint-correct-color;
}
}
.project-ci-linter {
.ci-editor {
height: 400px;
}
.ci-template pre {
white-space: pre-wrap;
}
}
...@@ -20,7 +20,6 @@ ...@@ -20,7 +20,6 @@
display: flex; display: flex;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
margin-top: 40px; margin-top: 40px;
color: $almost-black;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -43,7 +42,11 @@ ...@@ -43,7 +42,11 @@
cursor: pointer; cursor: pointer;
&.file-open { &.file-open {
background: $white-normal; background: $link-active-background;
}
&.file-active {
font-weight: $gl-font-weight-bold;
} }
.ide-file-name { .ide-file-name {
...@@ -72,7 +75,10 @@ ...@@ -72,7 +75,10 @@
margin-right: -8px; margin-right: -8px;
} }
&:hover { &:hover,
&:focus {
background: $link-active-background;
.ide-new-btn { .ide-new-btn {
display: block; display: block;
} }
...@@ -402,7 +408,7 @@ ...@@ -402,7 +408,7 @@
} }
.branch-container { .branch-container {
border-left: 4px solid $indigo-700; border-left: 4px solid;
margin-bottom: $gl-bar-padding; margin-bottom: $gl-bar-padding;
} }
...@@ -414,7 +420,6 @@ ...@@ -414,7 +420,6 @@
.branch-header-title { .branch-header-title {
flex: 1; flex: 1;
padding: $grid-size $gl-padding; padding: $grid-size $gl-padding;
color: $indigo-700;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
svg { svg {
...@@ -721,9 +726,7 @@ ...@@ -721,9 +726,7 @@
} }
.ide-view { .ide-view {
height: calc( height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
100vh - #{$header-height + $performance-bar-height + $flash-height}
);
} }
} }
} }
...@@ -767,20 +770,7 @@ ...@@ -767,20 +770,7 @@
.ide-sidebar-link { .ide-sidebar-link {
padding: $gl-padding-8 $gl-padding; padding: $gl-padding-8 $gl-padding;
background: $indigo-700;
color: $white-light;
text-decoration: none;
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: $gl-font-weight-bold;
&:focus,
&:hover {
color: $white-light;
text-decoration: underline;
background: $indigo-500;
}
&:active {
background: $indigo-800;
}
} }
...@@ -4,20 +4,5 @@ module Ci ...@@ -4,20 +4,5 @@ module Ci
def show def show
end end
def create
@content = params[:content]
@error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank?
if @error.blank?
@config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
end
render :show
end
end end
end end
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
if attachment
redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
send_params.merge!(filename: attachment, disposition: disposition)
end
if file_upload.file_storage?
send_file file_upload.path, send_params
elsif file_upload.class.proxy_download_enabled?
headers.store(*Gitlab::Workhorse.send_url(file_upload.url(**redirect_params)))
head :ok
else
redirect_to file_upload.url(**redirect_params)
end
end
end
module UploadsActions module UploadsActions
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
...@@ -26,14 +27,11 @@ module UploadsActions ...@@ -26,14 +27,11 @@ module UploadsActions
def show def show
return render_404 unless uploader&.exists? return render_404 unless uploader&.exists?
if uploader.file_storage? expires_in 0.seconds, must_revalidate: true, private: true
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition disposition = uploader.image_or_video? ? 'inline' : 'attachment'
else
redirect_to uploader.url send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
end end
private private
...@@ -62,19 +60,27 @@ module UploadsActions ...@@ -62,19 +60,27 @@ module UploadsActions
end end
def build_uploader_from_upload def build_uploader_from_upload
return nil unless params[:secret] && params[:filename] return unless uploader = build_uploader
upload_path = uploader_class.upload_path(params[:secret], params[:filename]) upload_paths = uploader.upload_paths(params[:filename])
upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_path) upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_paths)
upload&.build_uploader upload&.build_uploader
end end
def build_uploader_from_params def build_uploader_from_params
return unless uploader = build_uploader
uploader.retrieve_from_store!(params[:filename])
uploader
end
def build_uploader
return unless params[:secret] && params[:filename]
uploader = uploader_class.new(model, secret: params[:secret]) uploader = uploader_class.new(model, secret: params[:secret])
return nil unless uploader.model_valid? return unless uploader.model_valid?
uploader.retrieve_from_store!(params[:filename])
uploader uploader
end end
......
class Projects::ArtifactsController < Projects::ApplicationController class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include RendersBlob include RendersBlob
include SendFileUpload
layout 'project' layout 'project'
before_action :authorize_read_build! before_action :authorize_read_build!
...@@ -10,11 +11,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -10,11 +11,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :entry, only: [:file] before_action :entry, only: [:file]
def download def download
if artifacts_file.file_storage? send_upload(artifacts_file, attachment: artifacts_file.filename)
send_file artifacts_file.path, disposition: 'attachment'
else
redirect_to artifacts_file.url
end
end end
def browse def browse
...@@ -45,8 +42,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -45,8 +42,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def raw def raw
path = Gitlab::Ci::Build::Artifacts::Path path = Gitlab::Ci::Build::Artifacts::Path.new(params[:path])
.new(params[:path])
send_artifacts_entry(build, path) send_artifacts_entry(build, path)
end end
...@@ -75,7 +71,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -75,7 +71,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end end
def validate_artifacts! def validate_artifacts!
render_404 unless build && build.artifacts? render_404 unless build&.artifacts?
end end
def build def build
......
...@@ -21,17 +21,13 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -21,17 +21,13 @@ class Projects::BranchesController < Projects::ApplicationController
fetch_branches_by_mode fetch_branches_by_mode
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names = @merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
repository.merged_branch_names(@branches.map(&:name)) @max_commits = @branches.reduce(0) do |memo, branch|
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 diverging_commit_counts = repository.diverging_commit_counts(branch)
Gitlab::GitalyClient.allow_n_plus_1_calls do [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
render
end end
render
end end
format.json do format.json do
branches = BranchesFinder.new(@repository, params).execute branches = BranchesFinder.new(@repository, params).execute
......
class Projects::Ci::LintsController < Projects::ApplicationController
before_action :authorize_create_pipeline!
def show
end
def create
@content = params[:content]
@error = Gitlab::Ci::YamlProcessor.validation_message(@content, yaml_processor_options)
@status = @error.blank?
if @error.blank?
@config_processor = Gitlab::Ci::YamlProcessor.new(@content, yaml_processor_options)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
end
render :show
end
private
def yaml_processor_options
{ project: @project, sha: project.repository.commit.sha }
end
end
class Projects::JobsController < Projects::ApplicationController class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, before_action :authorize_read_build!,
...@@ -117,11 +119,17 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -117,11 +119,17 @@ class Projects::JobsController < Projects::ApplicationController
end end
def raw def raw
build.trace.read do |stream| if trace_artifact_file
if stream.file? send_upload(trace_artifact_file,
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline' send_params: raw_send_params,
else redirect_params: raw_redirect_params)
render_404 else
build.trace.read do |stream|
if stream.file?
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
end end
end end
end end
...@@ -136,9 +144,21 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -136,9 +144,21 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build) return access_denied! unless can?(current_user, :erase_build, build)
end end
def raw_send_params
{ type: 'text/plain; charset=utf-8', disposition: 'inline' }
end
def raw_redirect_params
{ query: { 'response-content-type' => 'text/plain; charset=utf-8', 'response-content-disposition' => 'inline' } }
end
def trace_artifact_file
@trace_artifact_file ||= build.job_artifacts_trace&.file
end
def build def build
@build ||= project.builds.find(params[:id]) @build ||= project.builds.find(params[:id])
.present(current_user: current_user) .present(current_user: current_user)
end end
def build_path(build) def build_path(build)
......
...@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin begin
return render_404 unless promote_service.execute(@label) return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to group label." flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to(project_labels_path(@project), status: 303) redirect_to(project_labels_path(@project), status: 303)
......
class Projects::LfsStorageController < Projects::GitHttpClientController class Projects::LfsStorageController < Projects::GitHttpClientController
include LfsRequest include LfsRequest
include WorkhorseRequest include WorkhorseRequest
include SendFileUpload
skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize] skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
...@@ -11,25 +12,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -11,25 +12,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
return return
end end
send_file lfs_object.file.path, content_type: "application/octet-stream" send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" })
end end
def upload_authorize def upload_authorize
set_workhorse_internal_api_content_type set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
authorized = LfsObjectUploader.workhorse_authorize
authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized
end end
def upload_finalize def upload_finalize
unless tmp_filename if store_file!(oid, size)
render_lfs_forbidden
return
end
if store_file(oid, size, tmp_filename)
head 200 head 200
else else
render plain: 'Unprocessable entity', status: 422 render plain: 'Unprocessable entity', status: 422
end end
rescue ActiveRecord::RecordInvalid
render_400
rescue ObjectStorage::RemoteStoreError
render_lfs_forbidden
end end
private private
...@@ -50,38 +54,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -50,38 +54,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
params[:size].to_i params[:size].to_i
end end
def tmp_filename def store_file!(oid, size)
name = request.headers['X-Gitlab-Lfs-Tmp'] object = LfsObject.find_by(oid: oid, size: size)
return if name.include?('/') unless object&.file&.exists?
return unless oid.present? && name.start_with?(oid) object = create_file!(oid, size)
end
name
end
def store_file(oid, size, tmp_file) return unless object
# Define tmp_file_path early because we use it in "ensure"
tmp_file_path = File.join(LfsObjectUploader.workhorse_upload_path, tmp_file)
object = LfsObject.find_or_create_by(oid: oid, size: size) link_to_project!(object)
file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
file_exists && link_to_project(object)
ensure
FileUtils.rm_f(tmp_file_path)
end end
def move_tmp_file_to_storage(object, path) def create_file!(oid, size)
File.open(path) do |f| LfsObject.new(oid: oid, size: size).tap do |object|
object.file = f object.file.store_workhorse_file!(params, :file)
object.save!
end end
object.file.store!
object.save
end end
def link_to_project(object) def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id) if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project object.projects << storage_project
object.save object.save!
end end
end end
end end
...@@ -42,6 +42,10 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -42,6 +42,10 @@ class Projects::MilestonesController < Projects::ApplicationController
def show def show
@project_namespace = @project.namespace.becomes(Namespace) @project_namespace = @project.namespace.becomes(Namespace)
respond_to do |format|
format.html
end
end end
def create def create
...@@ -70,9 +74,9 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -70,9 +74,9 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def promote def promote
Milestones::PromoteService.new(project, current_user).execute(milestone) promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
flash[:notice] = "#{milestone.title} promoted to group milestone" flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to project_milestones_path(project) redirect_to project_milestones_path(project)
......
...@@ -5,12 +5,8 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController ...@@ -5,12 +5,8 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
@project.repository.branches @project.repository.branches
end end
def create_service_class def service_namespace
::ProtectedBranches::CreateService ::ProtectedBranches
end
def update_service_class
::ProtectedBranches::UpdateService
end end
def load_protected_ref def load_protected_ref
......
...@@ -37,7 +37,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController ...@@ -37,7 +37,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end end
def destroy def destroy
@protected_ref.destroy destroy_service_class.new(@project, current_user).execute(@protected_ref)
respond_to do |format| respond_to do |format|
format.html { redirect_to_repository_settings(@project) } format.html { redirect_to_repository_settings(@project) }
...@@ -47,6 +47,18 @@ class Projects::ProtectedRefsController < Projects::ApplicationController ...@@ -47,6 +47,18 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
protected protected
def create_service_class
service_namespace::CreateService
end
def update_service_class
service_namespace::UpdateService
end
def destroy_service_class
service_namespace::DestroyService
end
def access_level_attributes def access_level_attributes
%i(access_level id) %i(access_level id)
end end
......
...@@ -5,12 +5,8 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController ...@@ -5,12 +5,8 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
@project.repository.tags @project.repository.tags
end end
def create_service_class def service_namespace
::ProtectedTags::CreateService ::ProtectedTags
end
def update_service_class
::ProtectedTags::UpdateService
end end
def load_protected_ref def load_protected_ref
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Projects::RawController < Projects::ApplicationController class Projects::RawController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include BlobHelper include BlobHelper
include SendFileUpload
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars before_action :assign_ref_vars
...@@ -31,7 +32,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -31,7 +32,7 @@ class Projects::RawController < Projects::ApplicationController
lfs_object = find_lfs_object lfs_object = find_lfs_object
if lfs_object && lfs_object.project_allowed_access?(@project) if lfs_object && lfs_object.project_allowed_access?(@project)
send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment' send_upload(lfs_object.file, attachment: @blob.name)
else else
render_404 render_404
end end
......
...@@ -29,12 +29,12 @@ module Projects ...@@ -29,12 +29,12 @@ module Projects
@project_runners = @project.runners.ordered @project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners @assignable_runners = current_user.ci_authorized_runners
.assignable_for(project).ordered.page(params[:page]).per(20) .assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active @shared_runners = ::Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all) @shared_runners_count = @shared_runners.count(:all)
end end
def define_secret_variables def define_secret_variables
@variable = Ci::Variable.new(project: project) @variable = ::Ci::Variable.new(project: project)
.present(current_user: current_user) .present(current_user: current_user)
@variables = project.variables.order_key_asc @variables = project.variables.order_key_asc
.map { |variable| variable.present(current_user: current_user) } .map { |variable| variable.present(current_user: current_user) }
...@@ -42,7 +42,7 @@ module Projects ...@@ -42,7 +42,7 @@ module Projects
def define_triggers_variables def define_triggers_variables
@triggers = @project.triggers @triggers = @project.triggers
@trigger = Ci::Trigger.new @trigger = ::Ci::Trigger.new
end end
def define_badges_variables def define_badges_variables
......
...@@ -42,6 +42,10 @@ class RootController < Dashboard::ProjectsController ...@@ -42,6 +42,10 @@ class RootController < Dashboard::ProjectsController
redirect_to(dashboard_groups_path) redirect_to(dashboard_groups_path)
when 'todos' when 'todos'
redirect_to(dashboard_todos_path) redirect_to(dashboard_todos_path)
when 'issues'
redirect_to(issues_dashboard_path(assignee_id: current_user.id))
when 'merge_requests'
redirect_to(merge_requests_dashboard_path(assignee_id: current_user.id))
end end
end end
......
...@@ -31,7 +31,7 @@ module NamespacesHelper ...@@ -31,7 +31,7 @@ module NamespacesHelper
def namespace_icon(namespace, size = 40) def namespace_icon(namespace, size = 40)
if namespace.is_a?(Group) if namespace.is_a?(Group)
group_icon(namespace) group_icon_url(namespace)
else else
avatar_icon_for_user(namespace.owner, size) avatar_icon_for_user(namespace.owner, size)
end end
......
...@@ -39,7 +39,10 @@ module PageLayoutHelper ...@@ -39,7 +39,10 @@ module PageLayoutHelper
end end
def favicon def favicon
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico' return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
return 'favicon-blue.ico' if Rails.env.development?
'favicon.ico'
end end
def page_image def page_image
......
...@@ -9,12 +9,14 @@ module PreferencesHelper ...@@ -9,12 +9,14 @@ module PreferencesHelper
# Maps `dashboard` values to more user-friendly option text # Maps `dashboard` values to more user-friendly option text
DASHBOARD_CHOICES = { DASHBOARD_CHOICES = {
projects: 'Your Projects (default)', projects: _("Your Projects (default)"),
stars: 'Starred Projects', stars: _("Starred Projects"),
project_activity: "Your Projects' Activity", project_activity: _("Your Projects' Activity"),
starred_project_activity: "Starred Projects' Activity", starred_project_activity: _("Starred Projects' Activity"),
groups: "Your Groups", groups: _("Your Groups"),
todos: "Your Todos" todos: _("Your Todos"),
issues: _("Assigned Issues"),
merge_requests: _("Assigned Merge Requests")
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text # Returns an Array usable by a select field for more user-friendly option text
......
class Appearance < ActiveRecord::Base class Appearance < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include AfterCommitQueue
include ObjectStorage::BackgroundMove
cache_markdown_field :description cache_markdown_field :description
cache_markdown_field :new_project_guidelines cache_markdown_field :new_project_guidelines
......
...@@ -3,6 +3,7 @@ module Ci ...@@ -3,6 +3,7 @@ module Ci
prepend ArtifactMigratable prepend ArtifactMigratable
include TokenAuthenticatable include TokenAuthenticatable
include AfterCommitQueue include AfterCommitQueue
include ObjectStorage::BackgroundMove
include Presentable include Presentable
include Importable include Importable
...@@ -23,6 +24,9 @@ module Ci ...@@ -23,6 +24,9 @@ module Ci
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
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! # The "environment" field for builds is a String, and is the unexpanded name!
# #
...@@ -47,6 +51,7 @@ module Ci ...@@ -47,6 +51,7 @@ module Ci
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
...@@ -153,6 +158,14 @@ module Ci ...@@ -153,6 +158,14 @@ module Ci
before_transition any => [:running] do |build| before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies') build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end end
before_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
def ensure_metadata
metadata || build_metadata(project: project)
end end
def detailed_status(current_user) def detailed_status(current_user)
...@@ -233,10 +246,6 @@ module Ci ...@@ -233,10 +246,6 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx) latest_builds.where('stage_idx < ?', stage_idx)
end end
def timeout
project.build_timeout
end
def triggered_by?(current_user) def triggered_by?(current_user)
user == current_user user == current_user
end end
...@@ -382,13 +391,19 @@ module Ci ...@@ -382,13 +391,19 @@ module Ci
project.running_or_pending_build_count(force: true) project.running_or_pending_build_count(force: true)
end end
def browsable_artifacts?
artifacts_metadata?
end
def artifacts_metadata_entry(path, **options) def artifacts_metadata_entry(path, **options)
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( artifacts_metadata.use_file do |metadata_path|
artifacts_metadata.path, metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
path, metadata_path,
**options) path,
**options)
metadata.to_entry metadata.to_entry
end
end end
def erase_artifacts! def erase_artifacts!
......
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
module Ci module Ci
class JobArtifact < ActiveRecord::Base class JobArtifact < ActiveRecord::Base
include AfterCommitQueue
include ObjectStorage::BackgroundMove
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
belongs_to :project belongs_to :project
...@@ -7,9 +9,11 @@ module Ci ...@@ -7,9 +9,11 @@ module Ci
before_save :set_size, if: :file_changed? before_save :set_size, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
mount_uploader :file, JobArtifactUploader mount_uploader :file, JobArtifactUploader
delegate :open, :exists?, to: :file delegate :exists?, :open, to: :file
enum file_type: { enum file_type: {
archive: 1, archive: 1,
...@@ -21,6 +25,10 @@ module Ci ...@@ -21,6 +25,10 @@ module Ci
self.where(project: project).sum(:size) self.where(project: project).sum(:size)
end end
def local_store?
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
def set_size def set_size
self.size = file.size self.size = file.size
end end
......
...@@ -3,12 +3,13 @@ module Ci ...@@ -3,12 +3,13 @@ module Ci
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include RedisCacheable include RedisCacheable
include ChronicDurationAttribute
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour ONLINE_CONTACT_TIMEOUT = 1.hour
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
has_many :builds has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
...@@ -51,6 +52,12 @@ module Ci ...@@ -51,6 +52,12 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
validates :maximum_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600,
message: 'needs to be at least 10 minutes' }
# Searches for runners matching the given query. # Searches for runners matching the given query.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
......
...@@ -10,6 +10,7 @@ module Clusters ...@@ -10,6 +10,7 @@ module Clusters
Applications::Prometheus.application_name => Applications::Prometheus, Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner Applications::Runner.application_name => Applications::Runner
}.freeze }.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
belongs_to :user belongs_to :user
...@@ -50,6 +51,7 @@ module Clusters ...@@ -50,6 +51,7 @@ module Clusters
scope :enabled, -> { where(enabled: true) } scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) } scope :disabled, -> { where(enabled: false) }
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name def status_name
if provider if provider
......
...@@ -3,6 +3,7 @@ module Avatarable ...@@ -3,6 +3,7 @@ module Avatarable
included do included do
prepend ShadowMethods prepend ShadowMethods
include ObjectStorage::BackgroundMove
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
......
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
module DeploymentPlatform module DeploymentPlatform
# EE would override this and utilize the extra argument # EE would override this and utilize environment argument
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil) def deployment_platform(environment: nil)
@deployment_platform ||= @deployment_platform ||= {}
find_cluster_platform_kubernetes ||
find_kubernetes_service_integration || @deployment_platform[environment] ||= find_deployment_platform(environment)
build_cluster_and_deployment_platform
end end
private private
def find_cluster_platform_kubernetes def find_deployment_platform(environment)
clusters.find_by(enabled: true)&.platform_kubernetes find_cluster_platform_kubernetes(environment: environment) ||
find_kubernetes_service_integration ||
build_cluster_and_deployment_platform
end
# EE would override this and utilize environment argument
def find_cluster_platform_kubernetes(environment: nil)
clusters.enabled.default_environment
.last&.platform_kubernetes
end end
def find_kubernetes_service_integration def find_kubernetes_service_integration
......
...@@ -27,6 +27,10 @@ class DeployKey < Key ...@@ -27,6 +27,10 @@ class DeployKey < Key
self.private? self.private?
end end
def user
super || User.ghost
end
def has_access_to?(project) def has_access_to?(project)
deploy_keys_project_for(project).present? deploy_keys_project_for(project).present?
end end
......
class LfsObject < ActiveRecord::Base class LfsObject < ActiveRecord::Base
include AfterCommitQueue
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects has_many :projects, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
validates :oid, presence: true, uniqueness: true validates :oid, presence: true, uniqueness: true
mount_uploader :file, LfsObjectUploader mount_uploader :file, LfsObjectUploader
before_save :update_file_store
def update_file_store
self.file_store = file.object_store
end
def project_allowed_access?(project) def project_allowed_access?(project)
projects.exists?(project.lfs_storage_project.id) projects.exists?(project.lfs_storage_project.id)
end end
def local_store?
[nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
end
def self.destroy_unreferenced def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id") joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil }) .where(lfs_objects_projects: { id: nil })
......
...@@ -536,18 +536,25 @@ class MergeRequest < ActiveRecord::Base ...@@ -536,18 +536,25 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true) merge_request_diff(true)
end end
def viewable_diffs
@viewable_diffs ||= merge_request_diffs.viewable.to_a
end
def merge_request_diff_for(diff_refs_or_sha) def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha| matcher =
diffs = merge_request_diffs.viewable if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
h[diff_refs_or_sha] = {
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs) 'start_commit_sha' => diff_refs_or_sha.start_sha,
diffs.find_by_diff_refs(diff_refs_or_sha) 'head_commit_sha' => diff_refs_or_sha.head_sha,
else 'base_commit_sha' => diff_refs_or_sha.base_sha
diffs.find_by(head_commit_sha: diff_refs_or_sha) }
end else
end { 'head_commit_sha' => diff_refs_or_sha }
end
@merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha] viewable_diffs.find do |diff|
diff.attributes.slice(*matcher.keys) == matcher
end
end end
def version_params_for(diff_refs) def version_params_for(diff_refs)
......
...@@ -36,7 +36,7 @@ class GemnasiumService < Service ...@@ -36,7 +36,7 @@ class GemnasiumService < Service
after: data[:after], after: data[:after],
token: token, token: token,
api_key: api_key, api_key: api_key,
repo: project.repository.path_to_repo repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9
) )
end end
end end
...@@ -17,32 +17,4 @@ class RedirectRoute < ActiveRecord::Base ...@@ -17,32 +17,4 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%") where(wheres, path, "#{sanitize_sql_like(path)}/%")
end end
scope :permanent, -> do
if column_permanent_exists?
where(permanent: true)
else
none
end
end
scope :temporary, -> do
if column_permanent_exists?
where(permanent: [false, nil])
else
all
end
end
default_value_for :permanent, false
def permanent=(value)
if self.class.column_permanent_exists?
super
end
end
def self.column_permanent_exists?
ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
end
end end
...@@ -93,10 +93,6 @@ class Repository ...@@ -93,10 +93,6 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>" "#<#{self.class.name}:#{@disk_path}>"
end end
def create_hooks
Gitlab::Git::Repository.create_hooks(path_to_repo, Gitlab.config.gitlab_shell.hooks_path)
end
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
return ref if ref.is_a?(::Commit) return ref if ref.is_a?(::Commit)
...@@ -253,13 +249,13 @@ class Repository ...@@ -253,13 +249,13 @@ class Repository
end end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.commit(root_ref).id @root_ref_hash ||= raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather # Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes # than SHA-1 hashes
number_commits_behind, number_commits_ahead = number_commits_behind, number_commits_ahead =
raw_repository.count_commits_between( raw_repository.count_commits_between(
root_ref_hash, @root_ref_hash,
branch.dereferenced_target.sha, branch.dereferenced_target.sha,
left_right: true, left_right: true,
max_count: MAX_DIVERGING_COUNT) max_count: MAX_DIVERGING_COUNT)
......
...@@ -10,8 +10,6 @@ class Route < ActiveRecord::Base ...@@ -10,8 +10,6 @@ class Route < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
validate :ensure_permanent_paths, if: :path_changed?
before_validation :delete_conflicting_orphaned_routes before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed? after_update :delete_conflicting_redirects, if: :path_changed?
...@@ -45,7 +43,7 @@ class Route < ActiveRecord::Base ...@@ -45,7 +43,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes # We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already # of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants. # called it, which deletes conflicts for all descendants.
route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path] route.create_redirect(old_path) if attributes[:path]
end end
end end
end end
...@@ -55,31 +53,17 @@ class Route < ActiveRecord::Base ...@@ -55,31 +53,17 @@ class Route < ActiveRecord::Base
end end
def conflicting_redirects def conflicting_redirects
RedirectRoute.temporary.matching_path_and_descendants(path) RedirectRoute.matching_path_and_descendants(path)
end end
def create_redirect(path, permanent: false) def create_redirect(path)
RedirectRoute.create(source: source, path: path, permanent: permanent) RedirectRoute.create(source: source, path: path)
end end
private private
def create_redirect_for_old_path def create_redirect_for_old_path
create_redirect(path_was, permanent: permanent_redirect?) if path_changed? create_redirect(path_was) if path_changed?
end
def permanent_redirect?
source_type != "Project"
end
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "has been taken before") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end end
def delete_conflicting_orphaned_routes def delete_conflicting_orphaned_routes
......
...@@ -273,6 +273,7 @@ class Service < ActiveRecord::Base ...@@ -273,6 +273,7 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template) def self.build_from_template(project_id, template)
service = template.dup service = template.dup
service.active = false unless service.valid?
service.template = false service.template = false
service.project_id = project_id service.project_id = project_id
service service
......
...@@ -9,6 +9,8 @@ class Upload < ActiveRecord::Base ...@@ -9,6 +9,8 @@ class Upload < ActiveRecord::Base
validates :model, presence: true validates :model, presence: true
validates :uploader, presence: true validates :uploader, presence: true
scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
before_save :calculate_checksum!, if: :foreground_checksummable? before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable? after_commit :schedule_checksum, if: :checksummable?
...@@ -21,6 +23,7 @@ class Upload < ActiveRecord::Base ...@@ -21,6 +23,7 @@ class Upload < ActiveRecord::Base
end end
def absolute_path def absolute_path
raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
return path unless relative_path? return path unless relative_path?
uploader_class.absolute_path(self) uploader_class.absolute_path(self)
...@@ -30,11 +33,11 @@ class Upload < ActiveRecord::Base ...@@ -30,11 +33,11 @@ class Upload < ActiveRecord::Base
self.checksum = nil self.checksum = nil
return unless checksummable? return unless checksummable?
self.checksum = self.class.hexdigest(absolute_path) self.checksum = Digest::SHA256.file(absolute_path).hexdigest
end end
def build_uploader def build_uploader(mounted_as = nil)
uploader_class.new(model, mount_point, **uploader_context).tap do |uploader| uploader_class.new(model, mounted_as || mount_point).tap do |uploader|
uploader.upload = self uploader.upload = self
uploader.retrieve_from_store!(identifier) uploader.retrieve_from_store!(identifier)
end end
...@@ -51,6 +54,12 @@ class Upload < ActiveRecord::Base ...@@ -51,6 +54,12 @@ class Upload < ActiveRecord::Base
}.compact }.compact
end end
def local?
return true if store.nil?
store == ObjectStorage::Store::LOCAL
end
private private
def delete_file! def delete_file!
...@@ -61,10 +70,6 @@ class Upload < ActiveRecord::Base ...@@ -61,10 +70,6 @@ class Upload < ActiveRecord::Base
checksum.nil? && local? && exist? checksum.nil? && local? && exist?
end end
def local?
true
end
def foreground_checksummable? def foreground_checksummable?
checksummable? && size <= CHECKSUM_THRESHOLD checksummable? && size <= CHECKSUM_THRESHOLD
end end
......
...@@ -82,11 +82,8 @@ class User < ActiveRecord::Base ...@@ -82,11 +82,8 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile # Profile
has_many :keys, -> do has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
type = Key.arel_table[:type] has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
where(type.not_eq('DeployKey').or(type.eq(nil)))
end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
...@@ -187,7 +184,7 @@ class User < ActiveRecord::Base ...@@ -187,7 +184,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference # User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos] enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests]
# User's Project preference # User's Project preference
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
......
class ProtectedBranchPolicy < BasePolicy
delegate { @subject.project }
rule { can?(:admin_project) }.policy do
enable :create_protected_branch
enable :update_protected_branch
enable :destroy_protected_branch
end
end
module Ci
class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
TIMEOUT_SOURCES = {
unknown_timeout_source: nil,
project_timeout_source: 'project',
runner_timeout_source: 'runner'
}.freeze
presents :metadata
def timeout_source
return unless metadata.timeout_source?
TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
metadata.timeout_source
end
end
end
...@@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity ...@@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
expose :runner, using: RunnerEntity expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity expose :pipeline, using: PipelineEntity
expose :metadata, using: BuildMetadataEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build| expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build) erase_project_job_path(project, build)
......
class BuildMetadataEntity < Grape::Entity
expose :timeout_human_readable do |metadata|
metadata.timeout_human_readable unless metadata.timeout.nil?
end
expose :timeout_source do |metadata|
metadata.present.timeout_source
end
end
...@@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity ...@@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity
expose :details_path expose :details_path
expose :favicon do |status| expose :favicon do |status|
dir = 'ci_favicons' dir =
dir = File.join(dir, 'dev') if Rails.env.development? if Gitlab::Utils.to_boolean(ENV['CANARY'])
File.join('ci_favicons', 'canary')
elsif Rails.env.development?
File.join('ci_favicons', 'dev')
else
'ci_favicons'
end
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end end
......
...@@ -90,9 +90,6 @@ module Projects ...@@ -90,9 +90,6 @@ module Projects
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.write_repository_config @project.write_repository_config
@project.create_wiki unless skip_wiki? @project.create_wiki unless skip_wiki?
create_services_from_active_templates(@project)
@project.create_labels
end end
event_service.create_project(@project, current_user) event_service.create_project(@project, current_user)
...@@ -121,21 +118,29 @@ module Projects ...@@ -121,21 +118,29 @@ module Projects
Project.transaction do Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
if @project.save && !@project.import? if @project.save
raise 'Failed to create repository' unless @project.create_repository unless @project.gitlab_project_import?
create_services_from_active_templates(@project)
@project.create_labels
end
unless @project.import?
raise 'Failed to create repository' unless @project.create_repository
end
end end
end end
end end
def fail(error:) def fail(error:)
message = "Unable to save project. Error: #{error}" message = "Unable to save project. Error: #{error}"
message << "Project ID: #{@project.id}" if @project && @project.id log_message = message.dup
Rails.logger.error(message) log_message << " Project ID: #{@project.id}" if @project&.id
Rails.logger.error(log_message)
if @project && @project.import? if @project
@project.errors.add(:base, message) @project.errors.add(:base, message)
@project.mark_import_as_failed(message) @project.mark_import_as_failed(message) if @project.import?
end end
@project @project
......
...@@ -81,11 +81,13 @@ module Projects ...@@ -81,11 +81,13 @@ module Projects
end end
def extract_tar_archive!(temp_path) def extract_tar_archive!(temp_path)
results = Open3.pipeline(%W(gunzip -c #{artifacts}), build.artifacts_file.use_file do |artifacts_path|
%W(dd bs=#{BLOCK_SIZE} count=#{blocks}), results = Open3.pipeline(%W(gunzip -c #{artifacts_path}),
%W(tar -x -C #{temp_path} #{SITE_PATH}), %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
err: '/dev/null') %W(tar -x -C #{temp_path} #{SITE_PATH}),
raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?) err: '/dev/null')
raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
end
end end
def extract_zip_archive!(temp_path) def extract_zip_archive!(temp_path)
...@@ -103,8 +105,10 @@ module Projects ...@@ -103,8 +105,10 @@ module Projects
# -n never overwrite existing files # -n never overwrite existing files
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*') site_path = File.join(SITE_PATH, '*')
unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path})) build.artifacts_file.use_file do |artifacts_path|
raise FailedToExtractError, 'pages failed to extract' unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
raise FailedToExtractError, 'pages failed to extract'
end
end end
end end
......
module ProtectedBranches module ProtectedBranches
class CreateService < BaseService class CreateService < BaseService
attr_reader :protected_branch
def execute(skip_authorization: false) def execute(skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project) raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?
protected_branch.save
protected_branch
end
def authorized?
can?(current_user, :create_protected_branch, protected_branch)
end
private
project.protected_branches.create(params) def protected_branch
@protected_branch ||= project.protected_branches.new(params)
end end
end end
end end
module ProtectedBranches
class DestroyService < BaseService
def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_protected_branch, protected_branch)
protected_branch.destroy
end
end
end
module ProtectedBranches module ProtectedBranches
class UpdateService < BaseService class UpdateService < BaseService
def execute(protected_branch) def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch)
protected_branch.update(params) protected_branch.update(params)
protected_branch protected_branch
......
module ProtectedTags
class DestroyService < BaseService
def execute(protected_tag)
protected_tag.destroy
end
end
end
...@@ -34,7 +34,8 @@ class VerifyPagesDomainService < BaseService ...@@ -34,7 +34,8 @@ class VerifyPagesDomainService < BaseService
# Prevent any pre-existing grace period from being truncated # Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max
domain.update!(verified_at: Time.now, enabled_until: reverify) domain.assign_attributes(verified_at: Time.now, enabled_until: reverify)
domain.save!(validate: false)
if was_disabled if was_disabled
notify(:enabled) notify(:enabled)
...@@ -47,7 +48,9 @@ class VerifyPagesDomainService < BaseService ...@@ -47,7 +48,9 @@ class VerifyPagesDomainService < BaseService
def unverify_domain! def unverify_domain!
if domain.verified? if domain.verified?
domain.update!(verified_at: nil) domain.assign_attributes(verified_at: nil)
domain.save!(validate: false)
notify(:verification_failed) notify(:verification_failed)
end end
...@@ -55,7 +58,8 @@ class VerifyPagesDomainService < BaseService ...@@ -55,7 +58,8 @@ class VerifyPagesDomainService < BaseService
end end
def disable_domain! def disable_domain!
domain.update!(verified_at: nil, enabled_until: nil) domain.assign_attributes(verified_at: nil, enabled_until: nil)
domain.save!(validate: false)
notify(:disabled) notify(:disabled)
......
class AttachmentUploader < GitlabUploader class AttachmentUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern
storage :file prepend ObjectStorage::Extension::RecordsUploads
include UploaderHelper
private private
......
class AvatarUploader < GitlabUploader class AvatarUploader < GitlabUploader
include UploaderHelper include UploaderHelper
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern
storage :file prepend ObjectStorage::Extension::RecordsUploads
def exists? def exists?
model.avatar.file && model.avatar.file.present? model.avatar.file && model.avatar.file.present?
end end
def move_to_cache def move_to_store
false false
end end
def move_to_store def move_to_cache
false false
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment