Commit a582ee2f authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge branch 'master' into droplab-templating-xss-fix

parents b34534b6 cd041082
This diff is collapsed.
...@@ -16,47 +16,44 @@ const defaults = { ...@@ -16,47 +16,44 @@ const defaults = {
class BlobForkSuggestion { class BlobForkSuggestion {
constructor(options) { constructor(options) {
this.elementMap = Object.assign({}, defaults, options); this.elementMap = Object.assign({}, defaults, options);
this.onClickWrapper = this.onClick.bind(this); this.onOpenButtonClick = this.onOpenButtonClick.bind(this);
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
document.addEventListener('click', this.onClickWrapper);
} }
showSuggestionSection(forkPath, action = 'edit') { init() {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => { this.bindEvents();
suggestionSection.classList.remove('hidden');
});
[].forEach.call(this.elementMap.forkButtons, (forkButton) => { return this;
forkButton.setAttribute('href', forkPath); }
});
[].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => { bindEvents() {
// eslint-disable-next-line no-param-reassign $(this.elementMap.openButtons).on('click', this.onOpenButtonClick);
actionTextPiece.textContent = action; $(this.elementMap.cancelButtons).on('click', this.onCancelButtonClick);
});
} }
hideSuggestionSection() { showSuggestionSection(forkPath, action = 'edit') {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => { $(this.elementMap.suggestionSections).removeClass('hidden');
suggestionSection.classList.add('hidden'); $(this.elementMap.forkButtons).attr('href', forkPath);
}); $(this.elementMap.actionTextPieces).text(action);
} }
onClick(e) { hideSuggestionSection() {
const el = e.target; $(this.elementMap.suggestionSections).addClass('hidden');
}
if ([].includes.call(this.elementMap.openButtons, el)) { onOpenButtonClick(e) {
const { forkPath, action } = el.dataset; const forkPath = $(e.currentTarget).attr('data-fork-path');
this.showSuggestionSection(forkPath, action); const action = $(e.currentTarget).attr('data-action');
} this.showSuggestionSection(forkPath, action);
}
if ([].includes.call(this.elementMap.cancelButtons, el)) { onCancelButtonClick() {
this.hideSuggestionSection(); this.hideSuggestionSection();
}
} }
destroy() { destroy() {
document.removeEventListener('click', this.onClickWrapper); $(this.elementMap.openButtons).off('click', this.onOpenButtonClick);
$(this.elementMap.cancelButtons).off('click', this.onCancelButtonClick);
} }
} }
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab'; import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource); Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => { export default () => {
const el = document.getElementById('js-notebook-viewer'); const el = document.getElementById('js-notebook-viewer');
...@@ -19,6 +18,9 @@ export default () => { ...@@ -19,6 +18,9 @@ export default () => {
json: {}, json: {},
}; };
}, },
components: {
notebookLab,
},
template: ` template: `
<div class="container-fluid md prepend-top-default append-bottom-default"> <div class="container-fluid md prepend-top-default append-bottom-default">
<div <div
......
...@@ -97,7 +97,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -97,7 +97,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
}); })
.init();
} }
switch (page) { switch (page) {
......
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue';
export default {
components: {
'code-cell': CodeCell,
'output-cell': OutputCell,
},
props: {
cell: {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
computed: {
rawInputCode() {
if (this.cell.source) {
return this.cell.source.join('');
}
return '';
},
hasOutput() {
return this.cell.outputs.length;
},
output() {
return this.cell.outputs[0];
},
},
};
</script>
<style scoped>
.cell {
flex-direction: column;
}
</style>
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<script>
import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
export default {
components: {
prompt: Prompt,
},
props: {
count: {
type: Number,
required: false,
default: 0,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
},
computed: {
code() {
return this.rawCode;
},
promptType() {
const type = this.type.split('put')[0];
return type.charAt(0).toUpperCase() + type.slice(1);
},
},
mounted() {
Prism.highlightElement(this.$refs.code);
},
};
</script>
export { default as MarkdownCell } from './markdown.vue';
export { default as CodeCell } from './code.vue';
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<script>
/* global katex */
import marked from 'marked';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
/*
Regex to match KaTex blocks.
Supports the following:
\begin{equation}<math>\end{equation}
$$<math>$$
inline $<math>$
The matched text then goes through the KaTex renderer & then outputs the HTML
*/
const katexRegexString = `(
^\\\\begin{[a-zA-Z]+}\\s
|
^\\$\\$
|
\\s\\$(?!\\$)
)
(.+?)
(
\\s\\\\end{[a-zA-Z]+}$
|
\\$\\$$
|
\\$
)
`.replace(/\s/g, '').trim();
renderer.paragraph = (t) => {
let text = t;
let inline = false;
if (typeof katex !== 'undefined') {
const katexString = text.replace(/\\/g, '\\');
const matches = new RegExp(katexRegexString, 'gi').exec(katexString);
if (matches && matches.length > 0) {
if (matches[1].trim() === '$' && matches[3].trim() === '$') {
inline = true;
text = `${katexString.replace(matches[0], '')} ${katex.renderToString(matches[2])}`;
} else {
text = katex.renderToString(matches[2]);
}
}
}
return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`;
};
marked.setOptions({
sanitize: true,
renderer,
});
export default {
components: {
prompt: Prompt,
},
props: {
cell: {
type: Object,
required: true,
},
},
computed: {
markdown() {
return marked(this.cell.source.join(''));
},
},
};
</script>
<style>
.markdown .katex {
display: block;
text-align: center;
}
.markdown .inline-katex .katex {
display: inline;
text-align: initial;
}
</style>
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<script>
import Prompt from '../prompt.vue';
export default {
props: {
rawCode: {
type: String,
required: true,
},
},
components: {
prompt: Prompt,
},
};
</script>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<script>
import Prompt from '../prompt.vue';
export default {
props: {
outputType: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
},
components: {
prompt: Prompt,
},
};
</script>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<script>
import CodeCell from '../code/index.vue';
import Html from './html.vue';
import Image from './image.vue';
export default {
props: {
codeCssClass: {
type: String,
required: false,
default: '',
},
count: {
type: Number,
required: false,
default: 0,
},
output: {
type: Object,
requred: true,
},
},
components: {
'code-cell': CodeCell,
'html-output': Html,
'image-output': Image,
},
data() {
return {
outputType: '',
};
},
computed: {
componentName() {
if (this.output.text) {
return 'code-cell';
} else if (this.output.data['image/png']) {
this.outputType = 'image/png';
return 'image-output';
} else if (this.output.data['text/html']) {
this.outputType = 'text/html';
return 'html-output';
} else if (this.output.data['image/svg+xml']) {
this.outputType = 'image/svg+xml';
return 'html-output';
}
this.outputType = 'text/plain';
return 'code-cell';
},
rawCode() {
if (this.output.text) {
return this.output.text.join('');
}
return this.dataForType(this.outputType);
},
},
methods: {
dataForType(type) {
let data = this.output.data[type];
if (typeof data === 'object') {
data = data.join('');
}
return data;
},
},
};
</script>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: false,
},
count: {
type: Number,
required: false,
},
},
};
</script>
<style scoped>
.prompt {
padding: 0 10px;
min-width: 7em;
font-family: monospace;
}
</style>
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import {
MarkdownCell,
CodeCell,
} from './cells';
export default {
components: {
'code-cell': CodeCell,
'markdown-cell': MarkdownCell,
},
props: {
notebook: {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
methods: {
cellType(type) {
return `${type}-cell`;
},
},
computed: {
cells() {
if (this.notebook.worksheets) {
const data = {
cells: [],
};
return this.notebook.worksheets.reduce((cellData, sheet) => {
const cellDataCopy = cellData;
cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
return cellDataCopy;
}, data).cells;
}
return this.notebook.cells;
},
hasNotebook() {
return Object.keys(this.notebook).length;
},
},
};
</script>
<style>
.cell,
.input,
.output {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.cell pre {
margin: 0;
width: 100%;
}
</style>
import Prism from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/plugins/custom-class/prism-custom-class';
Prism.plugins.customClass.map({
comment: 'c',
error: 'err',
operator: 'o',
constant: 'kc',
namespace: 'kn',
keyword: 'k',
string: 's',
number: 'm',
'attr-name': 'na',
builtin: 'nb',
entity: 'ni',
function: 'nf',
tag: 'nt',
variable: 'nv',
});
export default Prism;
...@@ -387,6 +387,7 @@ ...@@ -387,6 +387,7 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
display: flex; display: flex;
width: 100%; width: 100%;
margin-bottom: 10px;
.comment-btn { .comment-btn {
flex-grow: 1; flex-grow: 1;
......
...@@ -386,6 +386,10 @@ ul.notes { ...@@ -386,6 +386,10 @@ ul.notes {
.note-headline-meta { .note-headline-meta {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
.system-note-message {
white-space: normal;
}
} }
/** /**
......
...@@ -23,6 +23,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -23,6 +23,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@project_namespace = @project.namespace.becomes(Namespace)
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
end end
......
##
# DEPRECATED
#
# These helpers are deprecated in favor of detailed CI/CD statuses.
#
# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
#
module CiStatusHelper module CiStatusHelper
def ci_status_path(pipeline) def ci_status_path(pipeline)
project = pipeline.project project = pipeline.project
namespace_project_pipeline_path(project.namespace, project, pipeline) namespace_project_pipeline_path(project.namespace, project, pipeline)
end end
# Is used by Commit and Merge Request Widget
def ci_label_for_status(status) def ci_label_for_status(status)
if detailed_status?(status) if detailed_status?(status)
return status.label return status.label
...@@ -22,6 +28,23 @@ module CiStatusHelper ...@@ -22,6 +28,23 @@ module CiStatusHelper
end end
end end
def ci_text_for_status(status)
if detailed_status?(status)
return status.text
end
case status
when 'success'
'passed'
when 'success_with_warnings'
'passed'
when 'manual'
'blocked'
else
status
end
end
def ci_status_for_statuseable(subject) def ci_status_for_statuseable(subject)
status = subject.try(:status) || 'not found' status = subject.try(:status) || 'not found'
status.humanize status.humanize
......
...@@ -7,6 +7,11 @@ module IconsHelper ...@@ -7,6 +7,11 @@ module IconsHelper
# font-awesome-rails gem, but should we ever use a different icon pack in the # font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls. # future we won't have to change hundreds of method calls.
def icon(names, options = {}) def icon(names, options = {})
if (options.keys & %w[aria-hidden aria-label]).empty?
# Add `aria-hidden` if there are no aria's set
options['aria-hidden'] = true
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end end
......
...@@ -75,29 +75,32 @@ module Ci ...@@ -75,29 +75,32 @@ module Ci
pipeline.update_duration pipeline.update_duration
end end
before_transition any => [:manual] do |pipeline|
pipeline.update_duration
end
before_transition canceled: any - [:canceled] do |pipeline| before_transition canceled: any - [:canceled] do |pipeline|
pipeline.auto_canceled_by = nil pipeline.auto_canceled_by = nil
end end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end end
after_transition any => [:success] do |pipeline| after_transition any => [:success] do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end end
after_transition [:created, :pending, :running] => :success do |pipeline| after_transition [:created, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) } pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
end end
after_transition do |pipeline, transition| after_transition do |pipeline, transition|
next if transition.loopback? next if transition.loopback?
pipeline.run_after_commit do pipeline.run_after_commit do
PipelineHooksWorker.perform_async(id) PipelineHooksWorker.perform_async(pipeline.id)
Ci::ExpirePipelineCacheService.new(project, nil) ExpirePipelineCacheWorker.perform_async(pipeline.id)
.execute(pipeline)
end end
end end
...@@ -385,6 +388,11 @@ module Ci ...@@ -385,6 +388,11 @@ module Ci
.select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
end end
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
@all_merge_requests ||= project.merge_requests.where(source_branch: ref)
end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -163,7 +163,20 @@ module Routable ...@@ -163,7 +163,20 @@ module Routable
end end
end end
# Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path,
# a new instance is instantiated, and we end up duplicating the same query to retrieve
# the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active?
key = "routable/full_path/#{self.class.name}/#{self.id}"
RequestStore[key] ||= uncached_full_path
end
private
def uncached_full_path
if route && route.path.present? if route && route.path.present?
@full_path ||= route.path @full_path ||= route.path
else else
...@@ -173,8 +186,6 @@ module Routable ...@@ -173,8 +186,6 @@ module Routable
end end
end end
private
def full_name_changed? def full_name_changed?
name_changed? || parent_changed? name_changed? || parent_changed?
end end
......
...@@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion ...@@ -10,4 +10,8 @@ class IndividualNoteDiscussion < Discussion
def individual_note? def individual_note?
true true
end end
def reply_attributes
super.tap { |attrs| attrs.delete(:discussion_id) }
end
end end
...@@ -15,8 +15,12 @@ class OutOfContextDiscussion < Discussion ...@@ -15,8 +15,12 @@ class OutOfContextDiscussion < Discussion
def self.override_discussion_id(note) def self.override_discussion_id(note)
discussion_id(note) discussion_id(note)
end end
def self.note_class def self.note_class
Note Note
end end
def reply_attributes
super.tap { |attrs| attrs.delete(:discussion_id) }
end
end end
module Ci
class ExpirePipelineCacheService < BaseService
attr_reader :pipeline
def execute(pipeline)
@pipeline = pipeline
store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path)
store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) }
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end
private
def project_pipelines_path
Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
project.namespace,
project,
format: :json)
end
def commit_pipelines_path
Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
project.namespace,
project,
pipeline.commit.id,
format: :json)
end
def new_merge_request_pipelines_path
Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
project.namespace,
project,
format: :json)
end
def merge_requests_pipelines_paths
pipeline.merge_requests.collect do |merge_request|
Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
project.namespace,
project,
merge_request,
format: :json)
end
end
end
end
- ref = local_assigns.fetch(:ref) - ref = local_assigns.fetch(:ref)
- status = commit.status(ref) - status = commit.status(ref)
- if status - if status
= link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status) = ci_icon_for_status(status)
= ci_label_for_status(status) = ci_text_for_status(status)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
......
-# @project is present when viewing Project's milestone -# @project is present when viewing Project's milestone
- project = @project || issuable.project - project = @project || issuable.project
- namespace = @project_namespace || project.namespace.becomes(Namespace)
- assignee = issuable.assignee - assignee = issuable.assignee
- issuable_type = issuable.class.table_name - issuable_type = issuable.class.table_name
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type] - base_url_args = [namespace, project]
- issuable_type_args = base_url_args + [issuable_type]
- issuable_url_args = base_url_args + [issuable]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable) - can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) } %li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable_url_args) }
%span %span
- if show_project_name - if show_project_name
%strong #{project.name} &middot; %strong #{project.name} &middot;
...@@ -13,17 +16,17 @@ ...@@ -13,17 +16,17 @@
%strong #{project.name_with_namespace} &middot; %strong #{project.name_with_namespace} &middot;
- if issuable.is_a?(Issue) - if issuable.is_a?(Issue)
= confidential_icon(issuable) = confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title = link_to_gfm issuable.title, issuable_url_args, title: issuable.title
.issuable-detail .issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do = link_to [project.namespace.becomes(Namespace), project, issuable] do
%span.issuable-number= issuable.to_reference %span.issuable-number= issuable.to_reference
- issuable.labels.each do |label| - issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- render_colored_label(label) - render_colored_label(label)
%span.assignee-icon %span.assignee-icon
- if assignee - if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
class ExpirePipelineCacheWorker
include Sidekiq::Worker
include PipelineQueue
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
project = pipeline.project
store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path(project))
store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path)
end
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
end
private
def project_pipelines_path(project)
Gitlab::Routing.url_helpers.namespace_project_pipelines_path(
project.namespace,
project,
format: :json)
end
def commit_pipelines_path(project, commit)
Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path(
project.namespace,
project,
commit.id,
format: :json)
end
def new_merge_request_pipelines_path(project)
Gitlab::Routing.url_helpers.new_namespace_project_merge_request_path(
project.namespace,
project,
format: :json)
end
def each_pipelines_merge_request_path(project, pipeline)
pipeline.all_merge_requests.each do |merge_request|
path = Gitlab::Routing.url_helpers.pipelines_namespace_project_merge_request_path(
project.namespace,
project,
merge_request,
format: :json)
yield(path)
end
end
end
---
title: Database SSL support for backup script.
merge_request: 9715
author: Guillaume Simon
---
title: Remove unnecessary test helpers includes
merge_request: 10567
author: Jacopo Beschi @jacopo-beschi
---
title: Allow OAuth clients to push code
merge_request: 10677
author:
---
title: Fixes an issue preventing screen readers from reading some icons
merge_request:
author:
---
title: Add index on ci_builds.updated_at
merge_request: 10870
author: blackst0ne
---
title: Fixed spacing of discussion submit buttons
merge_request:
author:
---
title: Ensure replying to an individual note by email creates a note with its own
discussion ID
merge_request:
author:
---
title: Fix commenting on an existing discussion on an unchanged line that is no longer
in the diff
merge_request:
author:
---
title: Fix missing duration for blocked pipelines
merge_request: 10856
author:
---
title: Fix lastest commit status text on main project page
merge_request: 10863
author:
---
title: Fix updating merge_when_build_succeeds via merge API endpoint
merge_request: 10873
author:
---
title: Cache Routable#full_path in RequestStore to reduce duplicate route loads
merge_request:
author:
---
title: Eliminate N+1 queries in loading namespaces for every issuable in milestones
merge_request:
author:
---
title: Properly expire cache for all MRs of a pipeline
merge_request: 10770
author:
...@@ -25,6 +25,7 @@ development: ...@@ -25,6 +25,7 @@ development:
pool: 5 pool: 5
username: root username: root
password: "secure password" password: "secure password"
# host: localhost
# socket: /tmp/mysql.sock # socket: /tmp/mysql.sock
# Warning: The database defined as "test" will be erased and # Warning: The database defined as "test" will be erased and
...@@ -39,4 +40,5 @@ test: &test ...@@ -39,4 +40,5 @@ test: &test
pool: 5 pool: 5
username: root username: root
password: password:
# host: localhost
# socket: /tmp/mysql.sock # socket: /tmp/mysql.sock
...@@ -9,7 +9,7 @@ production: ...@@ -9,7 +9,7 @@ production:
# username: git # username: git
# password: # password:
# host: localhost # host: localhost
# port: 5432 # port: 5432
# #
# Development specific # Development specific
...@@ -21,6 +21,7 @@ development: ...@@ -21,6 +21,7 @@ development:
pool: 5 pool: 5
username: postgres username: postgres
password: password:
# host: localhost
# #
# Staging specific # Staging specific
...@@ -32,6 +33,7 @@ staging: ...@@ -32,6 +33,7 @@ staging:
pool: 5 pool: 5
username: postgres username: postgres
password: password:
# host: localhost
# Warning: The database defined as "test" will be erased and # Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake". # re-generated from your development database when you run "rake".
...@@ -43,3 +45,4 @@ test: &test ...@@ -43,3 +45,4 @@ test: &test
pool: 5 pool: 5
username: postgres username: postgres
password: password:
# host: localhost
...@@ -505,6 +505,11 @@ production: &base ...@@ -505,6 +505,11 @@ production: &base
# If you use non-standard ssh port you need to specify it # If you use non-standard ssh port you need to specify it
# ssh_port: 22 # ssh_port: 22
workhorse:
# File that contains the secret key for verifying access for gitlab-workhorse.
# Default is '.gitlab_workhorse_secret' relative to Rails.root (i.e. root of the GitLab app).
# secret_file: /home/git/gitlab/.gitlab_workhorse_secret
## Git settings ## Git settings
# CAUTION! # CAUTION!
# Use the default values unless you really know what you are doing # Use the default values unless you really know what you are doing
......
...@@ -387,6 +387,12 @@ Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user ...@@ -387,6 +387,12 @@ Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user
Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user
Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.__send__(:build_gitlab_shell_ssh_path_prefix) Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.__send__(:build_gitlab_shell_ssh_path_prefix)
#
# Workhorse
#
Settings['workhorse'] ||= Settingslogic.new({})
Settings.workhorse['secret_file'] ||= Rails.root.join('.gitlab_workhorse_secret')
# #
# Repositories # Repositories
# #
......
...@@ -38,7 +38,7 @@ if Rails.env.test? ...@@ -38,7 +38,7 @@ if Rails.env.test?
end end
end end
if ENV.has_key?('CI') if ENV.has_key?('CI') && ENV['GITLAB_DATABASE'] == 'postgresql'
RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git) RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
RspecProfiling::Run.prepend(RspecProfilingExt::Run) RspecProfiling::Run.prepend(RspecProfilingExt::Run)
end end
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexOnCiBuildsUpdatedAt < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_builds, :updated_at
end
def down
remove_concurrent_index :ci_builds, :updated_at if index_exists?(:ci_builds, :updated_at)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170419001229) do ActiveRecord::Schema.define(version: 20170423064036) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -241,6 +241,7 @@ ActiveRecord::Schema.define(version: 20170419001229) do ...@@ -241,6 +241,7 @@ ActiveRecord::Schema.define(version: 20170419001229) do
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
create_table "ci_pipelines", force: :cascade do |t| create_table "ci_pipelines", force: :cascade do |t|
t.string "ref" t.string "ref"
......
# Gitaly # Gitaly
[Gitaly](https://gitlab.com/gitlab-org/gitlay) (introduced in GitLab [Gitaly](https://gitlab.com/gitlab-org/gitaly) (introduced in GitLab
9.0) is a service that provides high-level RPC access to Git 9.0) is a service that provides high-level RPC access to Git
repositories. As of GitLab 9.1 it is still an optional component with repositories. As of GitLab 9.1 it is still an optional component with
limited scope. limited scope.
......
...@@ -120,7 +120,7 @@ Example of response ...@@ -120,7 +120,7 @@ Example of response
Get a list of jobs for a pipeline. Get a list of jobs for a pipeline.
``` ```
GET /projects/:id/pipeline/:pipeline_id/jobs GET /projects/:id/pipelines/:pipeline_id/jobs
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
......
...@@ -859,6 +859,17 @@ Parameters: ...@@ -859,6 +859,17 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `file` | string | yes | The file to be uploaded | | `file` | string | yes | The file to be uploaded |
To upload a file from your filesystem, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded
by `@`. For example:
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v3/projects/5/uploads
```
Returned object:
```json ```json
{ {
"alt": "dk", "alt": "dk",
...@@ -868,8 +879,8 @@ Parameters: ...@@ -868,8 +879,8 @@ Parameters:
``` ```
**Note**: The returned `url` is relative to the project path. **Note**: The returned `url` is relative to the project path.
In Markdown contexts, the link is automatically expanded when the format in `markdown` is used. In Markdown contexts, the link is automatically expanded when the format in
`markdown` is used.
## Project members ## Project members
......
...@@ -75,7 +75,7 @@ sharing a Merge Request with a reviewer or a maintainer. ...@@ -75,7 +75,7 @@ sharing a Merge Request with a reviewer or a maintainer.
1. Follow the steps in [Vue.js Best Practices](vue.md) 1. Follow the steps in [Vue.js Best Practices](vue.md)
1. Follow the style guide. 1. Follow the style guide.
1. Only a handful of people are allowed to merge Vue related features. 1. Only a handful of people are allowed to merge Vue related features.
Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process. Reach out to one of Vue experts early in this process.
--- ---
......
...@@ -470,10 +470,8 @@ with setting up Gitaly until you upgrade to GitLab 9.2 or later. ...@@ -470,10 +470,8 @@ with setting up Gitaly until you upgrade to GitLab 9.2 or later.
sudo chmod 0700 /home/git/gitlab/tmp/sockets/private sudo chmod 0700 /home/git/gitlab/tmp/sockets/private
sudo chown git /home/git/gitlab/tmp/sockets/private sudo chown git /home/git/gitlab/tmp/sockets/private
# Configure Gitaly
cd /home/git/gitaly
sudo -u git cp config.toml.example config.toml
# If you are using non-default settings you need to update config.toml # If you are using non-default settings you need to update config.toml
cd /home/git/gitaly
sudo -u git -H editor config.toml sudo -u git -H editor config.toml
# Enable Gitaly in the init script # Enable Gitaly in the init script
......
...@@ -317,6 +317,17 @@ the socket path, but with `unix:` in front. ...@@ -317,6 +317,17 @@ the socket path, but with `unix:` in front.
Each entry under `storages:` should use the same `gitaly_address`. Each entry under `storages:` should use the same `gitaly_address`.
#### Compile Gitaly
This step will also create `config.toml.example` which you need below.
```shell
cd /home/git/gitaly
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
sudo -u git -H make
```
#### Gitaly config.toml #### Gitaly config.toml
In GitLab 9.1 we are replacing environment variables in Gitaly with a In GitLab 9.1 we are replacing environment variables in Gitaly with a
......
# Health Check # Health Check
> [Introduced][ce-3888] in GitLab 8.8. >**Notes:**
- Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1.
GitLab provides a health check endpoint for uptime monitoring on the `health_check` web - The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and will
endpoint. The health check reports on the overall system status based on the status of be deprecated in GitLab 9.1. Read more in the [old behavior](#old-behavior)
the database connection, the state of the database migrations, and the ability to write section.
and access the cache. This endpoint can be provided to uptime monitoring services like
[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health]. GitLab provides liveness and readiness probes to indicate service health and
reachability to required services. These probes report on the status of the
database connection, Redis connection, and access to the filesystem. These
endpoints [can be provided to schedulers like Kubernetes][kubernetes] to hold
traffic until the system is ready or restart the container as needed.
## Access Token ## Access Token
An access token needs to be provided while accessing the health check endpoint. The current An access token needs to be provided while accessing the probe endpoints. The current
accepted token can be found on the `admin/health_check` page of your GitLab instance. accepted token can be found under the **Admin area ➔ Monitoring ➔ Health check**
(`admin/health_check`) page of your GitLab instance.
![access token](img/health_check_token.png) ![access token](img/health_check_token.png)
The access token can be passed as a URL parameter: The access token can be passed as a URL parameter:
``` ```
https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN https://gitlab.example.com/-/readiness?token=ACCESS_TOKEN
``` ```
or as an HTTP header: which will then provide a report of system health in JSON format:
```bash ```
curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json {
"db_check": {
"status": "ok"
},
"redis_check": {
"status": "ok"
},
"fs_shards_check": {
"status": "ok",
"labels": {
"shard": "default"
}
}
}
``` ```
## Using the Endpoint ## Using the Endpoint
Once you have the access token, health information can be retrieved as plain text, JSON, Once you have the access token, the probes can be accessed:
or XML using the `health_check` endpoint:
- `https://gitlab.example.com/-/readiness?token=ACCESS_TOKEN`
- `https://gitlab.example.com/-/liveness?token=ACCESS_TOKEN`
## Status
On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
will return a valid successful HTTP status code, and a `success` message.
## Old behavior
>**Notes:**
- Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1.
- The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and will
be deprecated in GitLab 9.1. Read more in the [old behavior](#old-behavior)
section.
GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
endpoint. The health check reports on the overall system status based on the status of
the database connection, the state of the database migrations, and the ability to write
and access the cache. This endpoint can be provided to uptime monitoring services like
[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health].
Once you have the [access token](#access-token), health information can be
retrieved as plain text, JSON, or XML using the `health_check` endpoint:
- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN` - `https://gitlab.example.com/health_check?token=ACCESS_TOKEN`
- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN` - `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN`
...@@ -54,13 +96,13 @@ would be like: ...@@ -54,13 +96,13 @@ would be like:
{"healthy":true,"message":"success"} {"healthy":true,"message":"success"}
``` ```
## Status
On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint
will return a valid successful HTTP status code, and a `success` message. Ideally your will return a valid successful HTTP status code, and a `success` message. Ideally your
uptime monitoring should look for the success message. uptime monitoring should look for the success message.
[ce-10416]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 [ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888
[pingdom]: https://www.pingdom.com [pingdom]: https://www.pingdom.com
[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html [nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring [newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
[kubernetes]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
...@@ -60,7 +60,9 @@ Feature: Profile ...@@ -60,7 +60,9 @@ Feature: Profile
Then I should see a password error message Then I should see a password error message
Scenario: I visit history tab Scenario: I visit history tab
Given I have activity Given I logout
And I sign in via the UI
And I have activity
When I visit Audit Log page When I visit Audit Log page
Then I should see my activity Then I should see my activity
......
...@@ -41,8 +41,7 @@ Feature: Project Forked Merge Requests ...@@ -41,8 +41,7 @@ Feature: Project Forked Merge Requests
@javascript @javascript
Scenario: I see the users in the target project for a new merge request Scenario: I see the users in the target project for a new merge request
Given I logout Given I sign in as an admin
And I sign in as an admin
And I have a project forked off of "Shop" called "Forked Shop" And I have a project forked off of "Shop" called "Forked Shop"
Then I visit project "Forked Shop" merge requests page Then I visit project "Forked Shop" merge requests page
And I click link "New Merge Request" And I click link "New Merge Request"
......
...@@ -6,7 +6,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps ...@@ -6,7 +6,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
include Select2Helper include Select2Helper
step 'I am a member of project "Shop"' do step 'I am a member of project "Shop"' do
@project = Project.find_by(name: "Shop") @project = ::Project.find_by(name: "Shop")
@project ||= create(:project, :repository, name: "Shop") @project ||= create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter] @project.team << [@user, :reporter]
end end
......
...@@ -43,7 +43,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps ...@@ -43,7 +43,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
end end
step 'I am signed in as a developer of the project' do step 'I am signed in as a developer of the project' do
login_as(@user) sign_in(@user)
end end
step 'I should see merge request merged' do step 'I should see merge request merged' do
......
...@@ -31,7 +31,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps ...@@ -31,7 +31,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'I am signed in as a developer of the project' do step 'I am signed in as a developer of the project' do
@user = create(:user) { |u| @project.add_developer(u) } @user = create(:user) { |u| @project.add_developer(u) }
login_as(@user) sign_in(@user)
end end
step 'There is an open Merge Request' do step 'There is an open Merge Request' do
......
...@@ -7,7 +7,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -7,7 +7,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
include SharedMarkdown include SharedMarkdown
step 'I own project "Delta"' do step 'I own project "Delta"' do
@project = Project.find_by(name: "Delta") @project = ::Project.find_by(name: "Delta")
@project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace) @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
@project.team << [@user, :master] @project.team << [@user, :master]
end end
......
require Rails.root.join('spec', 'support', 'login_helpers') require Rails.root.join('features', 'support', 'login_helpers')
module SharedAuthentication module SharedAuthentication
include Spinach::DSL include Spinach::DSL
include LoginHelpers include LoginHelpers
step 'I sign in as a user' do step 'I sign in as a user' do
login_as :user sign_out(@user) if @user
@user = create(:user)
sign_in(@user)
end
step 'I sign in via the UI' do
gitlab_sign_in(create(:user))
end end
step 'I sign in as an admin' do step 'I sign in as an admin' do
login_as :admin sign_out(@user) if @user
@user = create(:admin)
sign_in(@user)
end end
step 'I sign in as "John Doe"' do step 'I sign in as "John Doe"' do
login_with(user_exists("John Doe")) gitlab_sign_in(user_exists("John Doe"))
end end
step 'I sign in as "Mary Jane"' do step 'I sign in as "Mary Jane"' do
login_with(user_exists("Mary Jane")) gitlab_sign_in(user_exists("Mary Jane"))
end end
step 'I should be redirected to sign in page' do step 'I should be redirected to sign in page' do
...@@ -25,14 +35,41 @@ module SharedAuthentication ...@@ -25,14 +35,41 @@ module SharedAuthentication
end end
step "I logout" do step "I logout" do
logout gitlab_sign_out
end end
step "I logout directly" do step "I logout directly" do
logout_direct gitlab_sign_out
end end
def current_user def current_user
@user || User.reorder(nil).first @user || User.reorder(nil).first
end end
private
def gitlab_sign_in(user)
visit new_user_session_path
fill_in "user_login", with: user.email
fill_in "user_password", with: "12345678"
check 'user_remember_me'
click_button "Sign in"
@user = user
end
def gitlab_sign_out
return unless @user
if Capybara.current_driver == Capybara.javascript_driver
find('.header-user-dropdown-toggle').click
click_link 'Sign out'
expect(page).to have_button('Sign in')
else
sign_out(@user)
end
@user = nil
end
end end
module LoginHelpers
# After inclusion, IntegrationHelpers calls these two methods that aren't
# supported by Spinach, so we perform the end results ourselves
class << self
def setup(*args)
Spinach.hooks.before_scenario do
Warden.test_mode!
end
end
def teardown(*args)
Spinach.hooks.after_scenario do
Warden.test_reset!
end
end
end
include Devise::Test::IntegrationHelpers
end
...@@ -197,14 +197,15 @@ module API ...@@ -197,14 +197,15 @@ module API
end end
put ':id/merge_requests/:merge_request_iid/merge' do put ':id/merge_requests/:merge_request_iid/merge' do
merge_request = find_project_merge_request(params[:merge_request_iid]) merge_request = find_project_merge_request(params[:merge_request_iid])
merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
# Merge request can not be merged # Merge request can not be merged
# because user dont have permissions to push into target branch # because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user) unauthorized! unless merge_request.can_be_merged_by?(current_user)
not_allowed! unless merge_request.mergeable_state? not_allowed! unless merge_request.mergeable_state?(skip_ci_check: merge_when_pipeline_succeeds)
render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds)
if params[:sha] && merge_request.diff_head_sha != params[:sha] if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
...@@ -215,7 +216,7 @@ module API ...@@ -215,7 +216,7 @@ module API
should_remove_source_branch: params[:should_remove_source_branch] should_remove_source_branch: params[:should_remove_source_branch]
} }
if params[:merge_when_pipeline_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService ::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user, merge_params) .new(merge_request.target_project, current_user, merge_params)
.execute(merge_request) .execute(merge_request)
......
...@@ -80,16 +80,32 @@ module Backup ...@@ -80,16 +80,32 @@ module Backup
'port' => '--port', 'port' => '--port',
'socket' => '--socket', 'socket' => '--socket',
'username' => '--user', 'username' => '--user',
'encoding' => '--default-character-set' 'encoding' => '--default-character-set',
# SSL
'sslkey' => '--ssl-key',
'sslcert' => '--ssl-cert',
'sslca' => '--ssl-ca',
'sslcapath' => '--ssl-capath',
'sslcipher' => '--ssl-cipher'
} }
args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
end end
def pg_env def pg_env
ENV['PGUSER'] = config["username"] if config["username"] args = {
ENV['PGHOST'] = config["host"] if config["host"] 'username' => 'PGUSER',
ENV['PGPORT'] = config["port"].to_s if config["port"] 'host' => 'PGHOST',
ENV['PGPASSWORD'] = config["password"].to_s if config["password"] 'port' => 'PGPORT',
'password' => 'PGPASSWORD',
# SSL
'sslmode' => 'PGSSLMODE',
'sslkey' => 'PGSSLKEY',
'sslcert' => 'PGSSLCERT',
'sslrootcert' => 'PGSSLROOTCERT',
'sslcrl' => 'PGSSLCRL',
'sslcompression' => 'PGSSLCOMPRESSION'
}
args.each { |opt, arg| ENV[arg] = config[opt].to_s if config[opt] }
end end
def report_success(success) def report_success(success)
......
...@@ -108,7 +108,7 @@ module Gitlab ...@@ -108,7 +108,7 @@ module Gitlab
token = Doorkeeper::AccessToken.by_token(password) token = Doorkeeper::AccessToken.by_token(password)
if valid_oauth_token?(token) if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id) user = User.find_by(id: token.resource_owner_id)
Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities) Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
end end
end end
end end
......
...@@ -82,7 +82,7 @@ module Gitlab ...@@ -82,7 +82,7 @@ module Gitlab
file_diff, old_line, new_line = results file_diff, old_line, new_line = results
Position.new( new_position = Position.new(
old_path: file_diff.old_path, old_path: file_diff.old_path,
new_path: file_diff.new_path, new_path: file_diff.new_path,
head_sha: new_diff_refs.head_sha, head_sha: new_diff_refs.head_sha,
...@@ -91,6 +91,13 @@ module Gitlab ...@@ -91,6 +91,13 @@ module Gitlab
old_line: old_line, old_line: old_line,
new_line: new_line new_line: new_line
) )
# If a position is found, but is not actually contained in the diff, for example
# because it was an unchanged line in the context of a change that was undone,
# we cannot return this as a successful trace.
return unless new_position.diff_line(repository)
new_position
end end
private private
......
...@@ -16,6 +16,10 @@ module Gitlab ...@@ -16,6 +16,10 @@ module Gitlab
def execute def execute
raise NotImplementedError raise NotImplementedError
end end
def metrics_params
{ handler: self.class.name }
end
end end
end end
end end
......
require 'gitlab/email/handler/base_handler' require 'gitlab/email/handler/base_handler'
module Gitlab module Gitlab
...@@ -37,6 +36,10 @@ module Gitlab ...@@ -37,6 +36,10 @@ module Gitlab
@project ||= Project.find_by_full_path(project_path) @project ||= Project.find_by_full_path(project_path)
end end
def metrics_params
super.merge(project: project)
end
private private
def create_issue def create_issue
......
...@@ -28,6 +28,10 @@ module Gitlab ...@@ -28,6 +28,10 @@ module Gitlab
record_name: 'comment') record_name: 'comment')
end end
def metrics_params
super.merge(project: project)
end
private private
def author def author
......
...@@ -19,6 +19,10 @@ module Gitlab ...@@ -19,6 +19,10 @@ module Gitlab
noteable.unsubscribe(sent_notification.recipient) noteable.unsubscribe(sent_notification.recipient)
end end
def metrics_params
super.merge(project: project)
end
private private
def sent_notification def sent_notification
......
require_dependency 'gitlab/email/handler' require_dependency 'gitlab/email/handler'
# Inspired in great part by Discourse's Email::Receiver # Inspired in great part by Discourse's Email::Receiver
...@@ -32,9 +31,7 @@ module Gitlab ...@@ -32,9 +31,7 @@ module Gitlab
raise UnknownIncomingEmail unless handler raise UnknownIncomingEmail unless handler
Gitlab::Metrics.add_event(:receive_email, Gitlab::Metrics.add_event(:receive_email, handler.metrics_params)
project: handler.try(:project),
handler: handler.class.name)
handler.execute handler.execute
end end
......
...@@ -168,7 +168,7 @@ module Gitlab ...@@ -168,7 +168,7 @@ module Gitlab
end end
def secret_path def secret_path
Rails.root.join('.gitlab_workhorse_secret') Gitlab.config.workhorse.secret_file
end end
def set_key_and_notify(key, value, expire: nil, overwrite: true) def set_key_and_notify(key, value, expire: nil, overwrite: true)
......
...@@ -2,6 +2,8 @@ namespace :gitlab do ...@@ -2,6 +2,8 @@ namespace :gitlab do
namespace :gitaly do namespace :gitaly do
desc "GitLab | Install or upgrade gitaly" desc "GitLab | Install or upgrade gitaly"
task :install, [:dir] => :environment do |t, args| task :install, [:dir] => :environment do |t, args|
require 'toml'
warn_user_is_not_gitlab warn_user_is_not_gitlab
unless args.dir.present? unless args.dir.present?
abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]") abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]")
...@@ -16,6 +18,7 @@ namespace :gitlab do ...@@ -16,6 +18,7 @@ namespace :gitlab do
command = status.zero? ? 'gmake' : 'make' command = status.zero? ? 'gmake' : 'make'
Dir.chdir(args.dir) do Dir.chdir(args.dir) do
create_gitaly_configuration
run_command!([command]) run_command!([command])
end end
end end
...@@ -33,5 +36,39 @@ namespace :gitlab do ...@@ -33,5 +36,39 @@ namespace :gitlab do
puts TOML.dump(storage: config) puts TOML.dump(storage: config)
end end
private
# We cannot create config.toml files for all possible Gitaly configuations.
# For instance, if Gitaly is running on another machine then it makes no
# sense to write a config.toml file on the current machine. This method will
# only write a config.toml file in the most common and simplest case: the
# case where we have exactly one Gitaly process and we are sure it is
# running locally because it uses a Unix socket.
def create_gitaly_configuration
storages = []
address = nil
Gitlab.config.repositories.storages.each do |key, val|
if address
if address != val['gitaly_address']
raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address."
end
elsif URI(val['gitaly_address']).scheme != 'unix'
raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses."
else
address = val['gitaly_address']
end
storages << { name: key, path: val['path'] }
end
File.open("config.toml", "w") do |f|
f.puts TOML.dump(socket_path: address.sub(%r{\Aunix:}, ''), storages: storages)
end
rescue ArgumentError => e
puts "Skipping config.toml generation:"
puts e.message
end
end end
end end
#!/bin/sh #!/bin/sh
retry() { . scripts/utils.sh
if eval "$@"; then
return 0 export SETUP_DB=${SETUP_DB:-true}
fi export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
# Determine the database by looking at the job name.
# For example, we'll get pg if the job is `rspec pg 19 20`
export GITLAB_DATABASE=$(echo $CI_JOB_NAME | cut -f2 -d' ')
# This would make the default database postgresql, and we could also use
# pg to mean postgresql.
if [ "$GITLAB_DATABASE" != 'mysql' ]; then
export GITLAB_DATABASE='postgresql'
fi
cp config/database.yml.$GITLAB_DATABASE config/database.yml
for i in 2 1; do if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
sleep 3s sed -i 's/# host:.*/host: postgres/g' config/database.yml
echo "Retrying $i..." else # Assume it's mysql
if eval "$@"; then sed -i 's/username:.*/username: root/g' config/database.yml
return 0 sed -i 's/password:.*/password:/g' config/database.yml
fi sed -i 's/# host:.*/host: mysql/g' config/database.yml
done fi
return 1
}
cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml
sed -i 's/password:.*/password:/g' config/database.yml
sed -i 's/# socket:.*/host: mysql/g' config/database.yml
cp config/resque.yml.example config/resque.yml cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml
export FLAGS="--path vendor --retry 3 --quiet" cp config/gitlab.yml.example config/gitlab.yml
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
retry bundle install --clean $BUNDLE_INSTALL_FLAGS
fi
# Only install knapsack after bundle install! Otherwise oddly some native
# gems could not be found under some circumstance. No idea why, hours wasted.
retry gem install knapsack fog-aws mime-types
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
if [ "$GITLAB_DATABASE" = "mysql" ]; then
bundle exec rake add_limits_mysql
fi
fi
retry() {
if eval "$@"; then
return 0
fi
for i in 2 1; do
sleep 3s
echo "Retrying $i..."
if eval "$@"; then
return 0
fi
done
return 1
}
require 'spec_helper' require 'spec_helper'
describe Dashboard::TodosController do describe Dashboard::TodosController do
include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
......
require 'spec_helper' require 'spec_helper'
describe Projects::BuildsController do describe Projects::BuildsController do
include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
...@@ -63,4 +61,44 @@ describe Projects::BuildsController do ...@@ -63,4 +61,44 @@ describe Projects::BuildsController do
expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico"
end end
end end
describe 'GET trace.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:user) { create(:user) }
context 'when user is logged in as developer' do
before do
project.add_developer(user)
sign_in(user)
get_trace
end
it 'traces build log' do
expect(response).to have_http_status(:ok)
expect(json_response['id']).to eq build.id
expect(json_response['status']).to eq build.status
end
end
context 'when user is logged in as non member' do
before do
sign_in(user)
get_trace
end
it 'traces build log' do
expect(response).to have_http_status(:ok)
expect(json_response['id']).to eq build.id
expect(json_response['status']).to eq build.status
end
end
def get_trace
get :trace, namespace_id: project.namespace,
project_id: project,
id: build.id,
format: :json
end
end
end end
require 'spec_helper'
describe Projects::BuildsController do
include ApiHelpers
let(:project) { create(:empty_project, :public) }
describe 'GET trace.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:user) { create(:user) }
context 'when user is logged in as developer' do
before do
project.add_developer(user)
sign_in(user)
get_trace
end
it 'traces build log' do
expect(response).to have_http_status(:ok)
expect(json_response['id']).to eq build.id
expect(json_response['status']).to eq build.status
end
end
context 'when user is logged in as non member' do
before do
sign_in(user)
get_trace
end
it 'traces build log' do
expect(response).to have_http_status(:ok)
expect(json_response['id']).to eq build.id
expect(json_response['status']).to eq build.status
end
end
def get_trace
get :trace, namespace_id: project.namespace,
project_id: project,
id: build.id,
format: :json
end
end
end
require 'spec_helper' require 'spec_helper'
describe Projects::EnvironmentsController do describe Projects::EnvironmentsController do
include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
......
require 'spec_helper' require 'spec_helper'
describe Projects::MergeRequestsController do describe Projects::MergeRequestsController do
include ApiHelpers
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
......
...@@ -5,6 +5,7 @@ describe Projects::MilestonesController do ...@@ -5,6 +5,7 @@ describe Projects::MilestonesController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) } let(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do before do
...@@ -13,6 +14,20 @@ describe Projects::MilestonesController do ...@@ -13,6 +14,20 @@ describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@project, project)
end end
describe "#show" do
render_views
def view_milestone
get :show, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
end
it 'shows milestone page' do
view_milestone
expect(response).to have_http_status(200)
end
end
describe "#destroy" do describe "#destroy" do
it "removes milestone" do it "removes milestone" do
expect(issue.milestone_id).to eq(milestone.id) expect(issue.milestone_id).to eq(milestone.id)
......
require 'spec_helper' require 'spec_helper'
describe Projects::PipelinesController do describe Projects::PipelinesController do
include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
......
require('spec_helper') require('spec_helper')
describe Projects::TodosController do describe Projects::TodosController do
include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
......
require 'spec_helper' require 'spec_helper'
RSpec.describe 'admin issues labels' do RSpec.describe 'admin issues labels' do
include WaitForAjax
let!(:bug_label) { Label.create(title: 'bug', template: true) } let!(:bug_label) { Label.create(title: 'bug', template: true) }
let!(:feature_label) { Label.create(title: 'feature', template: true) } let!(:feature_label) { Label.create(title: 'feature', template: true) }
......
require 'spec_helper' require 'spec_helper'
describe "Admin::Users", feature: true do describe "Admin::Users", feature: true do
include WaitForAjax
let!(:user) do let!(:user) do
create(:omniauth_user, provider: 'twitter', extern_uid: '123456') create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
end end
......
require 'spec_helper' require 'spec_helper'
describe 'Auto deploy' do describe 'Auto deploy' do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project, :repository) }
before do before do
project.create_kubernetes_service( project.create_kubernetes_service(
......
require 'rails_helper' require 'rails_helper'
describe 'Issue Boards add issue modal', :feature, :js do describe 'Issue Boards add issue modal', :feature, :js do
include WaitForAjax
include WaitForVueResource include WaitForVueResource
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
......
require 'rails_helper' require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource include WaitForVueResource
include DragTo include DragTo
......
require 'rails_helper' require 'rails_helper'
describe 'Issue Boards new issue', feature: true, js: true do describe 'Issue Boards new issue', feature: true, js: true do
include WaitForAjax
include WaitForVueResource include WaitForVueResource
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
......
require 'rails_helper' require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource include WaitForVueResource
let(:user) { create(:user) } let(:user) { create(:user) }
......
require 'spec_helper' require 'spec_helper'
feature 'Contributions Calendar', :feature, :js do feature 'Contributions Calendar', :feature, :js do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public) } let(:contributed_project) { create(:empty_project, :public) }
let(:issue_note) { create(:note, project: contributed_project) } let(:issue_note) { create(:note, project: contributed_project) }
# Ex/ Sunday Jan 1, 2016 # Ex/ Sunday Jan 1, 2016
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Commits' do describe 'Commits' do
include CiStatusHelper include CiStatusHelper
let(:project) { create(:project) } let(:project) { create(:project, :repository) }
describe 'CI' do describe 'CI' do
before do before do
......
...@@ -433,7 +433,7 @@ describe 'Copy as GFM', feature: true, js: true do ...@@ -433,7 +433,7 @@ describe 'Copy as GFM', feature: true, js: true do
end end
describe 'Copying code' do describe 'Copying code' do
let(:project) { create(:project) } let(:project) { create(:project, :repository) }
context 'from a diff' do context 'from a diff' do
before do before do
......
require 'spec_helper' require 'spec_helper'
feature 'Cycle Analytics', feature: true, js: true do feature 'Cycle Analytics', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:guest) { create(:user) } let(:guest) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(issue) } let(:mr) { create_merge_request_closing_issue(issue) }
......
require 'spec_helper' require 'spec_helper'
feature 'Tooltips on .timeago dates', feature: true, js: true do feature 'Tooltips on .timeago dates', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time } let(:created_date) { Date.yesterday.to_time }
......
require 'spec_helper' require 'spec_helper'
describe 'Dashboard Groups page', js: true, feature: true do describe 'Dashboard Groups page', js: true, feature: true do
include WaitForAjax
let!(:user) { create :user } let!(:user) { create :user }
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:nested_group) { create(:group, :nested) } let!(:nested_group) { create(:group, :nested) }
......
require 'spec_helper' require 'spec_helper'
feature 'Project member activity', feature: true, js: true do feature 'Project member activity', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, :public, name: 'x', namespace: user.namespace) } let(:project) { create(:empty_project, :public, name: 'x', namespace: user.namespace) }
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe "Dashboard Issues filtering", feature: true, js: true do describe "Dashboard Issues filtering", feature: true, js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
context 'filtering by milestone' do context 'filtering by milestone' do
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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