Commit 7217a19e authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 22604-manual-actions

* master: (151 commits)
  Improve build status specs contexts descriptions
  Add some missing tests for detailed status methods
  Fix wrong error message expectation in API::Commits spec
  Remove trailing blank line from Allowable module
  Move admin settings spinach feature to rspec
  Encode when migrating ProcessCommitWorker jobs
  Prevent overflow with vertical scroll when we have space to show content
  Make rubocop happy
  API: Ability to cherry-pick a commit
  Be smarter when finding a sudoed user in API::Helpers
  Update manual build icon SVG
  Make it possible to mix `Gitlab::Routing` in
  Backport hooks on group policies for the EE-specific implementation
  Extract abilities checking module from ability model
  Extend tests for pipeline detailed status helpers
  Add tests for common build detailed status helpers
  Add missing tests for build `cancelable?` method
  Add tests for detailed build statuses factory
  Make it possible to retry build that was canceled
  Make build retryable only if complete and executed
  ...

Conflicts:
	app/assets/stylesheets/pages/pipelines.scss
	app/models/ci/build.rb
	app/models/ci/pipeline.rb
	app/models/ci/stage.rb
	app/models/commit_status.rb
	app/views/admin/runners/show.html.haml
	app/views/projects/builds/_header.html.haml
	app/views/projects/ci/builds/_build.html.haml
	app/views/projects/ci/builds/_build_pipeline.html.haml
	app/views/projects/ci/pipelines/_pipeline.html.haml
	app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
	app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
	app/views/projects/pipelines/_info.html.haml
	lib/gitlab/ci/status/build/common.rb
	lib/gitlab/ci/status/build/play.rb
	lib/gitlab/ci/status/build/stop.rb
	lib/gitlab/ci/status/core.rb
	spec/lib/gitlab/ci/status/pipeline/common_spec.rb
