Commit 537f87a1 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-support-bitbucket-server-import

parents f94b5225 d22db4f4
......@@ -447,9 +447,8 @@ danger-review:
- retry gem install danger --no-ri --no-rdoc
cache: {}
only:
refs:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
variables:
- $DANGER_GITLAB_API_TOKEN
except:
refs:
- master
......
......@@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.1.1 (2018-07-23)
### Fixed (2 changes)
- Add missing Gitaly branch_update nil checks. !20711
- Fix filename for accelerated uploads.
### Added (1 change)
- Add uploader support to Import/Export uploads. !20484
## 11.1.0 (2018-07-22)
### Security (6 changes)
......
......@@ -133,7 +133,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Plan, ~Quality, ~Platform, etc.
- Team: ~"CI/CD", ~Plan, ~Manage, ~Quality, etc.
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
......@@ -192,9 +192,9 @@ The current team labels are:
- ~Documentation
- ~Geo
- ~Gitaly
- ~Manage
- ~Monitoring
- ~Plan
- ~Platform
- ~Quality
- ~Release
- ~"Security Products"
......@@ -376,8 +376,14 @@ on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone.
We also use [GitLab Triage] to automate some triaging policies. This is
currently setup as a [scheduled pipeline] running on the [`gl-triage`] branch.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
[GitLab Triage]: https://gitlab.com/gitlab-org/gitlab-triage
[scheduled pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipeline_schedules/3732/edit
[`gl-triage`]: https://gitlab.com/gitlab-org/gitlab-ce/tree/gl-triage
### Feature proposals
......
......@@ -4,3 +4,4 @@ danger.import_dangerfile(path: 'danger/changelog')
danger.import_dangerfile(path: 'danger/specs')
danger.import_dangerfile(path: 'danger/gemfile')
danger.import_dangerfile(path: 'danger/database')
danger.import_dangerfile(path: 'danger/frozen_string')
......@@ -220,6 +220,9 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration
gem 'slack-notifier', '~> 1.5.1'
# Hangouts Chat integration
gem 'hangouts-chat', '~> 0.0.5'
# Asana integration
gem 'asana', '~> 0.6.0'
......@@ -230,7 +233,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0'
# Sanitize user input
gem 'sanitize', '~> 4.6.5'
gem 'sanitize', '~> 4.6'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
......@@ -419,7 +422,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.106.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.109.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -284,7 +284,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.106.0)
gitaly-proto (0.109.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -387,6 +387,7 @@ GEM
temple (>= 0.8.0)
thor
tilt
hangouts-chat (0.0.5)
hashdiff (0.3.4)
hashie (3.5.7)
hashie-forbidden_attributes (0.1.1)
......@@ -396,7 +397,7 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (2.8.3)
html-pipeline (2.8.4)
activesupport (>= 2)
nokogiri (>= 1.4)
html2text (0.2.0)
......@@ -513,7 +514,7 @@ GEM
net-ldap (0.16.0)
net-ssh (5.0.1)
netrc (0.11.0)
nokogiri (1.8.3)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
......@@ -807,7 +808,7 @@ GEM
et-orbi (~> 1.0)
rugged (0.27.2)
safe_yaml (1.0.4)
sanitize (4.6.5)
sanitize (4.6.6)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
......@@ -1041,7 +1042,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.106.0)
gitaly-proto (~> 0.109.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......@@ -1062,6 +1063,7 @@ DEPENDENCIES
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
......@@ -1155,7 +1157,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 4.6.5)
sanitize (~> 4.6)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
......
......@@ -287,7 +287,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.106.0)
gitaly-proto (0.109.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -390,6 +390,7 @@ GEM
temple (>= 0.8.0)
thor
tilt
hangouts-chat (0.0.5)
hashdiff (0.3.4)
hashie (3.5.7)
hashie-forbidden_attributes (0.1.1)
......@@ -1051,7 +1052,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.106.0)
gitaly-proto (~> 0.109.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......@@ -1072,6 +1073,7 @@ DEPENDENCIES
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
......@@ -1166,7 +1168,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 4.6.5)
sanitize (~> 4.6)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
......
......@@ -71,6 +71,11 @@ export default {
required: false,
default: false,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState({
......@@ -78,7 +83,6 @@ export default {
diffFiles: state => state.diffs.diffFiles,
}),
...mapGetters(['isLoggedIn']),
...mapGetters('diffs', ['discussionsByLineCode']),
lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#';
},
......@@ -88,24 +92,19 @@ export default {
this.showCommentButton &&
!this.isMatchLine &&
!this.isContextLine &&
!this.hasDiscussions &&
!this.isMetaLine
!this.isMetaLine &&
!this.hasDiscussions
);
},
discussions() {
return this.discussionsByLineCode[this.lineCode] || [];
},
hasDiscussions() {
return this.discussions.length > 0;
},
shouldShowAvatarsOnGutter() {
let render = this.hasDiscussions && this.showCommentButton;
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
render = false;
return false;
}
return render;
return this.hasDiscussions && this.showCommentButton;
},
},
methods: {
......
......@@ -67,6 +67,11 @@ export default {
required: false,
default: false,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapGetters(['isLoggedIn']),
......@@ -136,6 +141,7 @@ export default {
:is-match-line="isMatchLine"
:is-context-line="isContentLine"
:is-meta-line="isMetaLine"
:discussions="discussions"
/>
</td>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
......@@ -21,15 +21,16 @@ export default {
type: Number,
required: true,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters('diffs', ['discussionsByLineCode']),
discussions() {
return this.discussionsByLineCode[this.line.lineCode] || [];
},
className() {
return this.discussions.length ? '' : 'js-temp-notes-holder';
},
......
......@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
......@@ -89,6 +94,7 @@ export default {
:is-bottom="isBottom"
:is-hover="isHover"
:show-comment-button="true"
:discussions="discussions"
class="diff-line-num old_line"
/>
<diff-table-cell
......@@ -98,6 +104,7 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
:discussions="discussions"
class="diff-line-num new_line"
/>
<td
......
......@@ -20,7 +20,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['commitId', 'discussionsByLineCode']),
...mapGetters('diffs', [
'commitId',
'shouldRenderInlineCommentRow',
'singleDiscussionByLineCode',
]),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
......@@ -34,18 +38,7 @@ export default {
return window.gon.user_color_scheme;
},
},
methods: {
shouldRenderCommentRow(line) {
if (this.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = this.discussionsByLineCode[line.lineCode];
if (lineDiscussions === undefined) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
},
},
methods: {},
};
</script>
......@@ -64,13 +57,15 @@ export default {
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode"
:discussions="singleDiscussionByLineCode(line.lineCode)"
/>
<inline-diff-comment-row
v-if="shouldRenderCommentRow(line)"
v-if="shouldRenderInlineCommentRow(line)"
:diff-file-hash="diffFile.fileHash"
:line="line"
:line-index="index"
:key="index"
:discussions="singleDiscussionByLineCode(line.lineCode)"
/>
</template>
</tbody>
......
<script>
import { mapState, mapGetters } from 'vuex';
import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
......@@ -21,48 +21,51 @@ export default {
type: Number,
required: true,
},
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters('diffs', ['discussionsByLineCode']),
leftLineCode() {
return this.line.left.lineCode;
},
rightLineCode() {
return this.line.right.lineCode;
},
hasDiscussion() {
const discussions = this.discussionsByLineCode;
return discussions[this.leftLineCode] || discussions[this.rightLineCode];
},
hasExpandedDiscussionOnLeft() {
const discussions = this.discussionsByLineCode[this.leftLineCode];
const discussions = this.leftDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
hasExpandedDiscussionOnRight() {
const discussions = this.discussionsByLineCode[this.rightLineCode];
const discussions = this.rightDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
},
shouldRenderDiscussionsOnRight() {
return (
this.discussionsByLineCode[this.rightLineCode] &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type;
},
showRightSideCommentForm() {
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
},
className() {
return this.hasDiscussion ? '' : 'js-temp-notes-holder';
return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0
? ''
: 'js-temp-notes-holder';
},
},
};
......@@ -80,13 +83,12 @@ export default {
class="content"
>
<diff-discussions
v-if="discussionsByLineCode[leftLineCode].length"
:discussions="discussionsByLineCode[leftLineCode]"
v-if="leftDiscussions.length"
:discussions="leftDiscussions"
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[leftLineCode] &&
diffLineCommentForms[leftLineCode]"
v-if="diffLineCommentForms[leftLineCode]"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
......@@ -100,13 +102,12 @@ export default {
class="content"
>
<diff-discussions
v-if="discussionsByLineCode[rightLineCode].length"
:discussions="discussionsByLineCode[rightLineCode]"
v-if="rightDiscussions.length"
:discussions="rightDiscussions"
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[rightLineCode] &&
diffLineCommentForms[rightLineCode] && line.right.type"
v-if="showRightSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
......
......@@ -36,6 +36,16 @@ export default {
required: false,
default: false,
},
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
......@@ -116,6 +126,7 @@ export default {
:is-hover="isLeftHover"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
:discussions="leftDiscussions"
class="diff-line-num old_line"
/>
<td
......@@ -136,6 +147,7 @@ export default {
:is-hover="isRightHover"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
:discussions="rightDiscussions"
class="diff-line-num new_line"
/>
<td
......
......@@ -21,7 +21,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['commitId', 'discussionsByLineCode']),
...mapGetters('diffs', [
'commitId',
'singleDiscussionByLineCode',
'shouldRenderParallelCommentRow',
]),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
......@@ -51,32 +55,6 @@ export default {
return window.gon.user_color_scheme;
},
},
methods: {
shouldRenderCommentRow(line) {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const discussions = this.discussionsByLineCode;
const leftDiscussions = discussions[leftLineCode];
const rightDiscussions = discussions[rightLineCode];
const hasDiscussion = leftDiscussions || rightDiscussions;
const hasExpandedDiscussionOnLeft = leftDiscussions
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
},
};
</script>
......@@ -97,13 +75,17 @@ export default {
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:key="index"
:left-discussions="singleDiscussionByLineCode(line.left.lineCode)"
:right-discussions="singleDiscussionByLineCode(line.right.lineCode)"
/>
<parallel-diff-comment-row
v-if="shouldRenderCommentRow(line)"
v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`"
:line="line"
:diff-file-hash="diffFile.fileHash"
:line-index="index"
:left-discussions="singleDiscussionByLineCode(line.left.lineCode)"
:right-discussions="singleDiscussionByLineCode(line.right.lineCode)"
/>
</template>
</tbody>
......
......@@ -75,19 +75,21 @@ export const discussionsByLineCode = (state, getters, rootState, rootGetters) =>
const isDiffDiscussion = note.diff_discussion;
const hasLineCode = note.line_code;
const isResolvable = note.resolvable;
const diffRefs = diffRefsByLineCode[note.line_code];
if (isDiffDiscussion && hasLineCode && isResolvable && diffRefs) {
const refs = convertObjectPropsToCamelCase(note.position.formatter);
const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter);
if (isDiffDiscussion && hasLineCode && isResolvable) {
const diffRefs = diffRefsByLineCode[note.line_code];
if (diffRefs) {
const refs = convertObjectPropsToCamelCase(note.position.formatter);
const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter);
if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) {
const lineCode = note.line_code;
if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) {
const lineCode = note.line_code;
if (acc[lineCode]) {
acc[lineCode].push(note);
} else {
acc[lineCode] = [note];
if (acc[lineCode]) {
acc[lineCode].push(note);
} else {
acc[lineCode] = [note];
}
}
}
}
......@@ -96,6 +98,47 @@ export const discussionsByLineCode = (state, getters, rootState, rootGetters) =>
}, {});
};
export const singleDiscussionByLineCode = (state, getters) => lineCode => {
if (!lineCode) return [];
const discussions = getters.discussionsByLineCode;
return discussions[lineCode] || [];
};
export const shouldRenderParallelCommentRow = (state, getters) => line => {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
const hasExpandedDiscussionOnLeft = leftDiscussions.length
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions.length
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
export const shouldRenderInlineCommentRow = (state, getters) => line => {
if (state.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
if (lineDiscussions.length === 0) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
};
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);
......
import $ from 'jquery';
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
const tag = $('.js-signature-container');
if (tag.length === 0) {
return Promise.resolve();
}
const badges = $('.js-loading-gpg-badge');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
const displayError = () => createFlash(__('An error occurred while loading commit signatures'));
const endpoint = tag.data('signaturesPath');
if (!endpoint) {
displayError();
return Promise.reject(new Error('Missing commit signatures endpoint!'));
}
const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
})
.catch(() => flash(__('An error occurred while loading commits')));
return axios
.get(endpoint, { params })
.then(({ data }) => {
data.signatures.forEach(signature => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
})
.catch(displayError);
}
}
......@@ -46,7 +46,7 @@
by
<a
:href="updatedByPath"
class="author_link"
class="author-link"
>
<span>{{ updatedByName }}</span>
</a>
......
......@@ -541,6 +541,26 @@ export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
});
};
/**
* Method to round of values with decimal places
* with provided precision.
*
* Taken from https://stackoverflow.com/a/7343013/414749
*
* Eg; roundOffFloat(3.141592, 3) = 3.142
*
* Refer to spec/javascripts/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
* @param {Number} precision
*/
export const roundOffFloat = (number, precision = 0) => {
// eslint-disable-next-line no-restricted-properties
const multiplier = Math.pow(10, precision);
return Math.round(number * multiplier) / multiplier;
};
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -38,7 +38,7 @@ import { normalizeHeaders } from './common_utils';
* } else {
* poll.stop();
* }
* });
* });
*
* 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header.
......@@ -51,8 +51,8 @@ export default class Poll {
constructor(options = {}) {
this.options = options;
this.options.data = options.data || {};
this.options.notificationCallback = options.notificationCallback ||
function notificationCallback() {};
this.options.notificationCallback =
options.notificationCallback || function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null;
......@@ -63,6 +63,7 @@ export default class Poll {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
clearTimeout(this.timeoutID);
this.timeoutID = setTimeout(() => {
this.makeRequest();
}, pollInterval);
......@@ -77,11 +78,11 @@ export default class Poll {
notificationCallback(true);
return resource[method](data)
.then((response) => {
.then(response => {
this.checkConditions(response);
notificationCallback(false);
})
.catch((error) => {
.catch(error => {
notificationCallback(false);
if (error.status === httpStatusCodes.ABORTED) {
return;
......
......@@ -42,7 +42,7 @@ export default {
by
<a
:href="editedBy.path"
class="js-vue-author author_link">
class="js-vue-author author-link">
{{ editedBy.name }}
</a>
</template>
......
......@@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
const { markdownVersion } = notesDataset;
const markdownVersion = parseInt(notesDataset.markdownVersion, 10);
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
......
......@@ -174,27 +174,19 @@ export default {
[types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) {
noteObj.notes.splice(0, 1, note);
} else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
Object.assign(comment, note);
}
},
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
let index = 0;
state.discussions.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
const selectedDiscussion = state.discussions.find(n => n.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
state.discussions.splice(index, 1, note);
Object.assign(selectedDiscussion, note);
},
[types.CLOSE_ISSUE](state) {
......@@ -215,12 +207,9 @@ export default {
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
const index = state.discussions.indexOf(discussion);
const discussionWithDiffLines = Object.assign({}, discussion, {
Object.assign(discussion, {
truncated_diff_lines: diffLines,
});
state.discussions.splice(index, 1, discussionWithDiffLines);
},
};
......@@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) =>
notes.filter(n => n.id === id)[0];
export const findNoteObjectById = (notes, id) => notes.find(n => n.id === id);
export const getQuickActionText = note => {
let text = 'Applying command';
const quickActions =
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`);
......@@ -29,5 +27,4 @@ export const getQuickActionText = note => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note =>
note.replace(REGEX_QUICK_ACTIONS, '').trim();
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
......@@ -26,4 +27,6 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
}
GpgBadges.fetch();
});
......@@ -7,6 +7,7 @@ import TreeView from '~/tree';
import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
......@@ -38,4 +39,6 @@ document.addEventListener('DOMContentLoaded', () => {
$(treeSlider).waitForImages(() => {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
GpgBadges.fetch();
});
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue from 'vue';
import initBlob from '~/blob_edit/blob_bundle';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import GpgBadges from '~/gpg_badges';
import TreeView from '../../../../tree';
import ShortcutsNavigation from '../../../../shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer';
......@@ -14,7 +15,8 @@ document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
$('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath));
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath),
);
initBlob();
const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
......@@ -36,4 +38,6 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
}
GpgBadges.fetch();
});
<script>
import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service';
import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
ciIcon,
loadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
components: {
ciIcon,
loadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
/* This prop can be used to replace some of the `render_commit_status`
/* This prop can be used to replace some of the `render_commit_status`
used across GitLab, this way we could use this vue component and add a
realtime status where it makes sense
realtime: {
......@@ -29,76 +29,77 @@
required: false,
default: true,
}, */
},
data() {
return {
ciStatus: {},
isLoading: true,
};
},
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
},
data() {
return {
ciStatus: {},
isLoading: true,
};
},
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
},
},
mounted() {
this.service = new CommitPipelineService(this.endpoint);
this.initPolling();
},
methods: {
successCallback(res) {
const { pipelines } = res.data;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
}
this.isLoading = false;
},
mounted() {
this.service = new CommitPipelineService(this.endpoint);
this.initPolling();
errorCallback() {
this.ciStatus = {
text: 'not found',
icon: 'status_notfound',
group: 'notfound',
};
this.isLoading = false;
Flash(s__('Something went wrong on our end'));
},
methods: {
successCallback(res) {
const { pipelines } = res.data;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
}
this.isLoading = false;
},
errorCallback() {
this.ciStatus = {
text: 'not found',
icon: 'status_notfound',
group: 'notfound',
};
this.isLoading = false;
Flash(s__('Something went wrong on our end'));
},
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: response => this.successCallback(response),
errorCallback: this.errorCallback,
});
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: response => this.successCallback(response),
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchPipelineCommitData();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
this.poll.restart();
} else {
this.fetchPipelineCommitData();
this.poll.stop();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
fetchPipelineCommitData() {
this.service.fetchData()
.then(this.successCallback)
.catch(this.errorCallback);
},
});
},
destroy() {
this.poll.stop();
fetchPipelineCommitData() {
this.service
.fetchData()
.then(this.successCallback)
.catch(this.errorCallback);
},
};
},
destroy() {
this.poll.stop();
},
};
</script>
<template>
<div>
<div class="ci-status-link">
<loading-icon
v-if="isLoading"
label="Loading pipeline status"
......@@ -113,6 +114,7 @@
:title="statusTitle"
:aria-label="statusTitle"
:status="ciStatus"
:size="24"
data-container="body"
/>
</a>
......
import Visibility from 'visibilityjs';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
/**
* We need to poll the reports endpoint while they are being parsed in the Backend.
* This can take up to one minute.
*
* Poll.js will handle etag response.
* While http status code is 204, it means it's parsing, and we'll keep polling
* When http status code is 200, it means parsing is done, we can show the results & stop polling
* When http status code is 500, it means parsing went wrong and we stop polling
*/
export const fetchReports = ({ state, dispatch }) => {
dispatch('requestReports');
eTagPoll = new Poll({
resource: {
getReports(endpoint) {
return axios.get(endpoint);
},
},
data: state.endpoint,
method: 'getReports',
successCallback: ({ data }) => dispatch('receiveReportsSuccess', data),
errorCallback: () => dispatch('receiveReportsError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartPolling');
} else {
dispatch('stopPolling');
}
});
};
export const receiveReportsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_REPORTS_SUCCESS, response);
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
state: state(),
});
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
[types.REQUEST_REPORTS](state) {
state.isLoading = true;
},
[types.RECEIVE_REPORTS_SUCCESS](state, response) {
state.isLoading = false;
state.summary.total = response.summary.total;
state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed;
state.reports = response.suites;
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
};
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
summary: {
total: 0,
resolved: 0,
failed: 0,
},
/**
* Each report will have the following format:
* {
* name: {String},
* summary: {
* total: {Number},
* resolved: {Number},
* failed: {Number},
* },
* new_failures: {Array.<Object>},
* resolved_failures: {Array.<Object>},
* existing_failures: {Array.<Object>},
* }
*/
reports: [],
});
......@@ -187,7 +187,7 @@ export default {
<template v-else-if="hasOneUser">
<a
:href="assigneeUrl(firstUser)"
class="author_link bold"
class="author-link bold"
>
<img
:alt="assigneeAlt(firstUser)"
......
......@@ -120,7 +120,7 @@
>
<a
:href="participant.web_url"
class="author_link"
class="author-link"
>
<user-avatar-image
:lazy="true"
......
......@@ -206,8 +206,8 @@ function UsersSelect(currentUser, els, options = {}) {
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
......
......@@ -13,12 +13,19 @@
* />
*/
import tooltip from '../directives/tooltip';
import Icon from '../components/icon.vue';
export default {
name: 'ClipboardButton',
directives: {
tooltip,
},
components: {
Icon,
},
props: {
text: {
type: String,
......@@ -58,10 +65,6 @@ export default {
type="button"
class="btn"
>
<i
aria-hidden="true"
class="fa fa-clipboard"
>
</i>
<icon name="duplicate" />
</button>
</template>
<script>
import { roundOffFloat } from '~/lib/utils/common_utils';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
......@@ -70,7 +71,7 @@ export default {
},
methods: {
getPercent(count) {
return Math.ceil((count / this.totalCount) * 100);
return roundOffFloat((count / this.totalCount) * 100, 1);
},
barStyle(percent) {
return `width: ${percent}%;`;
......
......@@ -7,7 +7,7 @@
.avatar-circle {
float: left;
margin-right: 15px;
border-radius: $avatar_radius;
border-radius: $avatar-radius;
border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); }
&.s18 { @include avatar-size(18px, 6px); }
......@@ -110,7 +110,7 @@
color: $white-light;
border: 1px solid $avatar-border;
border-radius: 1em;
font-family: $regular_font;
font-family: $regular-font;
font-size: 9px;
line-height: 16px;
text-align: center;
......
......@@ -294,6 +294,10 @@
.btn-clipboard {
border: 0;
padding: 0 5px;
svg {
top: auto;
}
}
.input-group-prepend,
......
......@@ -113,8 +113,6 @@ hr {
.item-title { font-weight: $gl-font-weight-bold; }
/** FLASH message **/
.author_link,
.author-link {
color: $gl-link-color;
}
......
......@@ -80,7 +80,7 @@ label {
.form-control {
height: 29px;
background: $white-light;
font-family: $monospace_font;
font-family: $monospace-font;
}
.input-group-prepend .btn,
......
......@@ -9,8 +9,8 @@
padding: 10px 0;
border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
font-family: $monospace-font;
font-size: $code-font-size;
line-height: 19px;
margin: 0;
overflow: auto;
......@@ -22,7 +22,7 @@
code {
display: inline-block;
min-width: 100%;
font-family: $monospace_font;
font-family: $monospace-font;
white-space: normal;
word-wrap: normal;
padding: 0;
......@@ -44,7 +44,7 @@
float: left;
a {
font-family: $monospace_font;
font-family: $monospace-font;
display: block;
font-size: $code_font_size !important;
min-height: 19px;
......
.ui-widget {
font-family: $regular_font;
font-family: $regular-font;
font-size: $font-size-base;
.ui-state-default {
......
......@@ -259,7 +259,7 @@ ul.controls {
margin-right: 0;
}
.author_link {
.author-link {
.avatar-inline {
margin-left: 0;
margin-right: 0;
......@@ -270,7 +270,7 @@ ul.controls {
.issuable-pipeline-broken a,
.issuable-pipeline-status a,
.author_link {
.author-link {
display: flex;
}
}
......
......@@ -3,13 +3,13 @@
* Mixins with fixed values
*/
@mixin str-truncated($max_width: 82%) {
@mixin str-truncated($max-width: 82%) {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
max-width: $max_width;
max-width: $max-width;
}
/*
......
......@@ -33,11 +33,11 @@
@include media-breakpoint-up(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
padding-right: $gutter-collapsed-width;
}
.merge-request-tabs-holder.affix {
right: $gutter_collapsed_width;
right: $gutter-collapsed-width;
}
}
......@@ -67,21 +67,21 @@
@include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
padding-right: $gutter-collapsed-width;
}
}
@include media-breakpoint-up(md) {
.content-wrapper {
padding-right: $gutter_width;
padding-right: $gutter-width;
}
&:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
right: $gutter-width;
}
&.with-overlay .merge-request-tabs-holder.affix {
right: $gutter_collapsed_width;
right: $gutter-collapsed-width;
}
}
}
......
......@@ -10,7 +10,7 @@
.status-neutral,
.status-red, {
height: 100%;
min-width: 30px;
min-width: 40px;
padding: 0 5px;
font-size: $tooltip-font-size;
font-weight: normal;
......
......@@ -44,7 +44,7 @@
// Single code lines should wrap
code {
font-family: $monospace_font;
font-family: $monospace-font;
white-space: pre-wrap;
word-wrap: normal;
}
......@@ -321,7 +321,7 @@ h6 {
/** CODE **/
pre {
font-family: $monospace_font;
font-family: $monospace-font;
display: block;
padding: $gl-padding-8;
margin: 0 0 $gl-padding-8;
......@@ -342,7 +342,7 @@ code {
}
.monospace {
font-family: $monospace_font;
font-family: $monospace-font;
}
.weight-normal {
......@@ -381,7 +381,7 @@ code {
*
*/
textarea.js-gfm-input {
font-family: $monospace_font;
font-family: $monospace-font;
font-size: 13px;
}
......
......@@ -2,9 +2,9 @@
* Layout
*/
$grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
$gutter-collapsed-width: 62px;
$gutter-width: 290px;
$gutter-inner-width: 250px;
$sidebar-transition-duration: 0.3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
......@@ -233,8 +233,8 @@ $md-area-border: #ddd;
/*
* Code
*/
$code_font_size: 90%;
$code_line_height: 1.6;
$code-font-size: 90%;
$code-line-height: 1.6;
/*
* Tooltips
......@@ -371,9 +371,9 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/*
......@@ -526,7 +526,7 @@ $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards
/*
* Avatar
*/
$avatar_radius: 50%;
$avatar-radius: 50%;
$avatar-border: $gray-normal;
$avatar-border-hover: $gray-darker;
$avatar-background: $gray-lightest;
......@@ -830,8 +830,8 @@ $secondary: $gray-light;
$input-disabled-bg: $gray-light;
$input-border-color: $theme-gray-200;
$input-color: $gl-text-color;
$font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font;
$font-family-sans-serif: $regular-font;
$font-family-monospace: $monospace-font;
$input-line-height: 20px;
$btn-line-height: 20px;
$table-accent-bg: $gray-light;
......@@ -77,13 +77,13 @@ $highlighted-gc-bg: #eaf2f5;
.code {
background-color: $white-light;
font-family: monospace;
font-size: $code_font_size;
font-size: $code-font-size;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
> tr {
line-height: $code_line_height;
line-height: $code-line-height;
}
}
......
......@@ -63,7 +63,7 @@
width: 100%;
&.is-compact {
width: calc(100% - #{$gutter_width});
width: calc(100% - #{$gutter-width});
}
}
}
......
......@@ -79,7 +79,7 @@
.commit-message-container {
background-color: $body-bg;
position: relative;
font-family: $monospace_font;
font-family: $monospace-font;
$left: 12px;
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker {
......@@ -205,7 +205,7 @@
> .ci-status-link,
> .btn,
> .commit-sha-group {
margin-left: $gl-padding-8;
margin-left: $gl-padding;
}
}
......@@ -235,10 +235,6 @@
fill: $gl-text-color-secondary;
}
.fa-clipboard {
color: $gl-text-color-secondary;
}
:first-child {
border-bottom-left-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
......
......@@ -368,7 +368,7 @@
.fa {
color: $gl-text-color-secondary;
font-size: $code_font_size;
font-size: $code-font-size;
}
}
}
......
......@@ -10,7 +10,7 @@
}
.issue_created_ago,
.author_link {
.author-link {
white-space: nowrap;
}
......
......@@ -56,7 +56,7 @@
table {
width: 100%;
font-family: $monospace_font;
font-family: $monospace-font;
border: 0;
border-collapse: separate;
margin: 0;
......@@ -73,8 +73,8 @@
}
.line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
line-height: $code-line-height;
font-size: $code-font-size;
&.noteable_line {
position: relative;
......
......@@ -84,7 +84,7 @@
.soft-wrap-toggle {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
font-family: $regular-font;
}
.soft-wrap-toggle {
......
......@@ -478,7 +478,7 @@
}
.deploy-info-text-link {
font-family: $monospace_font;
font-family: $monospace-font;
fill: $gl-link-color;
&:hover {
......
......@@ -166,7 +166,7 @@
border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
width: $gutter-inner-width;
// --
&.issuable-sidebar-header {
......@@ -197,7 +197,7 @@
}
&.assignee {
.author_link {
.author-link {
display: block;
padding-left: 42px;
position: relative;
......@@ -290,7 +290,7 @@
}
&.right-sidebar-expanded {
width: $gutter_width;
width: $gutter-width;
.value {
line-height: 1;
......@@ -377,11 +377,11 @@
display: block;
}
width: $gutter_collapsed_width;
width: $gutter-collapsed-width;
padding: 0;
.block {
width: $gutter_collapsed_width - 2px;
width: $gutter-collapsed-width - 2px;
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
......@@ -486,7 +486,7 @@
padding-bottom: 0;
margin-bottom: 10px;
.author_link {
.author-link {
padding-left: 0;
.avatar {
......@@ -595,7 +595,7 @@
margin: 16px 0 0;
font-size: 85%;
.author_link {
.author-link {
color: $gray-darkest;
}
}
......@@ -620,7 +620,7 @@
padding-right: 0;
}
.author_link {
.author-link {
display: block;
}
......
......@@ -12,7 +12,7 @@
}
.issuable-meta {
.author_link {
.author-link {
display: inline-block;
}
......
......@@ -208,7 +208,7 @@
position: absolute;
content: '...';
right: 0;
font-family: $regular_font;
font-family: $regular-font;
background-color: $gray-light;
}
}
......
......@@ -42,7 +42,7 @@
display: block;
padding: 10px 0;
color: $gl-text-color;
font-family: $regular_font;
font-family: $regular-font;
border: 0;
&:focus {
......
......@@ -328,7 +328,7 @@ ul.notes {
}
.notes_holder {
font-family: $regular_font;
font-family: $regular-font;
td {
border: 1px solid $white-normal;
......@@ -403,7 +403,7 @@ ul.notes {
}
}
.author_link {
.author-link {
color: $gl-text-color;
}
}
......
......@@ -961,7 +961,7 @@
overflow: hidden;
.note-textarea {
font-family: $monospace_font;
font-family: $monospace-font;
}
}
......
......@@ -6,9 +6,9 @@
$border-style: 1px solid $border-color;
font-family: $regular_font;
font-family: $regular-font;
font-size: $gl-font-size;
line-height: $code_line_height;
line-height: $code-line-height;
color: $gl-text-color;
margin: 20px;
font-weight: 200;
......@@ -48,9 +48,9 @@
padding: 10px;
border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
line-height: $code_line_height;
font-family: $monospace-font;
font-size: $code-font-size;
line-height: $code-line-height;
margin: 0;
overflow: auto;
overflow-y: hidden;
......@@ -66,10 +66,10 @@
float: left;
.diff-line-num {
font-family: $monospace_font;
font-family: $monospace-font;
display: block;
font-size: $code_font_size;
min-height: $code_line_height;
font-size: $code-font-size;
min-height: $code-line-height;
white-space: nowrap;
color: $black-transparent;
min-width: 30px;
......
......@@ -71,7 +71,22 @@ module LfsRequest
def lfs_download_access?
return false unless project.lfs_enabled?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code?
end
def deploy_token_can_download_code?
deploy_token_present? &&
deploy_token.project == project &&
deploy_token.active? &&
deploy_token.read_repository?
end
def deploy_token_present?
user && user.is_a?(DeployToken)
end
def deploy_token
user
end
def lfs_upload_access?
......@@ -86,7 +101,7 @@ module LfsRequest
end
def user_can_download_code?
has_authentication_ability?(:download_code) && can?(user, :download_code, project)
has_authentication_ability?(:download_code) && can?(user, :download_code, project) && !deploy_token_present?
end
def build_can_download_code?
......
class Import::GitlabController < Import::BaseController
MAX_PROJECT_PAGES = 15
PER_PAGE_PROJECTS = 100
before_action :verify_gitlab_import_enabled
before_action :gitlab_auth, except: :callback
......@@ -10,7 +13,7 @@ class Import::GitlabController < Import::BaseController
end
def status
@repos = client.projects
@repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
@already_added_projects = find_already_added_projects('gitlab')
already_added_projects_names = @already_added_projects.pluck(:import_source)
......
......@@ -99,7 +99,8 @@ class ProfilesController < Profiles::ApplicationController
:username,
:website_url,
:organization,
:preferred_language
:preferred_language,
:private_profile
)
end
end
......@@ -112,7 +112,7 @@ class Projects::WikisController < Projects::ApplicationController
private
def load_project_wiki
@project_wiki = ProjectWiki.new(@project, current_user)
@project_wiki = load_wiki
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
......@@ -128,6 +128,10 @@ class Projects::WikisController < Projects::ApplicationController
false
end
def load_wiki
ProjectWiki.new(@project, current_user)
end
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
......
......@@ -157,6 +157,8 @@ class SessionsController < Devise::SessionsController
end
def auto_sign_in_with_provider
return unless Gitlab::Auth.omniauth_enabled?
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present?
......
......@@ -13,6 +13,8 @@ class UsersController < ApplicationController
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
def show
respond_to do |format|
......@@ -148,4 +150,8 @@ class UsersController < ApplicationController
def build_canonical_path(user)
url_for(safe_params.merge(username: user.to_param))
end
def authorize_read_user_profile!
access_denied! unless can?(current_user, :read_user_profile, user)
end
end
......@@ -8,6 +8,7 @@
# owned: boolean
# parent: Group
# all_available: boolean (defaults to true)
# min_access_level: integer
#
# Users with full private access can see all groups. The `owned` and `parent`
# params can be used to restrict the groups that are returned.
......@@ -39,6 +40,7 @@ class GroupsFinder < UnionFinder
def all_groups
return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level?
return [Group.all] if current_user&.full_private_access? && all_available?
groups = []
......@@ -56,6 +58,16 @@ class GroupsFinder < UnionFinder
current_user.groups
end
def groups_with_min_access_level
groups = current_user
.groups
.where('members.access_level >= ?', params[:min_access_level])
Gitlab::GroupHierarchy
.new(groups)
.base_and_descendants
end
def by_parent(groups)
return groups unless params[:parent]
......@@ -73,4 +85,8 @@ class GroupsFinder < UnionFinder
def all_available?
params.fetch(:all_available, true)
end
def min_access_level?
current_user && params[:min_access_level].present?
end
end
class PersonalProjectsFinder < UnionFinder
def initialize(user)
include Gitlab::Allowable
def initialize(user, params = {})
@user = user
@params = params
end
# Finds the projects belonging to the user in "@user", limited to either
......@@ -8,9 +11,13 @@ class PersonalProjectsFinder < UnionFinder
#
# current_user - When given the list of projects is limited to those only
# visible by this user.
# params - Optional query parameters
# min_access_level: integer
#
# Returns an ActiveRecord::Relation.
def execute(current_user = nil)
return Project.none unless can?(current_user, :read_user_profile, @user)
segments = all_projects(current_user)
find_union(segments, Project).includes(:namespace).order_updated_desc
......@@ -19,11 +26,21 @@ class PersonalProjectsFinder < UnionFinder
private
def all_projects(current_user)
projects = []
return [projects_with_min_access_level(current_user)] if current_user && min_access_level?
projects = []
projects << @user.personal_projects.visible_to_user(current_user) if current_user
projects << @user.personal_projects.public_to_user(current_user)
projects
end
def projects_with_min_access_level(current_user)
@user
.personal_projects
.visible_to_user_and_access_level(current_user, @params[:min_access_level])
end
def min_access_level?
@params[:min_access_level].present?
end
end
......@@ -17,6 +17,7 @@
# search: string
# non_archived: boolean
# archived: 'only' or boolean
# min_access_level: integer
#
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
......@@ -34,7 +35,7 @@ class ProjectsFinder < UnionFinder
user = params.delete(:user)
collection =
if user
PersonalProjectsFinder.new(user).execute(current_user)
PersonalProjectsFinder.new(user, finder_params).execute(current_user)
else
init_collection
end
......@@ -65,6 +66,8 @@ class ProjectsFinder < UnionFinder
def collection_with_user
if owned_projects?
current_user.owned_projects
elsif min_access_level?
current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level])
else
if private_only?
current_user.authorized_projects
......@@ -76,7 +79,7 @@ class ProjectsFinder < UnionFinder
# Builds a collection for an anonymous user.
def collection_without_user
if private_only? || owned_projects?
if private_only? || owned_projects? || min_access_level?
Project.none
else
Project.public_to_user
......@@ -91,6 +94,10 @@ class ProjectsFinder < UnionFinder
params[:non_public].present?
end
def min_access_level?
params[:min_access_level].present?
end
def by_ids(items)
project_ids_relation ? items.where(id: project_ids_relation) : items
end
......@@ -143,4 +150,10 @@ class ProjectsFinder < UnionFinder
projects
end
end
def finder_params
return {} unless min_access_level?
{ min_access_level: params[:min_access_level] }
end
end
......@@ -7,6 +7,7 @@
class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Allowable
requires_cross_project_access
......@@ -21,6 +22,8 @@ class UserRecentEventsFinder
end
def execute
return Event.none unless can?(current_user, :read_user_profile, target_user)
recent_events(params[:offset] || 0)
.joins(:project)
.with_associations
......
......@@ -7,7 +7,7 @@ module AuthHelper
end
def omniauth_enabled?
Gitlab.config.omniauth.enabled
Gitlab::Auth.omniauth_enabled?
end
def provider_has_icon?(name)
......
......@@ -51,7 +51,7 @@ module ButtonHelper
}
content_tag :button, button_attributes do
concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon
concat(sprite_icon('duplicate')) unless hide_button_icon
concat(button_text)
end
end
......
......@@ -56,7 +56,7 @@ module CiStatusHelper
status.humanize
end
def ci_icon_for_status(status)
def ci_icon_for_status(status, size: 16)
if detailed_status?(status)
return sprite_icon(status.icon)
end
......@@ -85,7 +85,7 @@ module CiStatusHelper
'status_canceled'
end
sprite_icon(icon_name, size: 16)
sprite_icon(icon_name, size: size)
end
def pipeline_status_cache_key(pipeline_status)
......@@ -111,7 +111,8 @@ module CiStatusHelper
'commit',
commit.status(ref),
path,
tooltip_placement: tooltip_placement)
tooltip_placement: tooltip_placement,
icon_size: 24)
end
def render_pipeline_status(pipeline, tooltip_placement: 'left')
......@@ -125,16 +126,16 @@ module CiStatusHelper
Ci::Runner.instance_type.blank?
end
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
if path
link_to ci_icon_for_status(status), path,
link_to ci_icon_for_status(status, size: icon_size), path,
class: klass, title: title, data: data
else
content_tag :span, ci_icon_for_status(status),
content_tag :span, ci_icon_for_status(status, size: icon_size),
class: klass, title: title, data: data
end
end
......
......@@ -145,15 +145,14 @@ module CommitsHelper
person_name
end
options = {
class: "commit-#{options[:source]}-link has-tooltip",
title: source_email
link_options = {
class: "commit-#{options[:source]}-link"
}
if user.nil?
mail_to(source_email, text, options)
mail_to(source_email, text, link_options)
else
link_to(text, user_path(user), options)
link_to(text, user_path(user), link_options)
end
end
......
......@@ -63,10 +63,10 @@ module ProjectsHelper
author_html = author_html.html_safe
if opts[:name]
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
link_to(author_html, user_path(author), class: "author-link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe
link_to(author_html, user_path(author), class: "author-link has-tooltip", title: title, data: { container: 'body' }).html_safe
end
end
......
......@@ -42,7 +42,13 @@ module UsersHelper
private
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
tabs = []
if can?(current_user, :read_user_profile, @user)
tabs += [:activity, :groups, :contributed, :projects, :snippets]
end
tabs
end
def get_current_user_menu_items
......
......@@ -126,10 +126,9 @@ module VisibilityLevelHelper
end
def visibility_icon_description(form_model)
case form_model
when Project
if form_model.respond_to?(:visibility_level_allowed_as_fork?)
project_visibility_icon_description(form_model.visibility_level)
when Group
elsif form_model.respond_to?(:visibility_level_allowed_by_sub_groups?)
group_visibility_icon_description(form_model.visibility_level)
end
end
......
......@@ -27,7 +27,7 @@ class DeployToken < ActiveRecord::Base
end
def active?
!revoked
!revoked && expires_at > Date.today
end
def scopes
......@@ -58,6 +58,10 @@ class DeployToken < ActiveRecord::Base
write_attribute(:expires_at, value.presence || Forever.date)
end
def admin?
false
end
private
def ensure_at_least_one_scope
......
......@@ -154,6 +154,7 @@ class Project < ActiveRecord::Base
has_one :mock_monitoring_service
has_one :microsoft_teams_service
has_one :packagist_service
has_one :hangouts_chat_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
......@@ -326,6 +327,7 @@ class Project < ActiveRecord::Base
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
scope :archived, -> { where(archived: true) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
......
require 'hangouts_chat'
class HangoutsChatService < ChatNotificationService
def title
'Hangouts Chat'
end
def description
'Receive event notifications in Google Hangouts Chat'
end
def self.to_param
'hangouts_chat'
end
def help
'This service sends notifications about projects events to Google Hangouts Chat room.<br />
To set up this service:
<ol>
<li><a href="https://developers.google.com/hangouts/chat/how-tos/webhooks">Set up an incoming webhook for your room</a>. All notifications will come to this room.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def event_field(event)
end
def default_channel_placeholder
end
def webhook_placeholder
'https://chat.googleapis.com/v1/spaces…'
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' }
]
end
private
def notify(message, opts)
simple_text = parse_simple_text_message(message)
HangoutsChat::Sender.new(webhook).simple(simple_text)
end
def parse_simple_text_message(message)
header = message.pretext
return header if message.attachments.empty?
attachment = message.attachments.first
title = format_attachment_title(attachment)
body = attachment[:text]
[header, title, body].compact.join("\n")
end
def format_attachment_title(attachment)
return attachment[:title] unless attachment[:title_link]
"<#{attachment[:title_link]}|#{attachment[:title]}>"
end
end
......@@ -82,7 +82,7 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages(limit: nil)
def pages(limit: 0)
wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end
......
......@@ -254,6 +254,7 @@ class Service < ActiveRecord::Base
emails_on_push
external_wiki
flowdock
hangouts_chat
hipchat
irker
jira
......
# frozen_string_literal: true
class ApplicationSetting
class TermPolicy < BasePolicy
include Gitlab::Utils::StrongMemoize
......
# frozen_string_literal: true
require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base
......
# frozen_string_literal: true
module Ci
class BuildPolicy < CommitStatusPolicy
condition(:protected_ref) do
......
# frozen_string_literal: true
module Ci
class PipelinePolicy < BasePolicy
delegate { @subject.project }
......
# frozen_string_literal: true
module Ci
class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject
......
# frozen_string_literal: true
module Ci
class RunnerPolicy < BasePolicy
with_options scope: :subject, score: 0
......
# frozen_string_literal: true
module Ci
class TriggerPolicy < BasePolicy
delegate { @subject.project }
......
# frozen_string_literal: true
module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
......
# frozen_string_literal: true
class CommitStatusPolicy < BasePolicy
delegate { @subject.project }
......
# frozen_string_literal: true
class DeployKeyPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:private_deploy_key) { @subject.private? }
......
# frozen_string_literal: true
class DeployTokenPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:maintainer) { @subject.project.team.maintainer?(@user) }
......
# frozen_string_literal: true
class DeploymentPolicy < BasePolicy
delegate { @subject.project }
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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