Commit e03c85b9 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into fl-mr-widget-1

* master: (38 commits)
  Added more tests and corrected typos
  Ban Rugged from Repository
  Improve doc/development/automatic_ce_ee_merge.md
  fixed infinite loop crashing tests
  Converted todos.js to axios
  Converted usage_ping.js to use axios
  Converted pager.js to axios
  Converted notifications_form.js to axios
  Converted notes.js to axios
  Converted branch_graph.js to axios
  Converted mini_pipeline_graph_dropdown.js to axios
  Include subgroup issuables on the group page
  Remove grpc and google-protobuf platform-specific version markers in Gemfile.lock
  Replace $.ajax in find file with axios
  Replace $.ajax in activity calendar with axios
  Remove namespaced internationalization import
  Fix subgroup creation docs
  Fix a JSON schema that doesn't include enough fields
  Make user/author use project.creator in most factories
  Enable RuboCop Style/RegexpLiteral
  ...
parents 3fed0302 402f3dfc
...@@ -704,7 +704,9 @@ Style/RedundantSelf: ...@@ -704,7 +704,9 @@ Style/RedundantSelf:
# Configuration parameters: EnforcedStyle, AllowInnerSlashes. # Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed # SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral: Style/RegexpLiteral:
Enabled: false Enabled: true
EnforcedStyle: mixed
AllowInnerSlashes: false
# Offense count: 36 # Offense count: 36
# Cop supports --auto-correct. # Cop supports --auto-correct.
......
...@@ -340,7 +340,7 @@ GEM ...@@ -340,7 +340,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.4.1.1) google-protobuf (3.5.1.1)
googleapis-common-protos-types (1.0.1) googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.5.3) googleauth (0.5.3)
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { s__ } from './locale'; import { __ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -451,7 +451,7 @@ class AwardsHandler { ...@@ -451,7 +451,7 @@ class AwardsHandler {
callback(); callback();
} }
}) })
.catch(() => flash(s__('Something went wrong on our end.'))); .catch(() => flash(__('Something went wrong on our end.')));
} }
} }
......
import { getLocationHash } from './url_utility'; import { getLocationHash } from './url_utility';
import axios from './axios_utils';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
...@@ -382,22 +383,16 @@ export const resetFavicon = () => { ...@@ -382,22 +383,16 @@ export const resetFavicon = () => {
} }
}; };
export const setCiStatusFavicon = (pageUrl) => { export const setCiStatusFavicon = pageUrl =>
$.ajax({ axios.get(pageUrl)
url: pageUrl, .then(({ data }) => {
dataType: 'json',
success: (data) => {
if (data && data.favicon) { if (data && data.favicon) {
setFavicon(data.favicon); setFavicon(data.favicon);
} else { } else {
resetFavicon(); resetFavicon();
} }
}, })
error: () => { .catch(resetFavicon);
resetFavicon();
},
});
};
export const spriteIcon = (icon, className = '') => { export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : ''; const classAttribute = className.length > 0 ? `class="${className}"` : '';
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import Flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils';
/** /**
* In each pipelines table we have a mini pipeline graph for each pipeline. * In each pipelines table we have a mini pipeline graph for each pipeline.
...@@ -78,26 +79,21 @@ export default class MiniPipelineGraph { ...@@ -78,26 +79,21 @@ export default class MiniPipelineGraph {
const button = e.relatedTarget; const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint; const endpoint = button.dataset.stageEndpoint;
return $.ajax({
dataType: 'json',
type: 'GET',
url: endpoint,
beforeSend: () => {
this.renderBuildsList(button, ''); this.renderBuildsList(button, '');
this.toggleLoading(button); this.toggleLoading(button);
},
success: (data) => { axios.get(endpoint)
.then(({ data }) => {
this.toggleLoading(button); this.toggleLoading(button);
this.renderBuildsList(button, data.html); this.renderBuildsList(button, data.html);
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
}, })
error: () => { .catch(() => {
this.toggleLoading(button); this.toggleLoading(button);
if ($(button).parent().hasClass('open')) { if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle'); $(button).dropdown('toggle');
} }
new Flash('An error occurred while fetching the builds.', 'alert'); flash('An error occurred while fetching the builds.', 'alert');
},
}); });
} }
......
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
import Raphael from './raphael'; import Raphael from './raphael';
export default (function() { export default (function() {
...@@ -26,16 +29,13 @@ export default (function() { ...@@ -26,16 +29,13 @@ export default (function() {
} }
BranchGraph.prototype.load = function() { BranchGraph.prototype.load = function() {
return $.ajax({ axios.get(this.options.url)
url: this.options.url, .then(({ data }) => {
method: "get",
dataType: "json",
success: $.proxy(function(data) {
$(".loading", this.element).hide(); $(".loading", this.element).hide();
this.prepareData(data.days, data.commits); this.prepareData(data.days, data.commits);
return this.buildGraph(); this.buildGraph();
}, this) })
}); .catch(() => __('Error fetching network graph.'));
}; };
BranchGraph.prototype.prepareData = function(days, commits) { BranchGraph.prototype.prepareData = function(days, commits) {
......
...@@ -16,6 +16,7 @@ import Autosize from 'autosize'; ...@@ -16,6 +16,7 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash'; import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
...@@ -252,26 +253,20 @@ export default class Notes { ...@@ -252,26 +253,20 @@ export default class Notes {
return; return;
} }
this.refreshing = true; this.refreshing = true;
return $.ajax({ axios.get(this.notes_url, {
url: this.notes_url, headers: {
headers: { 'X-Last-Fetched-At': this.last_fetched_at }, 'X-Last-Fetched-At': this.last_fetched_at,
dataType: 'json', },
success: (function(_this) { }).then(({ data }) => {
return function(data) { const notes = data.notes;
var notes; this.last_fetched_at = data.last_fetched_at;
notes = data.notes; this.setPollingInterval(data.notes.length);
_this.last_fetched_at = data.last_fetched_at; $.each(notes, (i, note) => this.renderNote(note));
_this.setPollingInterval(data.notes.length);
return $.each(notes, function(i, note) { this.refreshing = false;
_this.renderNote(note); }).catch(() => {
this.refreshing = false;
}); });
};
})(this)
}).always((function(_this) {
return function() {
return _this.refreshing = false;
};
})(this));
} }
/** /**
......
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class NotificationsForm { export default class NotificationsForm {
constructor() { constructor() {
this.toggleCheckbox = this.toggleCheckbox.bind(this); this.toggleCheckbox = this.toggleCheckbox.bind(this);
...@@ -27,15 +31,10 @@ export default class NotificationsForm { ...@@ -27,15 +31,10 @@ export default class NotificationsForm {
saveEvent($checkbox, $parent) { saveEvent($checkbox, $parent) {
const form = $parent.parents('form:first'); const form = $parent.parents('form:first');
return $.ajax({
url: form.attr('action'),
method: form.attr('method'),
dataType: 'json',
data: form.serialize(),
beforeSend: () => {
this.showCheckboxLoadingSpinner($parent); this.showCheckboxLoadingSpinner($parent);
},
}).done((data) => { axios[form.attr('method')](form.attr('action'), form.serialize())
.then(({ data }) => {
$checkbox.enable(); $checkbox.enable();
if (data.saved) { if (data.saved) {
$parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
...@@ -45,6 +44,7 @@ export default class NotificationsForm { ...@@ -45,6 +44,7 @@ export default class NotificationsForm {
.toggleClass('fa-spin fa-spinner fa-check is-done'); .toggleClass('fa-spin fa-spinner fa-check is-done');
}, 2000); }, 2000);
} }
}); })
.catch(() => flash(__('There was an error saving your notification settings.')));
} }
} }
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
import { removeParams } from './lib/utils/url_utility'; import { removeParams } from './lib/utils/url_utility';
const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_BOTTOM_PX = 400;
...@@ -22,13 +23,12 @@ export default { ...@@ -22,13 +23,12 @@ export default {
getOld() { getOld() {
this.loading.show(); this.loading.show();
$.ajax({ axios.get(this.url, {
type: 'GET', params: {
url: this.url, limit: this.limit,
data: `limit=${this.limit}&offset=${this.offset}`, offset: this.offset,
dataType: 'json', },
error: () => this.loading.hide(), }).then(({ data }) => {
success: (data) => {
this.append(data.count, this.prepareData(data.html)); this.append(data.count, this.prepareData(data.html));
this.callback(); this.callback();
...@@ -38,8 +38,7 @@ export default { ...@@ -38,8 +38,7 @@ export default {
} else { } else {
this.loading.hide(); this.loading.hide();
} }
}, }).catch(() => this.loading.hide());
});
}, },
append(count, html) { append(count, html) {
......
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
import flash from '../../../flash';
export default function UsagePing() { export default function UsagePing() {
const usageDataUrl = $('.usage-data').data('endpoint'); const el = document.querySelector('.usage-data');
$.ajax({ axios.get(el.dataset.endpoint, {
type: 'GET', responseType: 'text',
url: usageDataUrl, }).then(({ data }) => {
dataType: 'html', el.innerHTML = data;
success(html) { }).catch(() => flash(__('Error fetching usage ping data.')));
$('.usage-data').html(html);
},
});
} }
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils'; import { isMetaClick } from '~/lib/utils/common_utils';
import { __ } from '../../../../locale';
import flash from '../../../../flash';
import axios from '../../../../lib/utils/axios_utils';
export default class Todos { export default class Todos {
constructor() { constructor() {
...@@ -59,18 +62,12 @@ export default class Todos { ...@@ -59,18 +62,12 @@ export default class Todos {
const target = e.target; const target = e.target;
target.setAttribute('disabled', true); target.setAttribute('disabled', true);
target.classList.add('disabled'); target.classList.add('disabled');
$.ajax({
type: 'POST', axios[target.dataset.method](target.dataset.href)
url: target.dataset.href, .then(({ data }) => {
dataType: 'json',
data: {
'_method': target.dataset.method,
},
success: (data) => {
this.updateRowState(target); this.updateRowState(target);
return this.updateBadges(data); this.updateBadges(data);
}, }).catch(() => flash(__('Error updating todo status.')));
});
} }
updateRowState(target) { updateRowState(target) {
...@@ -98,19 +95,15 @@ export default class Todos { ...@@ -98,19 +95,15 @@ export default class Todos {
e.preventDefault(); e.preventDefault();
const target = e.currentTarget; const target = e.currentTarget;
const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
target.setAttribute('disabled', true); target.setAttribute('disabled', true);
target.classList.add('disabled'); target.classList.add('disabled');
$.ajax({
type: 'POST', axios[target.dataset.method](target.dataset.href, {
url: target.dataset.href, ids: this.todo_ids,
dataType: 'json', }).then(({ data }) => {
data: requestData,
success: (data) => {
this.updateAllState(target, data); this.updateAllState(target, data);
return this.updateBadges(data); this.updateBadges(data);
}, }).catch(() => flash(__('Error updating status for all todos.')));
});
} }
updateAllState(target, data) { updateAllState(target, data) {
......
import Vue from 'vue';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import TreeView from '../../../../tree'; import TreeView from '../../../../tree';
import ShortcutsNavigation from '../../../../shortcuts_navigation'; import ShortcutsNavigation from '../../../../shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer'; import BlobViewer from '../../../../blob/viewer';
...@@ -11,5 +13,25 @@ export default () => { ...@@ -11,5 +13,25 @@ export default () => {
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
$('#tree-slider').waitForImages(() => $('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath));
const commitPipelineStatusEl = document.getElementById('commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
if (statusLink != null) {
statusLink.remove();
// eslint-disable-next-line no-new
new Vue({
el: commitPipelineStatusEl,
components: {
commitPipelineStatus,
},
render(createElement) {
return createElement('commit-pipeline-status', {
props: {
endpoint: commitPipelineStatusEl.dataset.endpoint,
},
});
},
});
}
}; };
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) { const highlighter = function(element, text, matches) {
...@@ -72,19 +75,14 @@ export default class ProjectFindFile { ...@@ -72,19 +75,14 @@ export default class ProjectFindFile {
// files pathes load // files pathes load
load(url) { load(url) {
return $.ajax({ axios.get(url)
url: url, .then(({ data }) => {
method: "get", this.element.find('.loading').hide();
dataType: "json", this.filePaths = data;
success: (function(_this) { this.findFile();
return function(data) { this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus();
_this.element.find(".loading").hide(); })
_this.filePaths = data; .catch(() => flash(__('An error occurred while loading filenames')));
_this.findFile();
return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus();
};
})(this)
});
} }
// render result // render result
......
<script>
import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
directives: {
tooltip,
},
components: {
ciIcon,
loadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
/* This prop can be used to replace some of the `render_commit_status`
used across GitLab, this way we could use this vue component and add a
realtime status where it makes sense
realtime: {
type: Boolean,
required: false,
default: true,
}, */
},
data() {
return {
ciStatus: {},
isLoading: true,
};
},
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
},
},
mounted() {
this.service = new CommitPipelineService(this.endpoint);
this.initPolling();
},
methods: {
successCallback(res) {
const pipelines = res.data.pipelines;
if (pipelines.length > 0) {
// The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
}
this.isLoading = false;
},
errorCallback() {
this.ciStatus = {
text: 'not found',
icon: 'status_notfound',
group: 'notfound',
};
this.isLoading = false;
Flash(s__('Something went wrong on our end'));
},
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: response => this.successCallback(response),
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchPipelineCommitData();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
fetchPipelineCommitData() {
this.service.fetchData()
.then(this.successCallback)
.catch(this.errorCallback);
},
},
destroy() {
this.poll.stop();
},
};
</script>
<template>
<div>
<loading-icon
label="Loading pipeline status"
size="3"
v-if="isLoading"
/>
<a
v-else
:href="ciStatus.details_path"
>
<ci-icon
v-tooltip
:title="statusTitle"
:aria-label="statusTitle"
data-container="body"
:status="ciStatus"
/>
</a>
</div>
</template>
import axios from '~/lib/utils/axios_utils';
export default class CommitPipelineService {
constructor(endpoint) {
this.endpoint = endpoint;
}
fetchData() {
return axios.get(this.endpoint);
}
}
...@@ -7,7 +7,12 @@ ...@@ -7,7 +7,12 @@
// //
// <code class="js-render-math"></div> // <code class="js-render-math"></div>
// //
// Only load once
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
// Only load once
let katexLoaded = false; let katexLoaded = false;
// Loop over all math elements and render math // Loop over all math elements and render math
...@@ -33,19 +38,26 @@ export default function renderMath($els) { ...@@ -33,19 +38,26 @@ export default function renderMath($els) {
if (katexLoaded) { if (katexLoaded) {
renderWithKaTeX($els); renderWithKaTeX($els);
} else { } else {
$.get(gon.katex_css_url, () => { axios.get(gon.katex_css_url)
.then(() => {
const css = $('<link>', { const css = $('<link>', {
rel: 'stylesheet', rel: 'stylesheet',
type: 'text/css', type: 'text/css',
href: gon.katex_css_url, href: gon.katex_css_url,
}); });
css.appendTo('head'); css.appendTo('head');
})
// Load KaTeX js .then(() => axios.get(gon.katex_js_url, {
$.getScript(gon.katex_js_url, () => { responseType: 'text',
}))
.then(({ data }) => {
// Add katex js to our document
$.globalEval(data);
})
.then(() => {
katexLoaded = true; katexLoaded = true;
renderWithKaTeX($els); // Run KaTeX renderWithKaTeX($els); // Run KaTeX
}); })
}); .catch(() => flash(__('An error occurred while rendering KaTeX')));
} }
} }
import _ from 'underscore'; import _ from 'underscore';
import { scaleLinear, scaleThreshold } from 'd3-scale'; import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection'; import { select } from 'd3-selection';
import { getDayName, getDayDifference } from '../lib/utils/datetime_utility'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
const d3 = { select, scaleLinear, scaleThreshold }; const d3 = { select, scaleLinear, scaleThreshold };
...@@ -221,14 +224,16 @@ export default class ActivityCalendar { ...@@ -221,14 +224,16 @@ export default class ActivityCalendar {
this.currentSelectedDate.getDate(), this.currentSelectedDate.getDate(),
].join('-'); ].join('-');
$.ajax({ $('.user-calendar-activities').html(LOADING_HTML);
url: this.calendarActivitiesPath,
data: { date }, axios.get(this.calendarActivitiesPath, {
cache: false, params: {
dataType: 'html', date,
beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML), },
success: data => $('.user-calendar-activities').html(data), responseType: 'text',
}); })
.then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else { } else {
this.currentSelectedDate = ''; this.currentSelectedDate = '';
$('.user-calendar-activities').html(''); $('.user-calendar-activities').html('');
......
export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: { type: Object, required: true },
state: { type: String, required: false },
},
computed: {
hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe;
},
closesText() {
if (this.state === 'merged') {
return 'Closed';
}
if (this.state === 'closed') {
return 'Did not close';
}
return 'Closes';
},
},
template: `
<section
v-if="hasLinks"
class="mr-info-list mr-links">
<p v-if="relatedLinks.closing">
{{closesText}} <span v-html="relatedLinks.closing"></span>
</p>
<p v-if="relatedLinks.mentioned">
Mentions <span v-html="relatedLinks.mentioned"></span>
</p>
<p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span>
</p>
</section>
`,
};
<script>
import { s__ } from '~/locale';
export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: {
type: Object,
required: true,
default: () => ({}),
},
state: {
type: String,
required: false,
default: '',
},
},
computed: {
closesText() {
if (this.state === 'merged') {
return s__('mrWidget|Closed');
}
if (this.state === 'closed') {
return s__('mrWidget|Did not close');
}
return s__('mrWidget|Closes');
},
},
};
</script>
<template>
<section class="mr-info-list mr-links">
<p v-if="relatedLinks.closing">
{{ closesText }} <span v-html="relatedLinks.closing"></span>
</p>
<p v-if="relatedLinks.mentioned">
{{ s__("mrWidget|Mentions") }} <span v-html="relatedLinks.mentioned"></span>
</p>
<p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span>
</p>
</section>
</template>
...@@ -15,7 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue'; ...@@ -15,7 +15,7 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
......
...@@ -257,7 +257,8 @@ export default { ...@@ -257,7 +257,8 @@ export default {
<mr-widget-related-links <mr-widget-related-links
v-if="shouldRenderRelatedLinks" v-if="shouldRenderRelatedLinks"
:state="mr.state" :state="mr.state"
:related-links="mr.relatedLinks" /> :related-links="mr.relatedLinks"
/>
</div> </div>
<div <div
class="mr-widget-footer" class="mr-widget-footer"
......
...@@ -195,6 +195,18 @@ ...@@ -195,6 +195,18 @@
.commit-actions { .commit-actions {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
font-size: 0; font-size: 0;
div {
display: inline;
}
.fa-spinner {
font-size: 12px;
}
span {
font-size: 6px;
}
} }
.ci-status-link { .ci-status-link {
...@@ -219,6 +231,11 @@ ...@@ -219,6 +231,11 @@
font-size: 14px; font-size: 14px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.ci-status-icon {
position: relative;
top: 1px;
}
} }
.commit, .commit,
......
class Admin::GitalyServersController < Admin::ApplicationController
def index
@gitaly_servers = Gitaly::Server.all
end
end
...@@ -94,6 +94,7 @@ module IssuableCollections ...@@ -94,6 +94,7 @@ module IssuableCollections
@filter_params[:project_id] = @project.id @filter_params[:project_id] = @project.id
elsif @group elsif @group
@filter_params[:group_id] = @group.id @filter_params[:group_id] = @group.id
@filter_params[:include_subgroups] = true
else else
# TODO: this filter ignore issues/mr created in public or # TODO: this filter ignore issues/mr created in public or
# internal repos where you are not a member. Enable this filter # internal repos where you are not a member. Enable this filter
......
...@@ -118,10 +118,10 @@ class GroupsController < Groups::ApplicationController ...@@ -118,10 +118,10 @@ class GroupsController < Groups::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(group_params_ce) params.require(:group).permit(group_params_attributes)
end end
def group_params_ce def group_params_attributes
[ [
:avatar, :avatar,
:description, :description,
...@@ -150,7 +150,6 @@ class GroupsController < Groups::ApplicationController ...@@ -150,7 +150,6 @@ class GroupsController < Groups::ApplicationController
@projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user) @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user)
.execute .execute
.includes(:namespace) .includes(:namespace)
.page(params[:page])
@events = EventCollection @events = EventCollection
.new(@projects, offset: params[:offset].to_i, filter: event_filter) .new(@projects, offset: params[:offset].to_i, filter: event_filter)
......
...@@ -5,7 +5,7 @@ class HelpController < ApplicationController ...@@ -5,7 +5,7 @@ class HelpController < ApplicationController
# Taken from Jekyll # Taken from Jekyll
# https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13 # https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13
YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
def index def index
# Remove YAML frontmatter so that it doesn't look weird # Remove YAML frontmatter so that it doesn't look weird
......
...@@ -403,6 +403,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -403,6 +403,6 @@ class ProjectsController < Projects::ApplicationController
# to # to
# localhost/group/project # localhost/group/project
# #
redirect_to request.original_url.sub(/\.git\/?\Z/, '') if params[:format] == 'git' redirect_to request.original_url.sub(%r{\.git/?\Z}, '') if params[:format] == 'git'
end end
end end
...@@ -87,9 +87,18 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -87,9 +87,18 @@ class GroupProjectsFinder < ProjectsFinder
options.fetch(:only_shared, false) options.fetch(:only_shared, false)
end end
# subgroups are supported only for owned projects not for shared
def include_subgroups?
options.fetch(:include_subgroups, false)
end
def owned_projects def owned_projects
if include_subgroups?
Project.where(namespace_id: group.self_and_descendants.select(:id))
else
group.projects group.projects
end end
end
def shared_projects def shared_projects
group.shared_projects group.shared_projects
......
...@@ -43,6 +43,7 @@ class IssuableFinder ...@@ -43,6 +43,7 @@ class IssuableFinder
search search
sort sort
state state
include_subgroups
].freeze ].freeze
ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze ARRAY_PARAMS = { label_name: [], iids: [], assignee_username: [] }.freeze
...@@ -148,7 +149,8 @@ class IssuableFinder ...@@ -148,7 +149,8 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related? if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects current_user.authorized_projects
elsif group elsif group
GroupProjectsFinder.new(group: group, current_user: current_user).execute finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else else
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end end
......
module SidekiqHelper module SidekiqHelper
SIDEKIQ_PS_REGEXP = /\A SIDEKIQ_PS_REGEXP = %r{\A
(?<pid>\d+)\s+ (?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+ (?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+
(?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+ (?<state>[DIEKNRSTVWXZNLpsl\+<>/\d]+)\s+
(?<start>.+?)\s+ (?<start>.+?)\s+
(?<command>(?:ruby\d+:\s+)?sidekiq.*\].*) (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x \z}x
def parse_sidekiq_ps(line) def parse_sidekiq_ps(line)
match = line.strip.match(SIDEKIQ_PS_REGEXP) match = line.strip.match(SIDEKIQ_PS_REGEXP)
......
...@@ -11,7 +11,7 @@ module SubmoduleHelper ...@@ -11,7 +11,7 @@ module SubmoduleHelper
url = File.join(Gitlab.config.gitlab.url, @project.full_path) url = File.join(Gitlab.config.gitlab.url, @project.full_path)
end end
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ if url =~ %r{([^/:]+)/([^/]+(?:\.git)?)\Z}
namespace, project = $1, $2 namespace, project = $1, $2
gitlab_hosts = [Gitlab.config.gitlab.url, gitlab_hosts = [Gitlab.config.gitlab.url,
Gitlab.config.gitlab_shell.ssh_path_prefix] Gitlab.config.gitlab_shell.ssh_path_prefix]
...@@ -23,7 +23,7 @@ module SubmoduleHelper ...@@ -23,7 +23,7 @@ module SubmoduleHelper
end end
end end
namespace.sub!(/\A\//, '') namespace.sub!(%r{\A/}, '')
project.rstrip! project.rstrip!
project.sub!(/\.git\z/, '') project.sub!(/\.git\z/, '')
...@@ -47,11 +47,11 @@ module SubmoduleHelper ...@@ -47,11 +47,11 @@ module SubmoduleHelper
protected protected
def github_dot_com_url?(url) def github_dot_com_url?(url)
url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/ url =~ %r{github\.com[/:][^/]+/[^/]+\Z}
end end
def gitlab_dot_com_url?(url) def gitlab_dot_com_url?(url)
url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/ url =~ %r{gitlab\.com[/:][^/]+/[^/]+\Z}
end end
def self_url?(url, namespace, project) def self_url?(url, namespace, project)
...@@ -65,7 +65,7 @@ module SubmoduleHelper ...@@ -65,7 +65,7 @@ module SubmoduleHelper
def relative_self_url?(url) def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) ) # (./)?(../repo.git) || (./)?(../../project/repo.git) )
url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/ url =~ %r{\A((\./)?(\.\./))(?!(\.\.)|(.*/)).*(\.git)?\z} || url =~ %r{\A((\./)?(\.\./){2})(?!(\.\.))([^/]*)/(?!(\.\.)|(.*/)).*(\.git)?\z}
end end
def standard_links(host, namespace, project, commit) def standard_links(host, namespace, project, commit)
......
...@@ -110,7 +110,7 @@ module TreeHelper ...@@ -110,7 +110,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant # returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree) def flatten_tree(root_path, tree)
return tree.flat_path.sub(/\A#{root_path}\//, '') if tree.flat_path.present? return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present?
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir? if subtree.count == 1 && subtree.first.dir?
......
...@@ -292,7 +292,7 @@ module Ci ...@@ -292,7 +292,7 @@ module Ci
def repo_url def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@" auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
prefix + auth prefix + auth
end end
end end
......
...@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end end
def group_name def group_name
name.to_s.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
end end
def failed_but_allowed? def failed_but_allowed?
......
...@@ -9,13 +9,13 @@ require 'task_list/filter' ...@@ -9,13 +9,13 @@ require 'task_list/filter'
module Taskable module Taskable
COMPLETED = 'completed'.freeze COMPLETED = 'completed'.freeze
INCOMPLETE = 'incomplete'.freeze INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = / ITEM_PATTERN = %r{
^ ^
\s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list \s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list
\s+ # whitespace prefix has to be always presented for a list item \s+ # whitespace prefix has to be always presented for a list item
(\[\s\]|\[[xX]\]) # checkbox (\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text. (\s.+) # followed by whitespace and some text.
/x }x
def self.get_tasks(content) def self.get_tasks(content)
content.to_s.scan(ITEM_PATTERN).map do |checkbox, label| content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
......
...@@ -115,7 +115,7 @@ class Environment < ActiveRecord::Base ...@@ -115,7 +115,7 @@ class Environment < ActiveRecord::Base
def formatted_external_url def formatted_external_url
return nil unless external_url return nil unless external_url
external_url.gsub(/\A.*?:\/\//, '') external_url.gsub(%r{\A.*?://}, '')
end end
def stop_action? def stop_action?
......
...@@ -234,7 +234,7 @@ class Project < ActiveRecord::Base ...@@ -234,7 +234,7 @@ class Project < ActiveRecord::Base
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true validates :description, length: { maximum: 2000 }, allow_blank: true
validates :ci_config_path, validates :ci_config_path,
format: { without: /(\.{2}|\A\/)/, format: { without: %r{(\.{2}|\A/)},
message: 'cannot include leading slash or directory traversal.' }, message: 'cannot include leading slash or directory traversal.' },
length: { maximum: 255 }, length: { maximum: 255 },
allow_blank: true allow_blank: true
...@@ -1338,7 +1338,7 @@ class Project < ActiveRecord::Base ...@@ -1338,7 +1338,7 @@ class Project < ActiveRecord::Base
host = "#{subdomain}.#{Settings.pages.host}".downcase host = "#{subdomain}.#{Settings.pages.host}".downcase
# The host in URL always needs to be downcased # The host in URL always needs to be downcased
url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{subdomain}." "#{prefix}#{subdomain}."
end.downcase end.downcase
......
...@@ -84,7 +84,7 @@ http://app.asana.com/-/account_api' ...@@ -84,7 +84,7 @@ http://app.asana.com/-/account_api'
# - fix/ed/es/ing # - fix/ed/es/ing
# - close/s/d # - close/s/d
# - closing # - closing
issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\d+/(\d+)|#(\d+))}i
message.scan(issue_finder).each do |tuple| message.scan(issue_finder).each do |tuple|
# tuple will be # tuple will be
......
...@@ -10,9 +10,9 @@ class IssueTrackerService < Service ...@@ -10,9 +10,9 @@ class IssueTrackerService < Service
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
def self.reference_pattern(only_long: false) def self.reference_pattern(only_long: false)
if only_long if only_long
%r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)} /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/
else else
%r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)} /(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)/
end end
end end
......
...@@ -19,7 +19,7 @@ class JiraService < IssueTrackerService ...@@ -19,7 +19,7 @@ class JiraService < IssueTrackerService
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern(only_long: true) def self.reference_pattern(only_long: true)
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @reference_pattern ||= /(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)/
end end
def initialize_properties def initialize_properties
......
...@@ -852,7 +852,7 @@ class Repository ...@@ -852,7 +852,7 @@ class Repository
@root_ref_sha ||= commit(root_ref).sha @root_ref_sha ||= commit(root_ref).sha
end end
delegate :merged_branch_names, to: :raw_repository delegate :merged_branch_names, :can_be_merged?, to: :raw_repository
def merge_base(first_commit_id, second_commit_id) def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
...@@ -951,7 +951,7 @@ class Repository ...@@ -951,7 +951,7 @@ class Repository
end end
instance_variable_set(ivar, value) instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
# Even if the above `#exists?` check passes these errors might still # Even if the above `#exists?` check passes these errors might still
# occur (for example because of a non-existing HEAD). We want to # occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything # gracefully handle this and not cache anything
......
...@@ -842,13 +842,13 @@ class User < ActiveRecord::Base ...@@ -842,13 +842,13 @@ class User < ActiveRecord::Base
end end
def full_website_url def full_website_url
return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\// return "http://#{website_url}" if website_url !~ %r{\Ahttps?://}
website_url website_url
end end
def short_website_url def short_website_url
website_url.sub(/\Ahttps?:\/\//, '') website_url.sub(%r{\Ahttps?://}, '')
end end
def all_ssh_keys def all_ssh_keys
......
...@@ -138,19 +138,11 @@ ...@@ -138,19 +138,11 @@
GitLab API GitLab API
%span.pull-right %span.pull-right
= API::API::version = API::API::version
%p
Gitaly
%span.pull-right
= Gitlab::GitalyClient.expected_server_version
- if Gitlab.config.pages.enabled - if Gitlab.config.pages.enabled
%p %p
GitLab Pages GitLab Pages
%span.pull-right %span.pull-right
= Gitlab::Pages::VERSION = Gitlab::Pages::VERSION
%p
Git
%span.pull-right
= Gitlab::Git.version
%p %p
Ruby Ruby
%span.pull-right %span.pull-right
...@@ -163,6 +155,8 @@ ...@@ -163,6 +155,8 @@
= Gitlab::Database.adapter_name = Gitlab::Database.adapter_name
%span.pull-right %span.pull-right
= Gitlab::Database.version = Gitlab::Database.version
%p
= link_to "Gitaly Servers", admin_gitaly_servers_path
.row .row
.col-md-4 .col-md-4
.info-well .info-well
......
- breadcrumb_title _("Gitaly Servers")
%h3.page-title= _("Gitaly Servers")
%hr
.gitaly_servers
- if @gitaly_servers.any?
.table-holder
%table.table.responsive-table
%thead.hidden-sm.hidden-xs
%tr
%th= _("Storage")
%th= n_("Gitaly|Address")
%th= _("Server version")
%th= _("Git version")
%th= _("Up to date")
- @gitaly_servers.each do |server|
%tr
%td
= server.storage
%td
= server.address
%td
= server.server_version
%td
= server.git_binary_version
%td
= boolean_to_icon(server.up_to_date?)
- else
.empty-state
.text-center
%h4= _("No connection could be made to a Gitaly Server, please check your logs!")
...@@ -51,6 +51,7 @@ ...@@ -51,6 +51,7 @@
- if commit.status(ref) - if commit.status(ref)
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
#commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id) } }
= link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link" = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
......
---
title: Include subgroup issues and merge requests on the group page
merge_request:
author:
type: changed
---
title: Add realtime ci status for the repository -> files view
merge_request: 16523
author:
type: added
---
title: Enable RuboCop Style/RegexpLiteral
merge_request: 16752
author: Takuya Noguchi
type: other
---
title: Update minimum git version to 2.9.5
merge_request: 16683
author:
type: other
---
title: Fix not all events being shown in group dashboard
merge_request:
author:
type: fixed
---
title: Remove N+1 queries with /projects/:project_id/{access_requests,members} API
endpoints
merge_request:
author:
type: performance
---
title: Add Gitaly Servers admin dashboard
merge_request:
author:
type: added
...@@ -110,7 +110,7 @@ class Settings < Settingslogic ...@@ -110,7 +110,7 @@ class Settings < Settingslogic
url = "http://#{url}" unless url.start_with?('http') url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it # Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^\/]+)/?.*}, '\1') url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host URI.parse(url_without_path).host
end end
...@@ -469,10 +469,10 @@ end ...@@ -469,10 +469,10 @@ end
# repository_downloads_path value. # repository_downloads_path value.
# #
repositories_storages = Settings.repositories.storages.values repositories_storages = Settings.repositories.storages.values
repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '') repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(%r{/$}, '')
repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home']) repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(/\/$/, '')) } if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(%r{/$}, '')) }
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
end end
......
...@@ -54,7 +54,7 @@ elsif Gitlab::Database.mysql? ...@@ -54,7 +54,7 @@ elsif Gitlab::Database.mysql?
def initialize_type_map(mapping) def initialize_type_map(mapping)
super mapping super mapping
mapping.register_type(%r(timestamp)i) do |sql_type| mapping.register_type(/timestamp/i) do |sql_type|
precision = extract_precision(sql_type) precision = extract_precision(sql_type)
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlDateTimeWithTimeZone.new(precision: precision) ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlDateTimeWithTimeZone.new(precision: precision)
end end
......
namespace :admin do namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do resources :users, constraints: { id: %r{[a-zA-Z./0-9_\-]+} } do
resources :keys, only: [:show, :destroy] resources :keys, only: [:show, :destroy]
resources :identities, except: [:show] resources :identities, except: [:show]
resources :impersonation_tokens, only: [:index, :create] do resources :impersonation_tokens, only: [:index, :create] do
...@@ -24,6 +24,8 @@ namespace :admin do ...@@ -24,6 +24,8 @@ namespace :admin do
resource :impersonation, only: :destroy resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy] resources :abuse_reports, only: [:index, :destroy]
resources :gitaly_servers, only: [:index]
resources :spam_logs, only: [:index, :destroy] do resources :spam_logs, only: [:index, :destroy] do
member do member do
post :mark_as_ham post :mark_as_ham
......
...@@ -35,7 +35,7 @@ constraints(GroupUrlConstrainer.new) do ...@@ -35,7 +35,7 @@ constraints(GroupUrlConstrainer.new) do
post :toggle_subscription, on: :member post :toggle_subscription, on: :member
end end
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do resources :milestones, constraints: { id: %r{[^/]+} }, only: [:index, :show, :edit, :update, :new, :create] do
member do member do
get :merge_requests get :merge_requests
get :participants get :participants
...@@ -52,7 +52,7 @@ constraints(GroupUrlConstrainer.new) do ...@@ -52,7 +52,7 @@ constraints(GroupUrlConstrainer.new) do
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
collection do collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
end end
end end
end end
......
...@@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do
# #
# Templates # Templates
# #
get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: /[^\/]+/ } get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} }
resource :avatar, only: [:show, :destroy] resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
...@@ -55,7 +55,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -55,7 +55,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
resource :pages, only: [:show, :destroy] do resource :pages, only: [:show, :destroy] do
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ } resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} }
end end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
...@@ -65,7 +65,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -65,7 +65,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do resources :services, constraints: { id: %r{[^/]+} }, only: [:index, :edit, :update] do
member do member do
put :test put :test
end end
...@@ -346,7 +346,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -346,7 +346,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do resources :project_members, except: [:show, :new, :edit], constraints: { id: %r{[a-zA-Z./0-9_\-#%+]+} }, concerns: :access_requestable do
collection do collection do
delete :leave delete :leave
...@@ -379,7 +379,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -379,7 +379,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
collection do collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
end end
end end
......
...@@ -2,17 +2,17 @@ scope path: :uploads do ...@@ -2,17 +2,17 @@ scope path: :uploads do
# Note attachments and User/Group/Project avatars # Note attachments and User/Group/Project avatars
get "-/system/:model/:mounted_as/:id/:filename", get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show", to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
# show uploads for models, snippets (notes) available for now # show uploads for models, snippets (notes) available for now
get '-/system/:model/:id/:secret/:filename', get '-/system/:model/:id/:secret/:filename',
to: 'uploads#show', to: 'uploads#show',
constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ } constraints: { model: /personal_snippet/, id: /\d+/, filename: %r{[^/]+} }
# show temporary uploads # show temporary uploads
get '-/system/temp/:secret/:filename', get '-/system/temp/:secret/:filename',
to: 'uploads#show', to: 'uploads#show',
constraints: { filename: /[^\/]+/ } constraints: { filename: %r{[^/]+} }
# Appearance # Appearance
get "-/system/:model/:mounted_as/:id/:filename", get "-/system/:model/:mounted_as/:id/:filename",
...@@ -22,7 +22,7 @@ scope path: :uploads do ...@@ -22,7 +22,7 @@ scope path: :uploads do
# Project markdown uploads # Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename", get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show", to: "projects/uploads#show",
constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: %r{[^/]+} }
# create uploads for models, snippets (notes) available for now # create uploads for models, snippets (notes) available for now
post ':model', post ':model',
...@@ -34,4 +34,4 @@ end ...@@ -34,4 +34,4 @@ end
# Redirect old note attachments path to new uploads path. # Redirect old note attachments path to new uploads path.
get "files/note/:id/:filename", get "files/note/:id/:filename",
to: redirect("uploads/note/attachment/%{id}/%{filename}"), to: redirect("uploads/note/attachment/%{id}/%{filename}"),
constraints: { filename: /[^\/]+/ } constraints: { filename: %r{[^/]+} }
...@@ -5,17 +5,32 @@ Enterprise Edition (look for the [`CE Upstream` merge requests]). ...@@ -5,17 +5,32 @@ Enterprise Edition (look for the [`CE Upstream` merge requests]).
This merge is done automatically in a This merge is done automatically in a
[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679). [scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679).
If a merge is already in progress, the job [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
**If you are pinged in a `CE Upstream` merge request to resolve a conflict, ## What to do if you are pinged in a `CE Upstream` merge request to resolve a conflict?
please resolve the conflict as soon as possible or ask someone else to do it!**
1. Please resolve the conflict as soon as possible or ask someone else to do it
>**Note:** - It's ok to resolve more conflicts than the one that you are asked to resolve.
It's ok to resolve more conflicts than the one that you are asked to resolve. In In that case, it's a good habit to ask for a double-check on your resolution
that case, it's a good habit to ask for a double-check on your resolution by by someone who is familiar with the code you touched.
someone who is familiar with the code you touched. 1. Once you have resolved your conflicts, push to the branch (no force-push)
1. Assign the merge request to the next person that has to resolve a conflict
1. If all conflicts are resolved after your resolution is pushed, keep the merge
request assigned to you: **you are now responsible for the merge request to be
green**
1. If you need any help, you can ping the current [release managers], or ask in
the `#ce-to-ee` Slack channel
A few notes about the automatic CE->EE merge job:
- If a merge is already in progress, the job
[doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
- If there is nothing to merge (i.e. EE is up-to-date with CE), the job doesn't
create a new one
- The job posts messages to the `#ce-to-ee` Slack channel to inform what's the
current CE->EE merge status (e.g. "A new MR has been created", "A MR is still pending")
[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream [`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
[release managers]: https://about.gitlab.com/release-managers/
## Always merge EE merge requests before their CE counterparts ## Always merge EE merge requests before their CE counterparts
......
...@@ -80,7 +80,7 @@ Make sure you have the right version of Git installed ...@@ -80,7 +80,7 @@ Make sure you have the right version of Git installed
# Install Git # Install Git
sudo apt-get install -y git-core sudo apt-get install -y git-core
# Make sure Git is version 2.14.3 or higher # Make sure Git is version 2.9.5 or higher
git --version git --version
Is the system packaged Git too old? Remove it and compile from source. Is the system packaged Git too old? Remove it and compile from source.
...@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source. ...@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source # Download and compile from source
cd /tmp cd /tmp
curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.8.4.tar.gz curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.14.3.tar.gz
echo '626e319f8a24fc0866167ea5f6bf3e2f38f69d6cb2e59e150f13709ca3ebf301 git-2.8.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.8.4.tar.gz echo '023ffff6d3ba8a1bea779dfecc0ed0bb4ad68ab8601d14435dd8c08416f78d7f git-2.14.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.14.3.tar.gz
cd git-2.8.4/ cd git-2.14.3/
./configure ./configure
make prefix=/usr/local all make prefix=/usr/local all
......
...@@ -90,7 +90,8 @@ structure. ...@@ -90,7 +90,8 @@ structure.
To create a subgroup: To create a subgroup:
1. In the group's dashboard go to the **Subgroups** page and click **New subgroup**. 1. In the group's dashboard expand the **New project** split button, select
**New subgroup** and click the **New subgroup** button.
![Subgroups page](img/create_subgroup_button.png) ![Subgroups page](img/create_subgroup_button.png)
......
...@@ -193,7 +193,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -193,7 +193,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end end
step 'The link with text "/ID" should have url "tree/markdownID"' do step 'The link with text "/ID" should have url "tree/markdownID"' do
find('a', text: /^\/#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id' find('a', text: %r{^/#id$})['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
end end
step 'The link with text "README.mdID" '\ step 'The link with text "README.mdID" '\
...@@ -203,7 +203,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -203,7 +203,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'The link with text "d/README.mdID" should have '\ step 'The link with text "d/README.mdID" should have '\
'url "blob/markdown/d/README.mdID"' do 'url "blob/markdown/d/README.mdID"' do
find('a', text: /^d\/README.md#id$/)['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id' find('a', text: %r{^d/README.md#id$})['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id'
end end
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
...@@ -212,7 +212,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps ...@@ -212,7 +212,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
end end
step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do
find('a', text: /^\/#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' find('a', text: %r{^/#id$})['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
end end
# Wiki # Wiki
......
...@@ -24,7 +24,7 @@ module API ...@@ -24,7 +24,7 @@ module API
access_requesters = AccessRequestsFinder.new(source).execute!(current_user) access_requesters = AccessRequestsFinder.new(source).execute!(current_user)
access_requesters = paginate(access_requesters.includes(:user)) access_requesters = paginate(access_requesters.includes(:user))
present access_requesters.map(&:user), with: Entities::AccessRequester, source: source present access_requesters, with: Entities::AccessRequester
end end
desc "Requests access for the authenticated user to a #{source_type}." do desc "Requests access for the authenticated user to a #{source_type}." do
...@@ -36,7 +36,7 @@ module API ...@@ -36,7 +36,7 @@ module API
access_requester = source.request_access(current_user) access_requester = source.request_access(current_user)
if access_requester.persisted? if access_requester.persisted?
present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester present access_requester, with: Entities::AccessRequester
else else
render_validation_error!(access_requester) render_validation_error!(access_requester)
end end
...@@ -56,7 +56,7 @@ module API ...@@ -56,7 +56,7 @@ module API
member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute
status :created status :created
present member.user, with: Entities::Member, member: member present member, with: Entities::Member
end end
desc 'Denies an access request for the given user.' do desc 'Denies an access request for the given user.' do
......
...@@ -205,22 +205,15 @@ module API ...@@ -205,22 +205,15 @@ module API
expose :build_artifacts_size, as: :job_artifacts_size expose :build_artifacts_size, as: :job_artifacts_size
end end
class Member < UserBasic class Member < Grape::Entity
expose :access_level do |user, options| expose :user, merge: true, using: UserBasic
member = options[:member] || options[:source].members.find_by(user_id: user.id) expose :access_level
member.access_level expose :expires_at
end
expose :expires_at do |user, options|
member = options[:member] || options[:source].members.find_by(user_id: user.id)
member.expires_at
end
end end
class AccessRequester < UserBasic class AccessRequester < Grape::Entity
expose :requested_at do |user, options| expose :user, merge: true, using: UserBasic
access_requester = options[:access_requester] || options[:source].requesters.find_by(user_id: user.id) expose :requested_at
access_requester.requested_at
end
end end
class Group < Grape::Entity class Group < Grape::Entity
......
...@@ -21,10 +21,11 @@ module API ...@@ -21,10 +21,11 @@ module API
get ":id/members" do get ":id/members" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
users = source.users members = source.members.where.not(user_id: nil).includes(:user)
users = users.merge(User.search(params[:query])) if params[:query].present? members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = paginate(members)
present paginate(users), with: Entities::Member, source: source present members, with: Entities::Member
end end
desc 'Gets a member of a group or project.' do desc 'Gets a member of a group or project.' do
...@@ -39,7 +40,7 @@ module API ...@@ -39,7 +40,7 @@ module API
members = source.members members = source.members
member = members.find_by!(user_id: params[:user_id]) member = members.find_by!(user_id: params[:user_id])
present member.user, with: Entities::Member, member: member present member, with: Entities::Member
end end
desc 'Adds a member to a group or project.' do desc 'Adds a member to a group or project.' do
...@@ -62,7 +63,7 @@ module API ...@@ -62,7 +63,7 @@ module API
if !member if !member
not_allowed! # This currently can only be reached in EE not_allowed! # This currently can only be reached in EE
elsif member.persisted? && member.valid? elsif member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member present member, with: Entities::Member
else else
render_validation_error!(member) render_validation_error!(member)
end end
...@@ -83,7 +84,7 @@ module API ...@@ -83,7 +84,7 @@ module API
member = source.members.find_by!(user_id: params.delete(:user_id)) member = source.members.find_by!(user_id: params.delete(:user_id))
if member.update_attributes(declared_params(include_missing: false)) if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: member present member, with: Entities::Member
else else
render_validation_error!(member) render_validation_error!(member)
end end
......
...@@ -17,15 +17,15 @@ module API ...@@ -17,15 +17,15 @@ module API
} }
}.freeze }.freeze
PROJECT_TEMPLATE_REGEX = PROJECT_TEMPLATE_REGEX =
/[\<\{\[] %r{[\<\{\[]
(project|description| (project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
[\>\}\]]/xi.freeze [\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX = FULLNAME_TEMPLATE_REGEX =
/[\<\{\[] %r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner)) (fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]/xi.freeze [\>\}\]]}xi.freeze
helpers do helpers do
def parsed_license_template def parsed_license_template
......
...@@ -22,10 +22,11 @@ module API ...@@ -22,10 +22,11 @@ module API
get ":id/members" do get ":id/members" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
users = source.users members = source.members.where.not(user_id: nil).includes(:user)
users = users.merge(User.search(params[:query])) if params[:query].present? members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = paginate(members)
present paginate(users), with: ::API::Entities::Member, source: source present members, with: ::API::Entities::Member
end end
desc 'Gets a member of a group or project.' do desc 'Gets a member of a group or project.' do
...@@ -40,7 +41,7 @@ module API ...@@ -40,7 +41,7 @@ module API
members = source.members members = source.members
member = members.find_by!(user_id: params[:user_id]) member = members.find_by!(user_id: params[:user_id])
present member.user, with: ::API::Entities::Member, member: member present member, with: ::API::Entities::Member
end end
desc 'Adds a member to a group or project.' do desc 'Adds a member to a group or project.' do
...@@ -69,7 +70,7 @@ module API ...@@ -69,7 +70,7 @@ module API
end end
if member.persisted? && member.valid? if member.persisted? && member.valid?
present member.user, with: ::API::Entities::Member, member: member present member, with: ::API::Entities::Member
else else
# This is to ensure back-compatibility but 400 behavior should be used # This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0! # for all validation errors in 9.0!
...@@ -93,7 +94,7 @@ module API ...@@ -93,7 +94,7 @@ module API
member = source.members.find_by!(user_id: params.delete(:user_id)) member = source.members.find_by!(user_id: params.delete(:user_id))
if member.update_attributes(declared_params(include_missing: false)) if member.update_attributes(declared_params(include_missing: false))
present member.user, with: ::API::Entities::Member, member: member present member, with: ::API::Entities::Member
else else
# This is to ensure back-compatibility but 400 behavior should be used # This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0! # for all validation errors in 9.0!
...@@ -125,7 +126,7 @@ module API ...@@ -125,7 +126,7 @@ module API
else else
::Members::DestroyService.new(source, current_user, declared_params).execute ::Members::DestroyService.new(source, current_user, declared_params).execute
present member.user, with: ::API::Entities::Member, member: member present member, with: ::API::Entities::Member
end end
end end
end end
......
...@@ -173,7 +173,7 @@ module API ...@@ -173,7 +173,7 @@ module API
use :sort_params use :sort_params
use :pagination use :pagination
end end
get "/search/:query", requirements: { query: /[^\/]+/ } do get "/search/:query", requirements: { query: %r{[^/]+} } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page], false) projects = search_service.objects('projects', params[:page], false)
projects = projects.reorder(params[:order_by] => params[:sort]) projects = projects.reorder(params[:order_by] => params[:sort])
......
...@@ -16,15 +16,15 @@ module API ...@@ -16,15 +16,15 @@ module API
} }
}.freeze }.freeze
PROJECT_TEMPLATE_REGEX = PROJECT_TEMPLATE_REGEX =
/[\<\{\[] %r{[\<\{\[]
(project|description| (project|description|
one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
[\>\}\]]/xi.freeze [\>\}\]]}xi.freeze
YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
FULLNAME_TEMPLATE_REGEX = FULLNAME_TEMPLATE_REGEX =
/[\<\{\[] %r{[\<\{\[]
(fullname|name\sof\s(author|copyright\sowner)) (fullname|name\sof\s(author|copyright\sowner))
[\>\}\]]/xi.freeze [\>\}\]]}xi.freeze
DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze DEPRECATION_MESSAGE = ' This endpoint is deprecated and has been removed in V4.'.freeze
helpers do helpers do
......
...@@ -54,9 +54,9 @@ module Banzai ...@@ -54,9 +54,9 @@ module Banzai
# Build a regexp that matches all valid :emoji: names. # Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern def self.emoji_pattern
@emoji_pattern ||= @emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^) %r{(?<=[^[:alnum:]:]|\n|^)
:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}): :(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):
(?=[^[:alnum:]:]|$)/x (?=[^[:alnum:]:]|$)}x
end end
# Build a regexp that matches all valid unicode emojis names. # Build a regexp that matches all valid unicode emojis names.
......
...@@ -51,10 +51,10 @@ module Banzai ...@@ -51,10 +51,10 @@ module Banzai
# See https://github.com/gollum/gollum/wiki # See https://github.com/gollum/gollum/wiki
# #
# Rubular: http://rubular.com/r/7dQnE5CUCH # Rubular: http://rubular.com/r/7dQnE5CUCH
TAGS_PATTERN = %r{\[\[(.+?)\]\]}.freeze TAGS_PATTERN = /\[\[(.+?)\]\]/.freeze
# Pattern to match allowed image extensions # Pattern to match allowed image extensions
ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i.freeze ALLOWED_IMAGE_EXTENSIONS = /.+(jpg|png|gif|svg|bmp)\z/i.freeze
def call def call
search_text_nodes(doc).each do |node| search_text_nodes(doc).each do |node|
......
...@@ -11,7 +11,7 @@ module ContainerRegistry ...@@ -11,7 +11,7 @@ module ContainerRegistry
private private
def default_path def default_path
@uri.sub(/^https?:\/\//, '') @uri.sub(%r{^https?://}, '')
end end
end end
end end
...@@ -56,7 +56,7 @@ module ExtractsPath ...@@ -56,7 +56,7 @@ module ExtractsPath
if valid_refs.length == 0 if valid_refs.length == 0
# No exact ref match, so just try our best # No exact ref match, so just try our best
pair = id.match(/([^\/]+)(.*)/).captures pair = id.match(%r{([^/]+)(.*)}).captures
else else
# There is a distinct possibility that multiple refs prefix the ID. # There is a distinct possibility that multiple refs prefix the ID.
# Use the longest match to maximize the chance that we have the # Use the longest match to maximize the chance that we have the
...@@ -68,7 +68,7 @@ module ExtractsPath ...@@ -68,7 +68,7 @@ module ExtractsPath
end end
# Remove ending slashes from path # Remove ending slashes from path
pair[1].gsub!(/^\/|\/$/, '') pair[1].gsub!(%r{^/|/$}, '')
pair pair
end end
......
module Gitaly
class Server
def self.all
Gitlab.config.repositories.storages.keys.map { |s| Gitaly::Server.new(s) }
end
attr_reader :storage
def initialize(storage)
@storage = storage
end
def server_version
info.server_version
end
def git_binary_version
info.git_version
end
def up_to_date?
server_version == Gitlab::GitalyClient.expected_server_version
end
def address
Gitlab::GitalyClient.address(@storage)
rescue RuntimeError => e
"Error getting the address: #{e.message}"
end
private
def info
@info ||=
begin
Gitlab::GitalyClient::ServerService.new(@storage).info
rescue GRPC::Unavailable, GRPC::GRPC::DeadlineExceeded
# This will show the server as being out of date
Gitaly::ServerInfoResponse.new(git_version: '', server_version: '')
end
end
end
end
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
# Ends with /:random_hex/:filename # Ends with /:random_hex/:filename
FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z} FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
FULL_PATH_CAPTURE = %r{\A(.+)#{FILE_UPLOADER_PATH}} FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
# These regex patterns are tested against a relative path, relative to # These regex patterns are tested against a relative path, relative to
# the upload directory. # the upload directory.
......
...@@ -97,7 +97,7 @@ module Gitlab ...@@ -97,7 +97,7 @@ module Gitlab
end end
def total_size def total_size
descendant_pattern = %r{^#{Regexp.escape(@path.to_s)}} descendant_pattern = /^#{Regexp.escape(@path.to_s)}/
entries.sum do |path, entry| entries.sum do |path, entry|
(entry[:size] if path =~ descendant_pattern).to_i (entry[:size] if path =~ descendant_pattern).to_i
end end
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def package_url(name) def package_url(name)
"https://packagist.org/packages/#{name}" if name =~ %r{\A#{REPO_REGEX}\z} "https://packagist.org/packages/#{name}" if name =~ /\A#{REPO_REGEX}\z/
end end
end end
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url)) link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url))
# Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo
link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself) link_regex(/(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/, &:itself)
# Link `source "https://rubygems.org"` to https://rubygems.org # Link `source "https://rubygems.org"` to https://rubygems.org
link_method_call('source', URL_REGEX, &:itself) link_method_call('source', URL_REGEX, &:itself)
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
def link_dependencies def link_dependencies
link_method_call('homepage', URL_REGEX, &:itself) link_method_call('homepage', URL_REGEX, &:itself)
link_regex(%r{(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]}, &:itself) link_regex(/(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/, &:itself)
link_method_call('license', &method(:license_url)) link_method_call('license', &method(:license_url))
link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url)) link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url))
......
...@@ -43,7 +43,7 @@ module Gitlab ...@@ -43,7 +43,7 @@ module Gitlab
return "" unless decoded return "" unless decoded
# Certain trigger phrases that means we didn't parse correctly # Certain trigger phrases that means we didn't parse correctly
if decoded =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/ if decoded =~ %r{(Content\-Type\:|multipart/alternative|text/plain)}
return "" return ""
end end
......
...@@ -6,14 +6,14 @@ module Gitlab ...@@ -6,14 +6,14 @@ module Gitlab
module FileDetector module FileDetector
PATTERNS = { PATTERNS = {
# Project files # Project files
readme: /\Areadme[^\/]*\z/i, readme: %r{\Areadme[^/]*\z}i,
changelog: /\A(changelog|history|changes|news)[^\/]*\z/i, changelog: %r{\A(changelog|history|changes|news)[^/]*\z}i,
license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i, license: %r{\A(licen[sc]e|copying)(\.[^/]+)?\z}i,
contributing: /\Acontributing[^\/]*\z/i, contributing: %r{\Acontributing[^/]*\z}i,
version: 'version', version: 'version',
avatar: /\Alogo\.(png|jpg|gif)\z/, avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: /\A\.gitlab\/issue_templates\/[^\/]+\.md\z/, issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: /\A\.gitlab\/merge_request_templates\/[^\/]+\.md\z/, merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
# Configuration files # Configuration files
gitignore: '.gitignore', gitignore: '.gitignore',
...@@ -22,17 +22,17 @@ module Gitlab ...@@ -22,17 +22,17 @@ module Gitlab
route_map: '.gitlab/route-map.yml', route_map: '.gitlab/route-map.yml',
# Dependency files # Dependency files
cartfile: /\ACartfile[^\/]*\z/, cartfile: %r{\ACartfile[^/]*\z},
composer_json: 'composer.json', composer_json: 'composer.json',
gemfile: /\A(Gemfile|gems\.rb)\z/, gemfile: /\A(Gemfile|gems\.rb)\z/,
gemfile_lock: 'Gemfile.lock', gemfile_lock: 'Gemfile.lock',
gemspec: /\A[^\/]*\.gemspec\z/, gemspec: %r{\A[^/]*\.gemspec\z},
godeps_json: 'Godeps.json', godeps_json: 'Godeps.json',
package_json: 'package.json', package_json: 'package.json',
podfile: 'Podfile', podfile: 'Podfile',
podspec_json: /\A[^\/]*\.podspec\.json\z/, podspec_json: %r{\A[^/]*\.podspec\.json\z},
podspec: /\A[^\/]*\.podspec\z/, podspec: %r{\A[^/]*\.podspec\z},
requirements_txt: /\A[^\/]*requirements\.txt\z/, requirements_txt: %r{\A[^/]*requirements\.txt\z},
yarn_lock: 'yarn.lock' yarn_lock: 'yarn.lock'
}.freeze }.freeze
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
def ref_name(ref) def ref_name(ref)
encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') encode!(ref).sub(%r{\Arefs/(tags|heads|remotes)/}, '')
end end
def branch_name(ref) def branch_name(ref)
......
...@@ -107,7 +107,7 @@ module Gitlab ...@@ -107,7 +107,7 @@ module Gitlab
def find_entry_by_path(repository, root_id, path) def find_entry_by_path(repository, root_id, path)
root_tree = repository.lookup(root_id) root_tree = repository.lookup(root_id)
# Strip leading slashes # Strip leading slashes
path[/^\/*/] = '' path[%r{^/*}] = ''
path_arr = path.split('/') path_arr = path.split('/')
entry = root_tree.find do |entry| entry = root_tree.find do |entry|
...@@ -140,7 +140,7 @@ module Gitlab ...@@ -140,7 +140,7 @@ module Gitlab
def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE) def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
return unless path return unless path
path = path.sub(/\A\/*/, '') path = path.sub(%r{\A/*}, '')
path = '/' if path.empty? path = '/' if path.empty?
name = File.basename(path) name = File.basename(path)
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
class << self class << self
def normalize_path(filename) def normalize_path(filename)
# Strip all leading slashes so that //foo -> foo # Strip all leading slashes so that //foo -> foo
filename[/^\/*/] = '' filename[%r{^/*}] = ''
# Expand relative paths (e.g. foo/../bar) # Expand relative paths (e.g. foo/../bar)
filename = Pathname.new(filename) filename = Pathname.new(filename)
......
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
# Ex. # Ex.
# Ref.extract_branch_name('refs/heads/master') #=> 'master' # Ref.extract_branch_name('refs/heads/master') #=> 'master'
def self.extract_branch_name(str) def self.extract_branch_name(str)
str.gsub(/\Arefs\/heads\//, '') str.gsub(%r{\Arefs/heads/}, '')
end end
# Gitaly: this method will probably be migrated indirectly via its call sites. # Gitaly: this method will probably be migrated indirectly via its call sites.
......
...@@ -462,7 +462,6 @@ module Gitlab ...@@ -462,7 +462,6 @@ module Gitlab
path: nil, path: nil,
follow: false, follow: false,
skip_merges: false, skip_merges: false,
disable_walk: false,
after: nil, after: nil,
before: nil before: nil
} }
...@@ -494,11 +493,7 @@ module Gitlab ...@@ -494,11 +493,7 @@ module Gitlab
return [] return []
end end
if log_using_shell?(options)
log_by_shell(sha, options) log_by_shell(sha, options)
else
log_by_walk(sha, options)
end
end end
def count_commits(options) def count_commits(options)
...@@ -1386,8 +1381,18 @@ module Gitlab ...@@ -1386,8 +1381,18 @@ module Gitlab
run_git(args).first.scrub.split(/^--$/) run_git(args).first.scrub.split(/^--$/)
end end
def can_be_merged?(source_sha, target_branch)
gitaly_migrate(:can_be_merged) do |is_enabled|
if is_enabled
gitaly_can_be_merged?(source_sha, find_branch(target_branch, true).target)
else
rugged_can_be_merged?(source_sha, target_branch)
end
end
end
def search_files_by_name(query, ref) def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(/^\/*/, "")) safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
return [] if empty? || safe_query.blank? return [] if empty? || safe_query.blank?
...@@ -1635,24 +1640,6 @@ module Gitlab ...@@ -1635,24 +1640,6 @@ module Gitlab
end end
end end
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
options[:skip_merges] ||
options[:after] ||
options[:before]
end
def log_by_walk(sha, options)
walk_options = {
show: sha,
sort: Rugged::SORT_NONE,
limit: options[:limit],
offset: options[:offset]
}
Rugged::Walker.walk(rugged, walk_options).to_a
end
# Gitaly note: JV: although #log_by_shell shells out to Git I think the # Gitaly note: JV: although #log_by_shell shells out to Git I think the
# complexity is such that we should migrate it as Ruby before trying to # complexity is such that we should migrate it as Ruby before trying to
# do it in Go. # do it in Go.
...@@ -2015,7 +2002,7 @@ module Gitlab ...@@ -2015,7 +2002,7 @@ module Gitlab
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
rescue Rugged::ReferenceError => e rescue Rugged::ReferenceError => e
raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/ raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ %r{'refs/heads/#{ref}'}
raise InvalidRef.new("Invalid reference #{start_point}") raise InvalidRef.new("Invalid reference #{start_point}")
end end
...@@ -2280,6 +2267,14 @@ module Gitlab ...@@ -2280,6 +2267,14 @@ module Gitlab
run_git(['fetch', remote_name], env: env).last.zero? run_git(['fetch', remote_name], env: env).last.zero?
end end
def gitaly_can_be_merged?(their_commit, our_commit)
!gitaly_conflicts_client(our_commit, their_commit).conflicts?
end
def rugged_can_be_merged?(their_commit, our_commit)
!rugged.merge_commits(our_commit, their_commit).conflicts?
end
def gitlab_projects_error def gitlab_projects_error
raise CommandError, @gitlab_projects.output raise CommandError, @gitlab_projects.output
end end
......
...@@ -43,7 +43,7 @@ module Gitlab ...@@ -43,7 +43,7 @@ module Gitlab
branches = [] branches = []
rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref| rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '') name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '')
begin begin
target_commit = Gitlab::Git::Commit.find(self, ref.target) target_commit = Gitlab::Git::Commit.find(self, ref.target)
......
...@@ -83,6 +83,8 @@ module Gitlab ...@@ -83,6 +83,8 @@ module Gitlab
commit_id: sha commit_id: sha
) )
end end
rescue Rugged::ReferenceError
[]
end end
end end
......
...@@ -257,7 +257,7 @@ module Gitlab ...@@ -257,7 +257,7 @@ module Gitlab
offset: options[:offset], offset: options[:offset],
follow: options[:follow], follow: options[:follow],
skip_merges: options[:skip_merges], skip_merges: options[:skip_merges],
disable_walk: options[:disable_walk] disable_walk: true # This option is deprecated. The 'walk' implementation is being removed.
) )
request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before] request.before = GitalyClient.timestamp(options[:before]) if options[:before]
......
module Gitlab
module GitalyClient
# Meant for extraction of server data, and later maybe to perform misc task
#
# Not meant for connection logic, look in Gitlab::GitalyClient
class ServerService
def initialize(storage)
@storage = storage
end
def info
GitalyClient.call(@storage, :server_service, :server_info, Gitaly::ServerInfoRequest.new)
end
end
end
end
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
:diff_hunk, :author, :note, :created_at, :updated_at, :diff_hunk, :author, :note, :created_at, :updated_at,
:github_id :github_id
NOTEABLE_ID_REGEX = /\/pull\/(?<iid>\d+)/i NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i
# Builds a diff note from a GitHub API response. # Builds a diff note from a GitHub API response.
# #
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
expose_attribute :noteable_id, :noteable_type, :author, :note, expose_attribute :noteable_id, :noteable_type, :author, :note,
:created_at, :updated_at, :github_id :created_at, :updated_at, :github_id
NOTEABLE_TYPE_REGEX = /\/(?<type>(pull|issues))\/(?<iid>\d+)/i NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i
# Builds a note from a GitHub API response. # Builds a note from a GitHub API response.
# #
......
...@@ -59,7 +59,7 @@ module Gitlab ...@@ -59,7 +59,7 @@ module Gitlab
end end
def extracted_files def extracted_files
Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ /.*\/\.{1,2}$/ } Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ %r{.*/\.{1,2}$} }
end end
end end
end end
......
...@@ -34,7 +34,7 @@ module Gitlab ...@@ -34,7 +34,7 @@ module Gitlab
end end
def relative_path(path) def relative_path(path)
path.gsub(/^#{Rails.root.to_s}\/?/, '') path.gsub(%r{^#{Rails.root.to_s}/?}, '')
end end
def values_for(event) def values_for(event)
......
...@@ -56,12 +56,12 @@ module Gitlab ...@@ -56,12 +56,12 @@ module Gitlab
end end
def strip_url(url) def strip_url(url)
url.gsub(/\Ahttps?:\/\//, '') url.gsub(%r{\Ahttps?://}, '')
end end
def project_path(request) def project_path(request)
path_info = request.env["PATH_INFO"] path_info = request.env["PATH_INFO"]
path_info.sub!(/^\//, '') path_info.sub!(%r{^/}, '')
project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX) project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
return unless project_path_match return unless project_path_match
......
module Gitlab module Gitlab
module Middleware module Middleware
class Static < ActionDispatch::Static class Static < ActionDispatch::Static
UPLOADS_REGEX = /\A\/uploads(\/|\z)/.freeze UPLOADS_REGEX = %r{\A/uploads(/|\z)}.freeze
def call(env) def call(env)
return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
# if date doesn't present return time with current date # if date doesn't present return time with current date
# in other cases return nil # in other cases return nil
class SpendTimeAndDateSeparator class SpendTimeAndDateSeparator
DATE_REGEX = /(\d{2,4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})/ DATE_REGEX = %r{(\d{2,4}[/\-.]\d{1,2}[/\-.]\d{1,2})}
def initialize(spend_command_arg) def initialize(spend_command_arg)
@spend_arg = spend_command_arg @spend_arg = spend_command_arg
......
...@@ -30,7 +30,7 @@ module Gitlab ...@@ -30,7 +30,7 @@ module Gitlab
raise NotFoundError.new("No known storage path matches #{repo_path.inspect}") raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
end end
result.sub(/\A\/*/, '') result.sub(%r{\A/*}, '')
end end
def self.find_project(project_path) def self.find_project(project_path)
......
...@@ -31,7 +31,7 @@ module Gitlab ...@@ -31,7 +31,7 @@ module Gitlab
storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s } storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
end end
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test? config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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