parents 92b0f54e 3e90aa11
...@@ -513,6 +513,21 @@ entry. ...@@ -513,6 +513,21 @@ entry.
- Fix broken Project API docs (Takuya Noguchi) - Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master) - Migrate invalid project members (owner -> master)
## 8.12.12 (2016-12-08)
- Replace MR access checks with use of MergeRequestsFinder
- Reenables /user API request to return private-token if user is admin and request is made with sudo
## 8.12.11 (2016-12-02)
- No changes
## 8.12.10 (2016-11-28)
- Fix information disclosure in `Projects::BlobController#update`
- Fix missing access checks on issue lookup using IssuableFinder
- Replace issue access checks with use of IssuableFinder
## 8.12.9 (2016-11-07) ## 8.12.9 (2016-11-07)
- Fix XSS issue in Markdown autolinker - Fix XSS issue in Markdown autolinker
......
...@@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2.7'
gem 'sidekiq-cron', '~> 0.4.4' gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
......
...@@ -126,7 +126,7 @@ GEM ...@@ -126,7 +126,7 @@ GEM
coffee-script-source (1.10.0) coffee-script-source (1.10.0)
colorize (0.7.7) colorize (0.7.7)
concurrent-ruby (1.0.2) concurrent-ruby (1.0.2)
connection_pool (2.2.0) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
creole (0.5.0) creole (0.5.0)
...@@ -648,10 +648,10 @@ GEM ...@@ -648,10 +648,10 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.2.1) sidekiq (4.2.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (~> 1.5) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
sidekiq-cron (0.4.4) sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
...@@ -928,7 +928,7 @@ DEPENDENCIES ...@@ -928,7 +928,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2) sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0) simplecov (= 0.12.0)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
this.CommitFile = (function() { this.CommitFile = (function() {
function CommitFile(file) { function CommitFile(file) {
if ($('.image', file).length) { if ($('.image', file).length) {
new ImageFile(file); new gl.ImageFile(file);
} }
} }
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
(function() { (function() {
this.ImageFile = (function() { gl.ImageFile = (function() {
var prepareFrames; var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2 // Width where images must fits in, for 2-up this gets divided by 2
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
var genericError, genericSuccess, showTooltip; var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) { genericSuccess = function(e) {
showTooltip(e.trigger, 'Copied!'); showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border // Clear the selection and blur the trigger so it loses its border
e.clearSelection(); e.clearSelection();
return $(e.trigger).blur(); return $(e.trigger).blur();
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
var originalTitle = $target.data('original-title'); var originalTitle = $target.data('original-title');
$target $target
.attr('title', 'Copied!') .attr('title', 'Copied')
.tooltip('fixTitle') .tooltip('fixTitle')
.tooltip('show') .tooltip('show')
.attr('title', originalTitle) .attr('title', originalTitle)
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
class Diff { class Diff {
constructor() { constructor() {
$('.files .diff-file').singleFileDiff(); const $diffFile = $('.files .diff-file');
$('.files .diff-file').filesCommentButton(); $diffFile.singleFileDiff();
$diffFile.filesCommentButton();
$diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited'); $('.content-wrapper .container-fluid').removeClass('container-limited');
......
...@@ -74,6 +74,8 @@ ...@@ -74,6 +74,8 @@
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath, newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath, helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
}; };
}, },
...@@ -227,7 +229,9 @@ ...@@ -227,7 +229,9 @@
:model="model" :model="model"
:toggleRow="toggleRow.bind(model)" :toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"></tr> :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0" <tr v-if="model.isOpen && model.children && model.children.length > 0"
is="environment-item" is="environment-item"
...@@ -235,7 +239,9 @@ ...@@ -235,7 +239,9 @@
:model="children" :model="children"
:toggleRow="toggleRow.bind(children)" :toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"> :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:commit-icon-svg="commitIconSvg">
</tr> </tr>
</template> </template>
......
...@@ -12,38 +12,18 @@ ...@@ -12,38 +12,18 @@
required: false, required: false,
default: () => [], default: () => [],
}, },
},
/**
* Appends the svg icon that were render in the index page.
* In order to reuse the svg instead of copy and paste in this template
* we need to render it outside this component using =custom_icon partial.
*
* TODO: Remove this when webpack is merged.
*
*/
mounted() {
const playIcon = document.querySelector('.play-icon-svg.hidden svg');
const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container');
const actionContainers = this.$el.querySelectorAll('.action-play-icon-container');
// Phantomjs does not have support to iterate a nodelist.
const actionsArray = [].slice.call(actionContainers);
if (playIcon && actionsArray && dropdownContainer) { playIconSvg: {
dropdownContainer.appendChild(playIcon.cloneNode(true)); type: String,
required: false,
actionsArray.forEach((element) => { },
element.appendChild(playIcon.cloneNode(true));
});
}
}, },
template: ` template: `
<div class="inline"> <div class="inline">
<div class="dropdown"> <div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="dropdown-play-icon-container"></span> <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
...@@ -53,7 +33,9 @@ ...@@ -53,7 +33,9 @@
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
class="js-manual-action-link"> class="js-manual-action-link">
<span class="action-play-icon-container"></span>
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
<span> <span>
{{action.name}} {{action.name}}
</span> </span>
......
...@@ -7,14 +7,14 @@ ...@@ -7,14 +7,14 @@
window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: { props: {
external_url: { externalUrl: {
type: String, type: String,
default: '', default: '',
}, },
}, },
template: ` template: `
<a class="btn external_url" :href="external_url" target="_blank"> <a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a> </a>
`, `,
......
...@@ -58,6 +58,16 @@ ...@@ -58,6 +58,16 @@
required: false, required: false,
default: false, default: false,
}, },
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
}, },
data() { data() {
...@@ -451,11 +461,12 @@ ...@@ -451,11 +461,12 @@
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
:commit_ref="commitRef" :commit-ref="commitRef"
:commit_url="commitUrl" :commit-url="commitUrl"
:short_sha="commitShortSha" :short-sha="commitShortSha"
:title="commitTitle" :title="commitTitle"
:author="commitAuthor"> :author="commitAuthor"
:commit-icon-svg="commitIconSvg">
</commit-component> </commit-component>
</div> </div>
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
...@@ -476,6 +487,7 @@ ...@@ -476,6 +487,7 @@
<div v-if="hasManualActions && canCreateDeployment" <div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container"> class="inline js-manual-actions-container">
<actions-component <actions-component
:play-icon-svg="playIconSvg"
:actions="manualActions"> :actions="manualActions">
</actions-component> </actions-component>
</div> </div>
...@@ -483,22 +495,22 @@ ...@@ -483,22 +495,22 @@
<div v-if="model.external_url && canReadEnvironment" <div v-if="model.external_url && canReadEnvironment"
class="inline js-external-url-container"> class="inline js-external-url-container">
<external-url-component <external-url-component
:external_url="model.external_url"> :external-url="model.external_url">
</external_url-component> </external-url-component>
</div> </div>
<div v-if="isStoppable && canCreateDeployment" <div v-if="isStoppable && canCreateDeployment"
class="inline js-stop-component-container"> class="inline js-stop-component-container">
<stop-component <stop-component
:stop_url="model.stop_path"> :stop-url="model.stop_path">
</stop-component> </stop-component>
</div> </div>
<div v-if="canRetry && canCreateDeployment" <div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container"> class="inline js-rollback-component-container">
<rollback-component <rollback-component
:is_last_deployment="isLastDeployment" :is-last-deployment="isLastDeployment"
:retry_url="retryUrl"> :retry-url="retryUrl">
</rollback-component> </rollback-component>
</div> </div>
</div> </div>
......
...@@ -7,19 +7,20 @@ ...@@ -7,19 +7,20 @@
window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: { props: {
retry_url: { retryUrl: {
type: String, type: String,
default: '', default: '',
}, },
is_last_deployment: {
isLastDeployment: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, },
template: ` template: `
<a class="btn" :href="retry_url" data-method="post" rel="nofollow"> <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<span v-if="is_last_deployment"> <span v-if="isLastDeployment">
Re-deploy Re-deploy
</span> </span>
<span v-else> <span v-else>
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
window.gl.environmentsList.StopComponent = Vue.component('stop-component', { window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: { props: {
stop_url: { stopUrl: {
type: String, type: String,
default: '', default: '',
}, },
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
template: ` template: `
<a class="btn stop-env-link" <a class="btn stop-env-link"
:href="stop_url" :href="stopUrl"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
rel="nofollow"> rel="nofollow">
......
...@@ -52,6 +52,10 @@ ...@@ -52,6 +52,10 @@
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
}, },
beforeInsert: function(value) { beforeInsert: function(value) {
if (value && !this.setting.skipSpecialCharacterTest) {
var withoutAt = value.substring(1);
if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
}
if (!GitLab.GfmAutoComplete.dataLoaded) { if (!GitLab.GfmAutoComplete.dataLoaded) {
return this.at; return this.at;
} else { } else {
...@@ -117,6 +121,7 @@ ...@@ -117,6 +121,7 @@
insertTpl: ':${name}:', insertTpl: ':${name}:',
data: ['loading'], data: ['loading'],
startWithSpace: false, startWithSpace: false,
skipSpecialCharacterTest: true,
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter, filter: this.DefaultOptions.filter,
...@@ -141,6 +146,7 @@ ...@@ -141,6 +146,7 @@
data: ['loading'], data: ['loading'],
startWithSpace: false, startWithSpace: false,
alwaysHighlightFirst: true, alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter, filter: this.DefaultOptions.filter,
...@@ -219,12 +225,13 @@ ...@@ -219,12 +225,13 @@
} }
}; };
})(this), })(this),
insertTpl: '${atwho-at}"${title}"', insertTpl: '${atwho-at}${title}',
data: ['loading'], data: ['loading'],
startWithSpace: false, startWithSpace: false,
callbacks: { callbacks: {
matcher: this.DefaultOptions.matcher, matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(milestones) { beforeSave: function(milestones) {
return $.map(milestones, function(m) { return $.map(milestones, function(m) {
if (m.title == null) { if (m.title == null) {
...@@ -284,18 +291,11 @@ ...@@ -284,18 +291,11 @@
callbacks: { callbacks: {
matcher: this.DefaultOptions.matcher, matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(merges) { beforeSave: function(merges) {
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
return "\"" + (sanitize(title)) + "\"";
} else {
return sanitize(title);
}
};
return $.map(merges, function(m) { return $.map(merges, function(m) {
return { return {
title: sanitizeLabelTitle(m.title), title: sanitize(m.title),
color: m.color, color: m.color,
search: "" + m.title search: "" + m.title
}; };
...@@ -308,6 +308,7 @@ ...@@ -308,6 +308,7 @@
at: '/', at: '/',
alias: 'commands', alias: 'commands',
searchKey: 'search', searchKey: 'search',
skipSpecialCharacterTest: true,
displayTpl: function(value) { displayTpl: function(value) {
var tpl = '<li>/${name}'; var tpl = '<li>/${name}';
if (value.aliases.length > 0) { if (value.aliases.length > 0) {
......
...@@ -650,6 +650,11 @@ ...@@ -650,6 +650,11 @@
} else if(value) { } else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
} }
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
return;
}
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
if (field && field.length) { if (field && field.length) {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
(function() { (function() {
window.ContributorsStatGraphUtil = { window.ContributorsStatGraphUtil = {
parse_log: function(log) { parse_log: function(log) {
var by_author, by_email, data, entry, i, len, total; var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {}; total = {};
by_author = {}; by_author = {};
by_email = {}; by_email = {};
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
if (total[entry.date] == null) { if (total[entry.date] == null) {
this.add_date(entry.date, total); this.add_date(entry.date, total);
} }
data = by_author[entry.author_name] || by_email[entry.author_email]; normalized_email = entry.author_email.toLowerCase();
data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) { if (data == null) {
data = this.add_author(entry, by_author, by_email); data = this.add_author(entry, by_author, by_email);
} }
...@@ -32,12 +33,14 @@ ...@@ -32,12 +33,14 @@
return collection[date].date = date; return collection[date].date = date;
}, },
add_author: function(author, by_author, by_email) { add_author: function(author, by_author, by_email) {
var data; var data, normalized_email;
data = {}; data = {};
data.author_name = author.author_name; data.author_name = author.author_name;
data.author_email = author.author_email; data.author_email = author.author_email;
normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data; by_author[author.author_name] = data;
return by_email[author.author_email] = data; by_email[normalized_email] = data;
return data;
}, },
store_data: function(entry, total, by_author) { store_data: function(entry, total, by_author) {
this.store_commits(total, by_author); this.store_commits(total, by_author);
......
/* eslint-disable */ /* eslint-disable class-methods-use-this */
((w) => { (() => {
w.gl = w.gl || {}; window.gl = window.gl || {};
class Members { class Members {
constructor() { constructor() {
this.addListeners(); this.addListeners();
this.initGLDropdown();
} }
addListeners() { addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit); $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
} }
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
$btn.glDropdown({
selectable: true,
isSelectable(selected, $el) {
return !$el.hasClass('is-active');
},
fieldName: $btn.data('field-name'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel(selected, $el) {
return $el.text();
},
clicked: (selected, $link) => {
this.formSubmit(null, $link);
},
});
});
}
removeRow(e) { removeRow(e) {
const $target = $(e.target); const $target = $(e.target);
if ($target.hasClass('btn-remove')) { if ($target.hasClass('btn-remove')) {
$target.closest('.member') $target.closest('.member')
.fadeOut(function () { .fadeOut(function fadeOutMemberRow() {
$(this).remove(); $(this).remove();
}); });
} }
} }
formSubmit() { formSubmit(e, $el = null) {
$(this).closest('form').trigger("submit.rails").end().disable(); const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
$this.closest('form').trigger('submit.rails');
$toggle.disable();
$dateInput.disable();
} }
formSuccess() { formSuccess(e) {
$(this).find('.js-member-update-control').enable(); const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
$toggle.enable();
$dateInput.enable();
}
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
return {
$memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
} }
} }
gl.Members = Members; gl.Members = Members;
})(window); })();
...@@ -40,19 +40,26 @@ ...@@ -40,19 +40,26 @@
$('#modal_merge_info').modal({ $('#modal_merge_info').modal({
show: false show: false
}); });
this.firstCICheck = true;
this.readyForCICheck = false;
this.readyForCIEnvironmentCheck = false;
this.cancel = false;
clearInterval(this.fetchBuildStatusInterval);
clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners(); this.clearEventListeners();
this.addEventListeners(); this.addEventListeners();
this.getCIStatus(false); this.getCIStatus(false);
this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon(); this.retrieveSuccessIcon();
this.pollCIStatus();
this.pollCIEnvironmentsStatus(); this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
maxInterval: 30000,
hiddenInterval: 120000,
incrementByFactorOf: 5000,
});
this.ciEnvironmentStatusInterval = new global.SmartInterval({
callback: this.getCIEnvironmentsStatus.bind(this),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
notifyPermissions(); notifyPermissions();
} }
...@@ -60,10 +67,6 @@ ...@@ -60,10 +67,6 @@
return $(document).off('page:change.merge_request'); return $(document).off('page:change.merge_request');
}; };
MergeRequestWidget.prototype.cancelPolling = function() {
return this.cancel = true;
};
MergeRequestWidget.prototype.addEventListeners = function() { MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages; var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
...@@ -72,9 +75,6 @@ ...@@ -72,9 +75,6 @@
var page; var page;
page = $('body').data('page').split(':').last(); page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) { if (allowedPages.indexOf(page) < 0) {
clearInterval(_this.fetchBuildStatusInterval);
clearInterval(_this.fetchBuildEnvironmentStatusInterval);
_this.cancelPolling();
return _this.clearEventListeners(); return _this.clearEventListeners();
} }
}; };
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix; return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) { } else if (data.merge_error) {
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>"); return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else { } else {
callback = function() { callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch); return merge_request_widget.mergeInProgress(deleteSourceBranch);
...@@ -114,6 +114,11 @@ ...@@ -114,6 +114,11 @@
}); });
}; };
MergeRequestWidget.prototype.cancelPolling = function () {
this.ciStatusInterval.cancel();
this.ciEnvironmentStatusInterval.cancel();
};
MergeRequestWidget.prototype.getMergeStatus = function() { MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) { return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data); return $('.mr-state-widget').replaceWith(data);
...@@ -131,18 +136,6 @@ ...@@ -131,18 +136,6 @@
} }
}; };
MergeRequestWidget.prototype.pollCIStatus = function() {
return this.fetchBuildStatusInterval = setInterval(((function(_this) {
return function() {
if (!_this.readyForCICheck) {
return;
}
_this.getCIStatus(true);
return _this.readyForCICheck = false;
};
})(this)), 10000);
};
MergeRequestWidget.prototype.getCIStatus = function(showNotification) { MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this; var _this;
_this = this; _this = this;
...@@ -150,23 +143,17 @@ ...@@ -150,23 +143,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) { return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) { return function(data) {
var message, status, title; var message, status, title;
if (_this.cancel) {
return;
}
_this.readyForCICheck = true;
if (data.status === '') { if (data.status === '') {
return; return;
} }
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status; _this.opts.ci_status = data.status;
_this.showCIStatus(data.status); _this.showCIStatus(data.status);
if (data.coverage) { if (data.coverage) {
_this.showCICoverage(data.coverage); _this.showCICoverage(data.coverage);
} }
// The first check should only update the UI, a notification if (showNotification) {
// should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
status = _this.ciLabelForStatus(data.status); status = _this.ciLabelForStatus(data.status);
if (status === "preparing") { if (status === "preparing") {
title = _this.opts.ci_title.preparing; title = _this.opts.ci_title.preparing;
...@@ -184,24 +171,13 @@ ...@@ -184,24 +171,13 @@
return Turbolinks.visit(_this.opts.builds_path); return Turbolinks.visit(_this.opts.builds_path);
}); });
} }
return _this.firstCICheck = false;
} }
}; };
})(this)); })(this));
}; };
MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
if (!this.readyForCIEnvironmentCheck) return;
this.getCIEnvironmentsStatus();
this.readyForCIEnvironmentCheck = false;
}, 300000);
};
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => { $.getJSON(this.opts.ci_environments_status_url, (environments) => {
if (this.cancel) return;
this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments); if (environments && environments.length) this.renderEnvironments(environments);
}); });
}; };
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
((global) => { ((global) => {
class Pipelines { class Pipelines {
constructor(options) { constructor(options = {}) {
if (options.initTabs && options.tabsOptions) { if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions); new global.LinkedTabs(options.tabsOptions);
...@@ -14,9 +14,11 @@ ...@@ -14,9 +14,11 @@
} }
addMarginToBuildColumns() { addMarginToBuildColumns() {
this.pipelineGraph = document.querySelector('.pipeline-graph'); this.pipelineGraph = document.querySelector('.js-pipeline-graph');
const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
for (buildNodeIndex in secondChildBuildNodes) { const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex]; const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling; const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
...@@ -28,6 +30,7 @@ ...@@ -28,6 +30,7 @@
const columnBuilds = previousColumn.querySelectorAll('.build'); const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
} }
this.pipelineGraph.classList.remove('hidden'); this.pipelineGraph.classList.remove('hidden');
} }
} }
......
...@@ -7,24 +7,31 @@ ...@@ -7,24 +7,31 @@
(() => { (() => {
class SmartInterval { class SmartInterval {
/** /**
* @param { function } callback Function to be called on each iteration (required) * @param { function } opts.callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily * when the page is hidden
* @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } opts.lazyStart Configure if timer is initialized on
* instantiation or lazily
* @param { boolean } opts.immediateExecution Configure if callback should
* be executed before the first interval.
*/ */
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { constructor(opts = {}) {
this.cfg = { this.cfg = {
callback, callback: opts.callback,
startingInterval, startingInterval: opts.startingInterval,
maxInterval, maxInterval: opts.maxInterval,
incrementByFactorOf, hiddenInterval: opts.hiddenInterval,
lazyStart, incrementByFactorOf: opts.incrementByFactorOf,
lazyStart: opts.lazyStart,
immediateExecution: opts.immediateExecution,
}; };
this.state = { this.state = {
intervalId: null, intervalId: null,
currentInterval: startingInterval, currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible', pageVisibility: 'visible',
}; };
...@@ -36,6 +43,11 @@ ...@@ -36,6 +43,11 @@
const cfg = this.cfg; const cfg = this.cfg;
const state = this.state; const state = this.state;
if (cfg.immediateExecution) {
cfg.immediateExecution = false;
cfg.callback();
}
state.intervalId = window.setInterval(() => { state.intervalId = window.setInterval(() => {
cfg.callback(); cfg.callback();
...@@ -54,14 +66,29 @@ ...@@ -54,14 +66,29 @@
this.stopTimer(); this.stopTimer();
} }
onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel();
}
}
// start a timer, using the existing interval // start a timer, using the existing interval
resume() { resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start(); this.start();
} }
onVisibilityVisible() {
this.cancel();
this.start();
}
destroy() { destroy() {
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload'); $(document).off('visibilitychange').off('page:before-unload');
} }
...@@ -80,11 +107,7 @@ ...@@ -80,11 +107,7 @@
initVisibilityChangeHandling() { initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling) // cancel interval when tab no longer shown (prevents cached pages from polling)
$(document) document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
} }
initPageUnloadHandling() { initPageUnloadHandling() {
...@@ -92,10 +115,11 @@ ...@@ -92,10 +115,11 @@
$(document).on('page:before-unload', () => this.cancel()); $(document).on('page:before-unload', () => this.cancel());
} }
handleVisibilityChange() { handleVisibilityChange(e) {
const state = this.state; this.state.pageVisibility = e.target.visibilityState;
const intervalAction = this.isPageVisible() ?
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; this.onVisibilityVisible :
this.onVisibilityHidden;
intervalAction.apply(this); intervalAction.apply(this);
} }
...@@ -111,6 +135,7 @@ ...@@ -111,6 +135,7 @@
incrementInterval() { incrementInterval() {
const cfg = this.cfg; const cfg = this.cfg;
const currentInterval = this.getCurrentInterval(); const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf; let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) { if (nextInterval > cfg.maxInterval) {
...@@ -120,6 +145,8 @@ ...@@ -120,6 +145,8 @@
this.setCurrentInterval(nextInterval); this.setCurrentInterval(nextInterval);
} }
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() { stopTimer() {
const state = this.state; const state = this.state;
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
* name * name
* ref_url * ref_url
*/ */
commit_ref: { commitRef: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
...@@ -32,16 +32,16 @@ ...@@ -32,16 +32,16 @@
/** /**
* Used to link to the commit sha. * Used to link to the commit sha.
*/ */
commit_url: { commitUrl: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
/** /**
* Used to show the commit short_sha that links to the commit url. * Used to show the commit short sha that links to the commit url.
*/ */
short_sha: { shortSha: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
...@@ -68,6 +68,11 @@ ...@@ -68,6 +68,11 @@
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
commitIconSvg: {
type: String,
required: false,
},
}, },
computed: { computed: {
...@@ -80,7 +85,7 @@ ...@@ -80,7 +85,7 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
hasCommitRef() { hasCommitRef() {
return this.commit_ref && this.commit_ref.name && this.commit_ref.ref_url; return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
}, },
/** /**
...@@ -110,24 +115,6 @@ ...@@ -110,24 +115,6 @@
}, },
}, },
/**
* In order to reuse the svg instead of copy and paste in this template
* we need to render it outside this component using =custom_icon partial.
* Make sure it has this structure:
* .commit-icon-svg.hidden
* svg
*
* TODO: Find a better way to include SVG
*/
mounted() {
const commitIconContainer = this.$el.querySelector('.commit-icon-container');
const commitIcon = document.querySelector('.commit-icon-svg.hidden svg');
if (commitIconContainer && commitIcon) {
commitIconContainer.appendChild(commitIcon.cloneNode(true));
}
},
template: ` template: `
<div class="branch-commit"> <div class="branch-commit">
...@@ -138,15 +125,15 @@ ...@@ -138,15 +125,15 @@
<a v-if="hasCommitRef" <a v-if="hasCommitRef"
class="monospace branch-name" class="monospace branch-name"
:href="commit_ref.ref_url"> :href="commitRef.ref_url">
{{commit_ref.name}} {{commitRef.name}}
</a> </a>
<div class="icon-container commit-icon commit-icon-container"></div> <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
<a class="commit-id monospace" <a class="commit-id monospace"
:href="commit_url"> :href="commitUrl">
{{short_sha}} {{shortSha}}
</a> </a>
<p class="commit-title"> <p class="commit-title">
...@@ -162,7 +149,7 @@ ...@@ -162,7 +149,7 @@
</a> </a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commit_url"> :href="commitUrl">
{{title}} {{title}}
</a> </a>
</span> </span>
......
@mixin btn-default { @mixin btn-default {
border-radius: 3px; border-radius: 3px;
font-size: $gl-font-size; font-size: $gl-font-size;
font-weight: 500; font-weight: 400;
padding: $gl-vert-padding $gl-btn-padding; padding: $gl-vert-padding $gl-btn-padding;
&:focus, &:focus,
......
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&[disabled] {
background-color: $input-bg-disabled;
cursor: not-allowed;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
border-bottom: 2px solid $link-underline-blue; border-bottom: 2px solid $link-underline-blue;
color: $black; color: $black;
font-weight: 600; font-weight: 600;
.badge {
color: $black;
}
} }
.badge { .badge {
......
...@@ -486,6 +486,7 @@ $jq-ui-default-color: #777; ...@@ -486,6 +486,7 @@ $jq-ui-default-color: #777;
$label-gray-bg: #f8fafc; $label-gray-bg: #f8fafc;
$label-inverse-bg: #333; $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 14px;
/* /*
* Lint * Lint
......
.deployments-container { @media (max-width: $screen-md-max) {
.deployments-container {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
}
} }
.environments-list-loading { .environments-list-loading {
......
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
.color-label { .color-label {
padding: 6px 10px; padding: 6px 10px;
border-radius: $label-border-radius;
} }
} }
......
...@@ -104,7 +104,8 @@ ...@@ -104,7 +104,8 @@
} }
.color-label { .color-label {
padding: 3px 4px; padding: 3px 7px;
border-radius: $label-border-radius;
} }
.dropdown-labels-error { .dropdown-labels-error {
......
...@@ -54,6 +54,10 @@ ...@@ -54,6 +54,10 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 50%; width: 50%;
} }
.dropdown-menu-toggle {
width: 100%;
}
} }
.member-access-text { .member-access-text {
......
.notification-list-item { .notification-list-item {
line-height: 34px; line-height: 34px;
.dropdown-menu {
@extend .dropdown-menu-align-right;
}
} }
.notification { .notification {
......
...@@ -37,13 +37,14 @@ ...@@ -37,13 +37,14 @@
} }
} }
.content-list { @media (max-width: $screen-md-max) {
.content-list {
&.pipelines, &.pipelines,
&.builds-content-list { &.builds-content-list {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
} }
}
} }
.pipeline-holder { .pipeline-holder {
...@@ -663,6 +664,10 @@ ...@@ -663,6 +664,10 @@
border-radius: 3px; border-radius: 3px;
color: $gl-text-color; color: $gl-text-color;
} }
.stage {
max-width: 100px;
width: 100px;
} }
.ci-status-icon svg { .ci-status-icon svg {
......
...@@ -188,6 +188,10 @@ ...@@ -188,6 +188,10 @@
margin-left: 10px; margin-left: 10px;
} }
.notification-dropdown .dropdown-menu {
@extend .dropdown-menu-align-right;
}
.download-button { .download-button {
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
margin-left: 0; margin-left: 0;
......
.snippet-row {
.title {
margin-bottom: 2px;
}
.snippet-filename {
padding: 0 2px;
}
}
.snippet-form-holder .file-holder .file-title { .snippet-form-holder .file-holder .file-title {
padding: 2px; padding: 2px;
} }
...@@ -24,11 +34,17 @@ ...@@ -24,11 +34,17 @@
padding-bottom: $gl-padding; padding-bottom: $gl-padding;
} }
.snippet-header {
padding: $gl-padding 0;
}
.snippet-title { .snippet-title {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
padding: $gl-padding; }
padding-left: 0;
.snippet-edited-ago {
color: $gray-darkest;
} }
.snippet-actions { .snippet-actions {
......
...@@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -35,13 +35,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end end
member_ids = @project_members.pluck(:id) wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
if group_members @project_members = Member.
member_ids += group_members.pluck(:id) where(wheres.join(' OR ')).
end order(access_level: :desc).page(params[:page])
@project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @requesters = AccessRequestsFinder.new(@project).execute(current_user)
......
...@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end end
def update def update
# Release belongs to Tag which is not active record object,
# it exists only to save a description to each Tag.
# If description is empty we should destroy the existing record.
if release_params[:description].present?
release.update_attributes(release_params) release.update_attributes(release_params)
else
release.destroy
end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end end
......
...@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@snippets = SnippetsFinder.new.execute(current_user, { @snippets = SnippetsFinder.new.execute(
current_user,
filter: :by_project, filter: :by_project,
project: @project project: @project,
}) scope: params[:scope]
)
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
end end
......
...@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController ...@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
end end
end end
def destroy
super
# hide the signed_out notice
flash[:notice] = nil
end
private private
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
......
class SnippetsFinder class SnippetsFinder
def execute(current_user, params = {}) def execute(current_user, params = {})
filter = params[:filter] filter = params[:filter]
user = params.fetch(:user, current_user)
case filter case filter
when :all then when :all then
snippets(current_user).fresh snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then when :by_user then
by_user(current_user, params[:user], params[:scope]) by_user(current_user, user, params[:scope])
when :by_project when :by_project
by_project(current_user, params[:project]) by_project(current_user, params[:project], params[:scope])
end end
end end
...@@ -29,35 +32,35 @@ class SnippetsFinder ...@@ -29,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope) def by_user(current_user, user, scope)
snippets = user.snippets.fresh snippets = user.snippets.fresh
return snippets.are_public unless current_user if current_user
include_private = user == current_user
if user == current_user by_scope(snippets, scope, include_private)
case scope
when 'are_internal' then
snippets.are_internal
when 'are_private' then
snippets.are_private
when 'are_public' then
snippets.are_public
else
snippets
end
else else
snippets.public_and_internal snippets.are_public
end end
end end
def by_project(current_user, project) def by_project(current_user, project, scope)
snippets = project.snippets.fresh snippets = project.snippets.fresh
if current_user if current_user
if project.team.member?(current_user) || current_user.admin? include_private = project.team.member?(current_user) || current_user.admin?
snippets by_scope(snippets, scope, include_private)
else else
snippets.public_and_internal snippets.are_public
end end
else end
def by_scope(snippets, scope = nil, include_private = false)
case scope.to_s
when 'are_private'
include_private ? snippets.are_private : Snippet.none
when 'are_internal'
snippets.are_internal
when 'are_public'
snippets.are_public snippets.are_public
else
include_private ? snippets : snippets.public_and_internal
end end
end end
end end
...@@ -16,7 +16,7 @@ module ButtonHelper ...@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage # See http://clipboardjs.com/#usage
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to Clipboard' title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
......
...@@ -14,10 +14,12 @@ module EnvironmentHelper ...@@ -14,10 +14,12 @@ module EnvironmentHelper
end end
end end
def deployment_link(deployment) def deployment_link(deployment, text: nil)
return unless deployment return unless deployment
link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] link_label = text ? text : "##{deployment.iid}"
link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
end end
def last_deployment_link_for_environment_build(project, build) def last_deployment_link_for_environment_build(project, build)
......
...@@ -45,6 +45,12 @@ module EventsHelper ...@@ -45,6 +45,12 @@ module EventsHelper
@project.feature_available?(feature_key, current_user) @project.feature_available?(feature_key, current_user)
end end
def comments_visible?
event_filter_visible(:repository) ||
event_filter_visible(:merge_requests) ||
event_filter_visible(:issues)
end
def event_preposition(event) def event_preposition(event)
if event.push? || event.commented? || event.target if event.push? || event.commented? || event.target
"at" "at"
......
...@@ -159,6 +159,11 @@ module GitlabRoutingHelper ...@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end end
# Snippets
def personal_snippet_url(snippet, *args)
snippet_url(snippet)
end
# Groups # Groups
## Members ## Members
......
...@@ -8,6 +8,17 @@ module SnippetsHelper ...@@ -8,6 +8,17 @@ module SnippetsHelper
end end
end end
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
def subject_snippets_path(subject = nil, opts = nil)
if subject.is_a?(Project)
namespace_project_snippets_path(subject.namespace, subject, opts)
else # assume subject === User
dashboard_snippets_path(opts)
end
end
# Get an array of line numbers surrounding a matching # Get an array of line numbers surrounding a matching
# line, bounded by min/max. # line, bounded by min/max.
# #
......
...@@ -101,7 +101,9 @@ module Ci ...@@ -101,7 +101,9 @@ module Ci
end end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory.new(self, current_user).fabricate! Gitlab::Ci::Status::Build::Factory
.new(self, current_user)
.fabricate!
end end
def manual? def manual?
...@@ -132,7 +134,8 @@ module Ci ...@@ -132,7 +134,8 @@ module Ci
end end
def retryable? def retryable?
project.builds_enabled? && commands.present? && complete? project.builds_enabled? && commands.present? &&
(success? || failed? || canceled?)
end end
def retried? def retried?
......
...@@ -337,7 +337,9 @@ module Ci ...@@ -337,7 +337,9 @@ module Ci
end end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory.new(self, current_user).fabricate! Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)
.fabricate!
end end
private private
......
...@@ -23,7 +23,9 @@ module Ci ...@@ -23,7 +23,9 @@ module Ci
end end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Stage::Factory.new(self, current_user).fabricate! Gitlab::Ci::Status::Stage::Factory
.new(self, current_user)
.fabricate!
end end
def statuses def statuses
......
...@@ -133,6 +133,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -133,6 +133,8 @@ class CommitStatus < ActiveRecord::Base
end end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Factory.new(self, current_user).fabricate! Gitlab::Ci::Status::Factory
.new(self, current_user)
.fabricate!
end end
end end
...@@ -7,6 +7,7 @@ module Routable ...@@ -7,6 +7,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy has_one :route, as: :source, autosave: true, dependent: :destroy
validates_associated :route validates_associated :route
validates :route, presence: true
before_validation :update_route_path, if: :full_path_changed? before_validation :update_route_path, if: :full_path_changed?
end end
...@@ -28,17 +29,17 @@ module Routable ...@@ -28,17 +29,17 @@ module Routable
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take where_full_path_in([path]).reorder(order_sql).take
end end
# Builds a relation to find multiple objects by their full paths. # Builds a relation to find multiple objects by their full paths.
# #
# Usage: # Usage:
# #
# Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) # Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def where_paths_in(paths) def where_full_path_in(paths)
wheres = [] wheres = []
cast_lower = Gitlab::Database.postgresql? cast_lower = Gitlab::Database.postgresql?
......
...@@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork? validate :validate_fork, unless: :closed_without_fork?
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
end
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) }
......
...@@ -3,7 +3,8 @@ require "addressable/uri" ...@@ -3,7 +3,8 @@ require "addressable/uri"
class BuildkiteService < CiService class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com" ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token, :enable_ssl_verification prop_accessor :project_url, :token
boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated? validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
......
class DroneCiService < CiService class DroneCiService < CiService
prop_accessor :drone_url, :token, :enable_ssl_verification prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated? validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
......
class EmailsOnPushService < Service class EmailsOnPushService < Service
prop_accessor :send_from_committer_email boolean_accessor :send_from_committer_email
prop_accessor :disable_diffs boolean_accessor :disable_diffs
prop_accessor :recipients prop_accessor :recipients
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :activated?
...@@ -33,11 +33,11 @@ class EmailsOnPushService < Service ...@@ -33,11 +33,11 @@ class EmailsOnPushService < Service
end end
def send_from_committer_email? def send_from_committer_email?
self.send_from_committer_email == "1" Gitlab::Utils.to_boolean(self.send_from_committer_email)
end end
def disable_diffs? def disable_diffs?
self.disable_diffs == "1" Gitlab::Utils.to_boolean(self.disable_diffs)
end end
def fields def fields
......
...@@ -8,8 +8,8 @@ class HipchatService < Service ...@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd ul ol li dl dt dd
] ]
prop_accessor :token, :room, :server, :notify, :color, :api_version prop_accessor :token, :room, :server, :color, :api_version
boolean_accessor :notify_only_broken_builds boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
def initialize_properties def initialize_properties
...@@ -75,7 +75,7 @@ class HipchatService < Service ...@@ -75,7 +75,7 @@ class HipchatService < Service
end end
def message_options(data = nil) def message_options(data = nil)
{ notify: notify.present? && notify == '1', color: message_color(data) } { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end end
def create_message(data) def create_message(data)
......
...@@ -2,7 +2,8 @@ require 'uri' ...@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :colorize_messages, :recipients, :channels prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :activated?
before_validation :get_channels before_validation :get_channels
......
...@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService ...@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService
entity_title = data[:entity][:title] entity_title = data[:entity][:title]
project_name = data[:project][:name] project_name = data[:project][:name]
message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}" link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title) link_props = build_remote_link_props(url: entity_url, title: link_title)
......
...@@ -304,10 +304,6 @@ class User < ActiveRecord::Base ...@@ -304,10 +304,6 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token personal_access_token.user if personal_access_token
end end
def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
# Returns a user for the given SSH key. # Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id) def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
......
...@@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy ...@@ -15,5 +15,11 @@ class GroupMemberPolicy < BasePolicy
elsif @user == target_user elsif @user == target_user
can! :destroy_group_member can! :destroy_group_member
end end
additional_rules!
end
def additional_rules!
# This is meant to be overriden in EE
end end
end end
...@@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy ...@@ -33,6 +33,8 @@ class GroupPolicy < BasePolicy
if globally_viewable && @subject.request_access_enabled && !member if globally_viewable && @subject.request_access_enabled && !member
can! :request_access can! :request_access
end end
additional_rules!(master)
end end
def can_read_group? def can_read_group?
...@@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy ...@@ -43,4 +45,8 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(@subject).execute(@user).any? GroupProjectsFinder.new(@subject).execute(@user).any?
end end
def additional_rules!(master)
# This is meant to be overriden in EE
end
end end
...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user if @subject.author == @user
can! :read_personal_snippet can! :read_personal_snippet
can! :update_personal_snippet can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet can! :admin_personal_snippet
end end
unless @user.external?
can! :create_personal_snippet
end
if @subject.internal? && !@user.external? if @subject.internal? && !@user.external?
can! :read_personal_snippet can! :read_personal_snippet
end end
......
...@@ -35,7 +35,7 @@ module Commits ...@@ -35,7 +35,7 @@ module Commits
success success
else else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg raise ChangeError, error_msg
end end
end end
......
...@@ -55,8 +55,9 @@ module MergeRequests ...@@ -55,8 +55,9 @@ module MergeRequests
# Refresh merge request diff if we push to source or target branch of merge request # Refresh merge request diff if we push to source or target branch of merge request
# Note: we should update merge requests from forks too # Note: we should update merge requests from forks too
def reload_merge_requests def reload_merge_requests
merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a merge_requests = @project.merge_requests.opened.
merge_requests += fork_merge_requests.by_branch(@branch_name).to_a by_source_or_target_branch(@branch_name).to_a
merge_requests += fork_merge_requests
merge_requests = filter_merge_requests(merge_requests) merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
...@@ -157,13 +158,14 @@ module MergeRequests ...@@ -157,13 +158,14 @@ module MergeRequests
def merge_requests_for_source_branch def merge_requests_for_source_branch
@source_merge_requests ||= begin @source_merge_requests ||= begin
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a merge_requests += fork_merge_requests
filter_merge_requests(merge_requests) filter_merge_requests(merge_requests)
end end
end end
def fork_merge_requests def fork_merge_requests
@fork_merge_requests ||= @project.fork_merge_requests.opened @fork_merge_requests ||= @project.fork_merge_requests.opened.
where(source_branch: @branch_name).to_a
end end
def branch_added? def branch_added?
......
# Extra methods for uploader # Extra methods for uploader
module UploaderHelper module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff] IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg]
# We recommend using the .mp4 format over .mov. Videos in .mov format can # We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the # still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play # proper MIME type video/mp4 and not video/quicktime or your videos won't play
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
%strong ##{build.id} %strong ##{build.id}
%td.status %td.status
= render "ci/status/icon_with_description", subject: build = render 'ci/status/badge', status: build.detailed_status(current_user)
%td.status %td.status
- if project - if project
......
- status = local_assigns.fetch(:status)
- if status.has_details?
= link_to status.details_path, class: "ci-status ci-#{status}" do
= custom_icon(status.icon)
= status.text
- else
%span{ class: "ci-status ci-#{status}" }
= custom_icon(status.icon)
= status.text
%ul.nav-links .top-area
%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets Your Snippets
= nav_link(page: explore_snippets_path) do = nav_link(page: explore_snippets_path) do
= link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
Explore Snippets Explore Snippets
- if current_user
.nav-controls.hidden-xs
= link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
New snippet
...@@ -2,41 +2,11 @@ ...@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path - header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head' = render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
.nav-block .visible-xs
.controls.hidden-xs &nbsp;
= link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
= icon('plus')
New snippet
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
= link_to dashboard_snippets_path do
All
%span.badge
= current_user.snippets.count
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to dashboard_snippets_path(scope: 'are_private') do
Private
%span.badge
= current_user.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to dashboard_snippets_path(scope: 'are_internal') do
Internal
%span.badge
= current_user.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public
%span.badge
= current_user.snippets.are_public.count
.visible-xs
= link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
= icon('plus')
New snippet New snippet
= render 'snippets/snippets' = render partial: 'snippets/snippets', locals: { link_project: true }
...@@ -6,12 +6,4 @@ ...@@ -6,12 +6,4 @@
- else - else
= render 'explore/head' = render 'explore/head'
.row-content-block = render partial: 'snippets/snippets', locals: { link_project: true }
- if current_user
= link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
New snippet
.oneline
Public snippets created by you and other users are listed here
= render 'snippets/snippets'
:plain :plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
- if group_issues(@group).exists? - if group_issues(@group).exists?
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls
- if current_user - if current_user
.nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss') = icon('rss')
%span.icon-label %span.icon-label
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
......
- page_title @path.split("/").reverse.map(&:humanize) - page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki .documentation.wiki
= markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com") = markdown @markdown
- if current_user - if current_user
- can_admin_group = can?(current_user, :admin_group, @group) - can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group) - can_edit = can?(current_user, :admin_group, @group)
- member = @group.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_group_member, member)
- if can_admin_group || can_edit || can_leave - if can_admin_group || can_edit
.controls .controls
.dropdown.group-settings-dropdown .dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
...@@ -14,13 +12,7 @@ ...@@ -14,13 +12,7 @@
- if can_admin_group - if can_admin_group
= nav_link(path: 'groups#projects') do = nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects' = link_to 'Projects', projects_group_path(@group), title: 'Projects'
- if (can_edit || can_leave) && can_admin_group - if can_edit && can_admin_group
%li.divider %li.divider
- if can_edit
%li %li
= link_to 'Edit Group', edit_group_path(@group) = link_to 'Edit Group', edit_group_path(@group)
- if can_leave
%li
= link_to polymorphic_path([:leave, @group, :members]),
data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
...@@ -6,23 +6,14 @@ ...@@ -6,23 +6,14 @@
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
-# We don't use @project.team.find_member because it searches for group members too...
- member = @project.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', can_edit: can_edit = render 'layouts/nav/project_settings', can_edit: can_edit
- if can_edit || can_leave
%li.divider
- if can_edit - if can_edit
%li.divider
%li %li
= link_to edit_project_path(@project) do = link_to edit_project_path(@project) do
Edit Project Edit Project
- if can_leave
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
.scrolling-tabs-container{ class: nav_control_class } .scrolling-tabs-container{ class: nav_control_class }
.fade-left .fade-left
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%br %br
.clearfix .clearfix
.form-group.pull-left.global-notification-setting .form-group.pull-left.global-notification-setting
= render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true = render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix .clearfix
......
.content-block.build-header .content-block.build-header
.header-content .header-content
= render "ci/status/icon_with_description", subject: build = render 'ci/status/badge', status: @build.detailed_status(current_user)
Build Build
%strong ##{@build.id} %strong ##{@build.id}
in pipeline in pipeline
......
...@@ -46,8 +46,7 @@ ...@@ -46,8 +46,7 @@
- else - else
This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment) - if environment.try(:last_deployment)
and will overwrite the and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
= link_to 'latest deployment', deployment_link(environment.last_deployment)
.prepend-top-default .prepend-top-default
- if @build.erased? - if @build.erased?
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
%tr.build.commit{class: ('retried' if retried)} %tr.build.commit{class: ('retried' if retried)}
%td.status %td.status
= render "ci/status/icon_with_description", subject: build = render "ci/status/badge", status: build.detailed_status(current_user)
%td.branch-commit %td.branch-commit
- if can?(current_user, :read_build, build) - if can?(current_user, :read_build, build)
......
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do
= ci_icon_for_status('play')
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
.ci-status-text= subject.name
- else
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
%tr.commit %tr.commit
%td.commit-link %td.commit-link
= render "ci/status/icon_with_description", subject: pipeline = render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td %td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)} %h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body .modal-body
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch .form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label' = label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -23,12 +23,11 @@ ...@@ -23,12 +23,11 @@
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
.js-create-merge-request-container .js-create-merge-request-container
.checkbox .checkbox
- nonce = SecureRandom.hex = label_tag do
= label_tag "create_merge_request-#{nonce}" do = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
Start a <strong>new merge request</strong> with these changes Start a <strong>new merge request</strong> with these changes
- else - else
= hidden_field_tag 'create_merge_request', 1 = hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions .form-actions
= submit_tag label, class: 'btn btn-create' = submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
......
.page-content-header .page-content-header
.header-main-content .header-main-content
%strong Commit %strong
%strong.monospace.js-details-short= @commit.short_id
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
%span.text-expander
\...
%span.js-details-content.hide
%strong.monospace.commit-hash-full= @commit.id
= clipboard_button(clipboard_text: @commit.id) = clipboard_button(clipboard_text: @commit.id)
= @commit.short_id
%span.hidden-xs authored %span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)} #{time_ago_with_tooltip(@commit.authored_date)}
%span by %span by
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
in in
= time_interval_in_words pipeline.duration = time_interval_in_words pipeline.duration
.row-content-block.build-content.middle-block.hidden .row-content-block.build-content.middle-block.js-pipeline-graph.hidden
= render "projects/pipelines/graph", pipeline: pipeline = render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present? - if pipeline.yaml_errors.present?
......
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
such as compressing file revisions and removing unreachable objects. such as compressing file revisions and removing unreachable objects.
.col-lg-9 .col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save" method: :post, class: "btn btn-default"
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-3
......
...@@ -17,4 +17,6 @@ ...@@ -17,4 +17,6 @@
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
"help-page-path" => help_page_path("ci/environments"), "help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class}} "css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"),
"play-icon-svg" => custom_icon("icon_play")}}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%tr.generic_commit_status{class: ('retried' if retried)} %tr.generic_commit_status{class: ('retried' if retried)}
%td.status %td.status
= render "ci/status/icon_with_description", subject: generic_commit_status = render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
%td.generic_commit_status-link %td.generic_commit_status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url - if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
......
%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } }
- if subject.target_url
= link_to subject.target_url do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
%span.ci-status-text= subject.name
- else
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
%span.ci-status-text= subject.name
:plain :plain
var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}'); var $listItem = $('#{escape_javascript(render('shared/members/group', group_link: @group_link))}');
$("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name')); $("#group_member_#{@group_link.id} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("#group_member_#{@group_link.id}"));
.page-content-header .page-content-header
.header-main-content .header-main-content
= render "ci/status/icon_with_description", subject: @pipeline = render 'ci/status/badge', status: @pipeline.detailed_status(current_user)
%strong Pipeline ##{@commit.pipelines.last.id} %strong Pipeline ##{@commit.pipelines.last.id}
triggered #{time_ago_with_tooltip(@commit.authored_date)} by triggered #{time_ago_with_tooltip(@commit.authored_date)} by
= author_avatar(@commit, size: 24) = author_avatar(@commit, size: 24)
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
.tab-content .tab-content
#js-tab-pipeline.tab-pane #js-tab-pipeline.tab-pane
.build-content.middle-block .build-content.middle-block.js-pipeline-graph
= render "projects/pipelines/graph", pipeline: pipeline = render "projects/pipelines/graph", pipeline: pipeline
#js-tab-builds.tab-pane #js-tab-builds.tab-pane
......
:plain :plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
.hidden-xs .hidden-xs
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
New snippet
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit Edit
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown .visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......
- page_title "Snippets" - page_title "Snippets"
.sub-header-block - if current_user
.top-area
- include_private = @project.team.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
.nav-controls.hidden-xs
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
New snippet New snippet
.oneline - if can?(current_user, :create_project_snippet, @project)
Share code pastes with others out of git repository .visible-xs
&nbsp;
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
New snippet
= render 'snippets/snippets' = render 'snippets/snippets'
...@@ -5,14 +5,11 @@ ...@@ -5,14 +5,11 @@
%tr %tr
%th Name %th Name
%th.hidden-xs %th.hidden-xs
.pull-left Last Commit .pull-left Last commit
.last-commit.hidden-sm.pull-left .last-commit.hidden-sm.pull-left
&nbsp;
%i.fa.fa-angle-right
&nbsp;
%small.light %small.light
= clipboard_button(clipboard_text: @commit.id)
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
= time_ago_with_tooltip(@commit.committed_date) = time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title = @commit.full_title
%small.commit-history-link-spacer &#124; %small.commit-history-link-spacer &#124;
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info .snippet-info
= "##{snippet_title.id}" = snippet_title.to_reference
%span %span
by by
= link_to user_snippets_path(snippet_title.author) do = link_to user_snippets_path(snippet_title.author) do
......
...@@ -6,6 +6,6 @@ ...@@ -6,6 +6,6 @@
= event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.merged, 'Merge events'
- if event_filter_visible(:issues) - if event_filter_visible(:issues)
= event_filter_link EventFilter.issue, 'Issue events' = event_filter_link EventFilter.issue, 'Issue events'
- if event_filter_visible(:issues) - if comments_visible?
= event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team' = event_filter_link EventFilter.team, 'Team'
- if can?(current_user, :request_access, source) - model_name = source.model_name.to_s.downcase
- if requester = source.requesters.find_by(user_id: current_user.id)
- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
= link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source) },
class: 'btn'
- elsif requester = source.requesters.find_by(user_id: current_user.id)
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
method: :delete, method: :delete,
data: { confirm: remove_member_message(requester) }, data: { confirm: remove_member_message(requester) },
class: 'btn' class: 'btn'
- else - elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]), = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post, method: :post,
class: 'btn' class: 'btn'
- group_link = local_assigns[:group_link] - group_link = local_assigns[:group_link]
- group = group_link.group - group = group_link.group
- can_admin_member = can?(current_user, :admin_project_member, @project) - can_admin_member = can?(current_user, :admin_project_member, @project)
%li.member.group_member{ id: "group_member_#{group_link.id}" } - dom_id = "group_member_#{group_link.id}"
%li.member.group_member{ id: dom_id }
%span{ class: "list-item-name" } %span{ class: "list-item-name" }
= image_tag group_icon(group), class: "avatar s40", alt: '' = image_tag group_icon(group), class: "avatar s40", alt: ''
%strong %strong
...@@ -14,7 +15,23 @@ ...@@ -14,7 +15,23 @@
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls .controls.member-controls
= form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do = form_tag namespace_project_group_link_path(@project.namespace, @project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
= select_tag 'group_link[group_access]', options_for_select(ProjectGroupLink.access_options, group_link.group_access), class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{group.id}", disabled: !can_admin_member = hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
disabled: !can_admin_member,
data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
%span.dropdown-toggle-text
= group_link.human_access
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
= dropdown_title("Change permissions")
.dropdown-content
%ul
- Gitlab::Access.options.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if group_link.group_access == role_id),
data: { id: role_id, el_id: dom_id }
.prepend-left-5.clearable-input.member-form-control .prepend-left-5.clearable-input.member-form-control
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
......
...@@ -48,9 +48,25 @@ ...@@ -48,9 +48,25 @@
- if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
- if user != current_user - if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member = f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
disabled: !can_admin_member,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text
= member.human_access
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
= dropdown_title("Change permissions")
.dropdown-content
%ul
- Gitlab::Access.options.each do |role, role_id|
%li
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
.prepend-left-5.clearable-input.member-form-control .prepend-left-5.clearable-input.member-form-control
= f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
- else - else
%span.member-access-text= member.human_access %span.member-access-text= member.human_access
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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