Commit 8aaa2a00 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-to-ee-2017-06-30' into 'master'

CE upstream: Friday

Closes #225, gitaly#201, and gitlab-ce#33172

See merge request !2313
parents 27a22281 dd69efa8
...@@ -68,7 +68,7 @@ stages: ...@@ -68,7 +68,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only: only:
- /mysql/ - /mysql/
- /-stable$/ - /-stable/
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
...@@ -460,9 +460,10 @@ codeclimate: ...@@ -460,9 +460,10 @@ codeclimate:
services: services:
- docker:dind - docker:dind
script: script:
- docker pull stedolan/jq
- docker pull codeclimate/codeclimate - docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
......
...@@ -4,13 +4,13 @@ entry. ...@@ -4,13 +4,13 @@ entry.
## 9.3.3 (2017-06-30) ## 9.3.3 (2017-06-30)
- Fix shared runners minutes query to update only projects with used allowance.
- Fix head pipeline stored in merge request for external pipelines. !12478 - Fix head pipeline stored in merge request for external pipelines. !12478
- Bring back branches badge to main project page. !12548 - Bring back branches badge to main project page. !12548
- Fix diff of requirements.txt file by not matching newlines as part of package names. - Fix diff of requirements.txt file by not matching newlines as part of package names.
- Perform housekeeping only when an import of a fresh project is completed.
- Fixed issue boards closed list not showing all closed issues. - Fixed issue boards closed list not showing all closed issues.
- Fixed multi-line markdown tooltip buttons in issue edit form. - Fixed multi-line markdown tooltip buttons in issue edit form.
- Fix shared runners minutes query to update only projects with used allowance.
- Perform housekeeping only when an import of a fresh project is completed.
## 9.3.2 (2017-06-27) ## 9.3.2 (2017-06-27)
......
...@@ -85,9 +85,8 @@ window.Build = (function () { ...@@ -85,9 +85,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) { if (!this.hasBeenScrolled) {
this.scrollToBottom(); this.scrollToBottom();
} }
}); })
.then(() => this.verifyTopPosition());
this.verifyTopPosition();
} }
Build.prototype.canScroll = function () { Build.prototype.canScroll = function () {
...@@ -176,7 +175,7 @@ window.Build = (function () { ...@@ -176,7 +175,7 @@ window.Build = (function () {
} }
if ($flashError.length) { if ($flashError.length) {
topPostion += $flashError.outerHeight(); topPostion += $flashError.outerHeight() + prependTopDefault;
} }
this.$buildTrace.css({ this.$buildTrace.css({
...@@ -196,6 +195,7 @@ window.Build = (function () { ...@@ -196,6 +195,7 @@ window.Build = (function () {
}) })
.done((log) => { .done((log) => {
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
} }
...@@ -220,7 +220,11 @@ window.Build = (function () { ...@@ -220,7 +220,11 @@ window.Build = (function () {
} }
if (!log.complete) { if (!log.complete) {
this.toggleScrollAnimation(true); if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
Build.timeout = setTimeout(() => { Build.timeout = setTimeout(() => {
//eslint-disable-next-line //eslint-disable-next-line
...@@ -229,7 +233,8 @@ window.Build = (function () { ...@@ -229,7 +233,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) { if (!this.hasBeenScrolled) {
this.scrollToBottom(); this.scrollToBottom();
} }
}); })
.then(() => this.verifyTopPosition());
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -8,8 +9,10 @@ let isBound = false; ...@@ -8,8 +9,10 @@ let isBound = false;
class Diff { class Diff {
constructor() { constructor() {
const $diffFile = $('.files .diff-file'); const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff(); $diffFile.singleFileDiff();
$diffFile.filesCommentButton();
FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file)); $diffFile.each((index, file) => new gl.ImageFile(file));
......
...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount; const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container') $(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0) .toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container') .nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0); .toggleClass('no-comment-btn', notesCount > 0);
}, },
toggleDiscussionsToggleState() { toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
/* global notes */ /* global notes */
let $commentButtonTemplate; /* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
window.FilesCommentButton = (function() { * bottleneck for pages with large diffs. For a comprehensive list of what
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
COMMENT_BUTTON_CLASS = '.add-diff-note';
const LINE_NUMBER_CLASS = 'diff-line-num';
LINE_HOLDER_CLASS = '.line_holder'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
LINE_NUMBER_CLASS = 'diff-line-num'; const EMPTY_CELL_CLASS = 'empty-cell';
const OLD_LINE_CLASS = 'old_line';
LINE_CONTENT_CLASS = 'line_content'; const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
const DIFF_CONTAINER_SELECTOR = '.files';
UNFOLDABLE_LINE_CLASS = 'js-unfold'; const DIFF_EXPANDED_CLASS = 'diff-expanded';
EMPTY_CELL_CLASS = 'empty-cell'; export default {
init($diffFile) {
OLD_LINE_CLASS = 'old_line'; /* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; * will be true in all cases */
TEXT_FILE_SELECTOR = '.text-file'; if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
function FilesCommentButton(filesContainerElement) { this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
this.render = this.render.bind(this);
this.hideButton = this.hideButton.bind(this);
this.isParallelView = notes.isParallelView();
filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
}
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
} }
textFileElement = this.getTextFileElement($currentTarget); if (typeof notes !== 'undefined' && !this.isParallelView) {
buttonParentElement.append(this.buildButton({ this.isParallelView = notes.isParallelView && notes.isParallelView();
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
FilesCommentButton.prototype.hideButton = function(e) {
var $currentTarget = $(e.currentTarget);
var buttonParentElement = this.getButtonParent($currentTarget);
buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
};
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
// DiffNote
'data-position': buttonAttributes.position
});
};
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return hoveredElement.closest(TEXT_FILE_SELECTOR);
};
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement;
}
if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
} }
};
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { if (this.userCanCreateNote) {
if (!this.isParallelView) { $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
if (hoveredElement.hasClass(OLD_LINE_CLASS)) { .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
return hoveredElement;
}
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
} else {
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
return hoveredElement;
}
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
} }
}; },
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { showButton(isParallelView, e) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { if (!this.validateButtonParent(buttonParentElement)) return;
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
};
return FilesCommentButton; buttonParentElement.classList.add('is-over');
})(); buttonParentElement.nextElementSibling.classList.add('is-over');
},
$.fn.filesCommentButton = function() { hideButton(isParallelView, e) {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
if (!(this && (this.parent().data('can-create-note') != null))) { buttonParentElement.classList.remove('is-over');
return; buttonParentElement.nextElementSibling.classList.remove('is-over');
} },
return this.each(function() {
if (!$.data(this, 'filesCommentButton')) { getButtonParent(hoveredElement, isParallelView) {
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); if (isParallelView) {
if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
return hoveredElement.previousElementSibling;
}
} else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
} }
}); return hoveredElement;
},
validateButtonParent(buttonParentElement) {
return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
},
}; };
...@@ -396,6 +396,13 @@ class GfmAutoComplete { ...@@ -396,6 +396,13 @@ class GfmAutoComplete {
this.cachedData = {}; this.cachedData = {};
} }
destroy() {
this.input.each((i, input) => {
const $input = $(input);
$input.atwho('destroy');
});
}
static isLoading(data) { static isLoading(data) {
let dataToInspect = data; let dataToInspect = data;
if (data && data.length > 0) { if (data && data.length > 0) {
......
...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) { ...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
GLForm.prototype.destroy = function() { GLForm.prototype.destroy = function() {
// Clean form listeners // Clean form listeners
this.clearEventListeners(); this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
return this.form.data('gl-form', null); return this.form.data('gl-form', null);
}; };
...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() { ...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form'); this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true, emojis: true,
members: this.enableGFM, members: this.enableGFM,
issues: this.enableGFM, issues: this.enableGFM,
......
...@@ -39,6 +39,17 @@ ...@@ -39,6 +39,17 @@
runnerId() { runnerId() {
return `#${this.job.runner.id}`; return `#${this.job.runner.id}`;
}, },
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
},
}, },
}; };
</script> </script>
...@@ -63,7 +74,7 @@ ...@@ -63,7 +74,7 @@
Retry Retry
</a> </a>
</div> </div>
<div class="block"> <div :class="{block : renderBlock }">
<p <p
class="build-detail-row js-job-mr" class="build-detail-row js-job-mr"
v-if="job.merge_request"> v-if="job.merge_request">
......
...@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.resetViewContainer(); this.resetViewContainer();
this.mountPipelinesView(); this.mountPipelinesView();
} else { } else {
this.expandView(); if (Breakpoints.get().getBreakpointSize() !== 'xs') {
this.expandView();
}
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView(); this.destroyPipelinesView();
} }
......
...@@ -829,6 +829,8 @@ export default class Notes { ...@@ -829,6 +829,8 @@ export default class Notes {
*/ */
setupDiscussionNoteForm(dataHolder, form) { setupDiscussionNoteForm(dataHolder, form) {
// setup note target // setup note target
const diffFileData = dataHolder.closest('.text-file');
var discussionID = dataHolder.data('discussionId'); var discussionID = dataHolder.data('discussionId');
if (discussionID) { if (discussionID) {
...@@ -839,9 +841,10 @@ export default class Notes { ...@@ -839,9 +841,10 @@ export default class Notes {
form.attr('data-line-code', dataHolder.data('lineCode')); form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType')); form.find('#line_type').val(dataHolder.data('lineType'));
form.find('#note_noteable_type').val(dataHolder.data('noteableType')); form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
form.find('#note_noteable_id').val(dataHolder.data('noteableId')); form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
form.find('#note_commit_id').val(dataHolder.data('commitId')); form.find('#note_commit_id').val(diffFileData.data('commitId'));
form.find('#note_type').val(dataHolder.data('noteType')); form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote // LegacyDiffNote
......
...@@ -10,6 +10,8 @@ import Cookies from 'js-cookie'; ...@@ -10,6 +10,8 @@ import Cookies from 'js-cookie';
this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab'); this.$navGitlab = $('.navbar-gitlab');
this.$layoutNav = $('.layout-nav');
this.$subScroll = $('.sub-nav-scroll');
this.$rightSidebar = $('.js-right-sidebar'); this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners(); this.removeListeners();
...@@ -27,14 +29,14 @@ import Cookies from 'js-cookie'; ...@@ -27,14 +29,14 @@ import Cookies from 'js-cookie';
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
const $document = $(document); const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); const slowerThrottledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight()); $(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => debouncedSetSidebarHeight()); $document.on('scroll', () => slowerThrottledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) { $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
...@@ -213,7 +215,7 @@ import Cookies from 'js-cookie'; ...@@ -213,7 +215,7 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.setSidebarHeight = function() { Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight(); const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0);
const diff = $navHeight - $(window).scrollTop(); const diff = $navHeight - $(window).scrollTop();
if (diff > 0) { if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff); this.$rightSidebar.outerHeight($(window).height() - diff);
......
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
(function() { (function() {
window.SingleFileDiff = (function() { window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
...@@ -78,6 +80,8 @@ ...@@ -78,6 +80,8 @@
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
FilesCommentButton.init($(_this.file));
if (cb) cb(); if (cb) cb();
}; };
})(this)); })(this));
......
...@@ -64,6 +64,12 @@ ...@@ -64,6 +64,12 @@
*/ */
return new gl.GLForm($(this.$refs['gl-form']), true); return new gl.GLForm($(this.$refs['gl-form']), true);
}, },
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
}; };
</script> </script>
......
...@@ -11,20 +11,19 @@ header.navbar-gitlab-new { ...@@ -11,20 +11,19 @@ header.navbar-gitlab-new {
padding-left: 0; padding-left: 0;
.title-container { .title-container {
align-items: stretch;
padding-top: 0; padding-top: 0;
overflow: visible; overflow: visible;
} }
.title { .title {
display: block; display: flex;
height: 100%;
padding-right: 0; padding-right: 0;
color: currentColor; color: currentColor;
> a { > a {
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%;
padding-top: 3px; padding-top: 3px;
padding-right: $gl-padding; padding-right: $gl-padding;
padding-left: $gl-padding; padding-left: $gl-padding;
......
...@@ -5,17 +5,46 @@ ...@@ -5,17 +5,46 @@
$new-sidebar-width: 220px; $new-sidebar-width: 220px;
.page-with-new-sidebar { .page-with-new-sidebar {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-left: $new-sidebar-width; padding-left: $new-sidebar-width;
} }
// Override position: absolute
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
height: 100%; height: 100%;
} }
} }
.context-header {
background-color: $gray-normal;
border-bottom: 1px solid $border-color;
font-weight: 600;
display: flex;
align-items: center;
padding: 10px 14px;
.avatar-container {
flex: 0 0 40px;
}
&:hover {
background-color: $border-color;
}
}
.settings-avatar {
background-color: $white-light;
i {
font-size: 20px;
width: 100%;
color: $gl-text-color-secondary;
text-align: center;
align-self: center;
}
}
.nav-sidebar { .nav-sidebar {
position: fixed; position: fixed;
z-index: 400; z-index: 400;
......
...@@ -147,10 +147,9 @@ ...@@ -147,10 +147,9 @@
top: 35px; top: 35px;
left: 10px; left: 10px;
bottom: 0; bottom: 0;
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px; padding: 10px 20px 20px 5px;
white-space: pre; white-space: pre-wrap;
overflow: auto;
} }
.environment-information { .environment-information {
...@@ -399,6 +398,7 @@ ...@@ -399,6 +398,7 @@
.build-light-text { .build-light-text {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
word-wrap: break-word;
} }
.build-gutter-toggle { .build-gutter-toggle {
......
...@@ -20,8 +20,6 @@ ...@@ -20,8 +20,6 @@
} }
.diff-content { .diff-content {
overflow: auto;
overflow-y: hidden;
background: $white-light; background: $white-light;
color: $gl-text-color; color: $gl-text-color;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
...@@ -476,6 +474,7 @@ ...@@ -476,6 +474,7 @@
height: 19px; height: 19px;
width: 19px; width: 19px;
margin-left: -15px; margin-left: -15px;
z-index: 100;
&:hover { &:hover {
.diff-comment-avatar, .diff-comment-avatar,
...@@ -491,7 +490,7 @@ ...@@ -491,7 +490,7 @@
transform: translateX((($i * $x-pos) - $x-pos)); transform: translateX((($i * $x-pos) - $x-pos));
&:hover { &:hover {
transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); transform: translateX((($i * $x-pos) - $x-pos));
} }
} }
} }
...@@ -542,6 +541,7 @@ ...@@ -542,6 +541,7 @@
height: 19px; height: 19px;
padding: 0; padding: 0;
transition: transform .1s ease-out; transition: transform .1s ease-out;
z-index: 100;
svg { svg {
position: absolute; position: absolute;
...@@ -555,10 +555,6 @@ ...@@ -555,10 +555,6 @@
fill: $white-light; fill: $white-light;
} }
&:hover {
transform: scale(1.2);
}
&:focus { &:focus {
outline: 0; outline: 0;
} }
......
...@@ -659,8 +659,8 @@ ...@@ -659,8 +659,8 @@
@media(max-width: $screen-md-max) { @media(max-width: $screen-md-max) {
.task-status, .task-status,
.issuable-due-date, .issuable-due-date,
.project-ref-path, .issuable-weight,
.issuable-weight { .project-ref-path {
display: none; display: none;
} }
} }
......
...@@ -628,8 +628,14 @@ ul.notes { ...@@ -628,8 +628,14 @@ ul.notes {
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
}
}
.add-diff-note { .add-diff-note {
display: none; opacity: 0;
margin-top: -2px; margin-top: -2px;
border-radius: 50%; border-radius: 50%;
background: $white-light; background: $white-light;
...@@ -642,13 +648,11 @@ ul.notes { ...@@ -642,13 +648,11 @@ ul.notes {
width: 23px; width: 23px;
height: 23px; height: 23px;
border: 1px solid $blue-500; border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover { &:hover {
background: $blue-500; background: $blue-500;
border-color: $blue-600; border-color: $blue-600;
color: $white-light; color: $white-light;
transform: scale(1.15);
} }
&:active { &:active {
......
.tree-holder { .tree-holder {
.nav-block { .nav-block {
margin: 10px 0; margin: 10px 0;
...@@ -15,6 +16,11 @@ ...@@ -15,6 +16,11 @@
.btn-group { .btn-group {
margin-left: 10px; margin-left: 10px;
} }
.control {
float: left;
margin-left: 10px;
}
} }
.tree-ref-holder { .tree-ref-holder {
......
class AbuseReportsController < ApplicationController class AbuseReportsController < ApplicationController
before_action :set_user, only: [:new]
def new def new
@abuse_report = AbuseReport.new @abuse_report = AbuseReport.new
@abuse_report.user_id = params[:user_id] @abuse_report.user_id = @user.id
@ref_url = params.fetch(:ref_url, '') @ref_url = params.fetch(:ref_url, '')
end end
...@@ -27,4 +29,14 @@ class AbuseReportsController < ApplicationController ...@@ -27,4 +29,14 @@ class AbuseReportsController < ApplicationController
user_id user_id
)) ))
end end
def set_user
@user = User.find_by(id: params[:user_id])
if @user.nil?
redirect_to root_path, alert: "Cannot create the abuse report. The user has been deleted."
elsif @user.blocked?
redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked."
end
end
end end
class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController
include DiffForPath include DiffForPath
include DiffHelper include DiffHelper
prepend ::EE::Projects::MergeRequests::CreationsController prepend ::EE::Projects::MergeRequests::CreationsController
skip_before_action :merge_request skip_before_action :merge_request
......
...@@ -4,6 +4,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -4,6 +4,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include RendersNotes include RendersNotes
include ToggleAwardEmoji include ToggleAwardEmoji
include IssuableCollections include IssuableCollections
prepend ::EE::Projects::MergeRequestsController prepend ::EE::Projects::MergeRequestsController
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
......
class Projects::PipelineSchedulesController < Projects::ApplicationController class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_read_pipeline_schedule! before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create, :edit, :take_ownership, :update] before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy] before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action :schedule, only: [:edit, :update, :destroy, :take_ownership] before_action :schedule, only: [:edit, :update, :destroy, :take_ownership]
......
...@@ -98,7 +98,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -98,7 +98,7 @@ class ProjectsController < Projects::ApplicationController
end end
if @project.pending_delete? if @project.pending_delete?
flash[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name }
end end
respond_to do |format| respond_to do |format|
......
...@@ -83,6 +83,8 @@ class TodosFinder ...@@ -83,6 +83,8 @@ class TodosFinder
if project? if project?
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
@project = nil if @project.pending_delete?
unless Ability.allowed?(current_user, :read_project, @project) unless Ability.allowed?(current_user, :read_project, @project)
@project = nil @project = nil
end end
......
...@@ -47,6 +47,18 @@ module NotesHelper ...@@ -47,6 +47,18 @@ module NotesHelper
data data
end end
def add_diff_note_button(line_code, position, line_type)
return if @diff_notes_disabled
button_tag '',
class: 'add-diff-note js-add-diff-note-button',
type: 'submit', name: 'button',
data: diff_view_line_data(line_code, position, line_type),
title: 'Add a comment to this line' do
icon('comment-o')
end
end
def link_to_reply_discussion(discussion, line_type = nil) def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user return unless current_user
......
...@@ -73,6 +73,7 @@ module SubmoduleHelper ...@@ -73,6 +73,7 @@ module SubmoduleHelper
end end
def relative_self_links(url, commit) def relative_self_links(url, commit)
url.rstrip!
# Map relative links to a namespace and project # Map relative links to a namespace and project
# For example: # For example:
# ../bar.git -> same namespace, repo bar # ../bar.git -> same namespace, repo bar
......
require 'declarative_policy' require_dependency 'declarative_policy'
class Ability class Ability
class << self class << self
......
...@@ -151,6 +151,7 @@ module Ci ...@@ -151,6 +151,7 @@ module Ci
where(id: max_id) where(id: max_id)
end end
end end
scope :internal, -> { where(source: internal_sources) }
def self.latest_status(ref = nil) def self.latest_status(ref = nil)
latest(ref).status latest(ref).status
...@@ -174,6 +175,10 @@ module Ci ...@@ -174,6 +175,10 @@ module Ci
where.not(duration: nil).sum(:duration) where.not(duration: nil).sum(:duration)
end end
def self.internal_sources
sources.reject { |source| source == "external" }.values
end
def stages_count def stages_count
statuses.select(:stage).distinct.count statuses.select(:stage).distinct.count
end end
......
module Ci module Ci
class Variable < ActiveRecord::Base class Variable < ActiveRecord::Base
extend Ci::Model extend Ci::Model
include HasVariable
belongs_to :project belongs_to :project
validates :key, validates :key, uniqueness: { scope: :project_id }
presence: true,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
scope :order_key_asc, -> { reorder(key: :asc) }
scope :unprotected, -> { where(protected: false) } scope :unprotected, -> { where(protected: false) }
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
def to_runner_variable
{ key: key, value: value, public: false }
end
end end
end end
module HasVariable
extend ActiveSupport::Concern
included do
validates :key,
presence: true,
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
scope :order_key_asc, -> { reorder(key: :asc) }
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
def to_runner_variable
{ key: key, value: value, public: false }
end
end
end
module ShaAttribute
extend ActiveSupport::Concern
module ClassMethods
def sha_attribute(name)
column = columns.find { |c| c.name == name.to_s }
# In case the table doesn't exist we won't be able to find the column,
# thus we will only check the type if the column is present.
if column && column.type != :binary
raise ArgumentError,
"sha_attribute #{name.inspect} is invalid since the column type is not :binary"
end
attribute(name, Gitlab::Database::ShaAttribute.new)
end
end
end
class ForkedProjectLink < ActiveRecord::Base class ForkedProjectLink < ActiveRecord::Base
belongs_to :forked_to_project, class_name: 'Project' belongs_to :forked_to_project, -> { where.not(pending_delete: true) }, class_name: 'Project'
belongs_to :forked_from_project, class_name: 'Project' belongs_to :forked_from_project, -> { where.not(pending_delete: true) }, class_name: 'Project'
end end
class Namespace < ActiveRecord::Base class Namespace < ActiveRecord::Base
acts_as_paranoid acts_as_paranoid without_default_scope: true
prepend EE::Namespace prepend EE::Namespace
include CacheMarkdownField include CacheMarkdownField
...@@ -224,6 +224,12 @@ class Namespace < ActiveRecord::Base ...@@ -224,6 +224,12 @@ class Namespace < ActiveRecord::Base
parent.present? parent.present?
end end
def soft_delete_without_removing_associations
# We can't use paranoia's `#destroy` since this will hard-delete projects.
# Project uses `pending_delete` instead of the acts_as_paranoia gem.
self.deleted_at = Time.now
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -19,7 +19,7 @@ class NotificationSetting < ActiveRecord::Base ...@@ -19,7 +19,7 @@ class NotificationSetting < ActiveRecord::Base
# pending delete). # pending delete).
# #
scope :for_projects, -> do scope :for_projects, -> do
includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil }) includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil, pending_delete: true })
end end
EMAIL_EVENTS = [ EMAIL_EVENTS = [
......
...@@ -228,9 +228,8 @@ class Project < ActiveRecord::Base ...@@ -228,9 +228,8 @@ class Project < ActiveRecord::Base
has_many :uploads, as: :model, dependent: :destroy has_many :uploads, as: :model, dependent: :destroy
# Scopes # Scopes
default_scope { where(pending_delete: false) } scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
scope :with_deleted, -> { unscope(where: :pending_delete) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
...@@ -353,7 +352,16 @@ class Project < ActiveRecord::Base ...@@ -353,7 +352,16 @@ class Project < ActiveRecord::Base
after_transition started: :finished do |project, _| after_transition started: :finished do |project, _|
project.reset_cache_and_import_attrs project.reset_cache_and_import_attrs
project.perform_housekeeping
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
project.run_after_commit do
begin
Projects::HousekeepingService.new(project).execute
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info("Could not perform housekeeping for project #{project.path_with_namespace} (#{project.id}): #{e}")
end
end
end
end end
end end
...@@ -511,22 +519,6 @@ class Project < ActiveRecord::Base ...@@ -511,22 +519,6 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id) ProjectCacheWorker.perform_async(self.id)
end end
remove_import_data
end
def perform_housekeeping
return unless repo_exists?
run_after_commit do
begin
Projects::HousekeepingService.new(self).execute
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info("Could not perform housekeeping for project #{self.path_with_namespace} (#{self.id}): #{e}")
end
end
end
def remove_import_data
import_data&.destroy import_data&.destroy
end end
...@@ -1461,7 +1453,7 @@ class Project < ActiveRecord::Base ...@@ -1461,7 +1453,7 @@ class Project < ActiveRecord::Base
def pending_delete_twin def pending_delete_twin
return false unless path return false unless path
Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) Project.pending_delete.find_by_full_path(path_with_namespace)
end end
## ##
......
...@@ -78,6 +78,7 @@ class RemoteMirror < ActiveRecord::Base ...@@ -78,6 +78,7 @@ class RemoteMirror < ActiveRecord::Base
def sync def sync
return unless project && enabled return unless project && enabled
return if project.pending_delete?
return if Gitlab::Geo.secondary? return if Gitlab::Geo.secondary?
RepositoryUpdateRemoteMirrorWorker.perform_in(BACKOFF_DELAY, self.id, Time.now) if project&.repository_exists? RepositoryUpdateRemoteMirrorWorker.perform_in(BACKOFF_DELAY, self.id, Time.now) if project&.repository_exists?
......
...@@ -12,6 +12,7 @@ class User < ActiveRecord::Base ...@@ -12,6 +12,7 @@ class User < ActiveRecord::Base
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn include IgnorableColumn
include FeatureGate include FeatureGate
prepend EE::GeoAwareAvatar prepend EE::GeoAwareAvatar
prepend EE::User prepend EE::User
...@@ -322,11 +323,20 @@ class User < ActiveRecord::Base ...@@ -322,11 +323,20 @@ class User < ActiveRecord::Base
table = arel_table table = arel_table
pattern = "%#{query}%" pattern = "%#{query}%"
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
WHEN users.username = %{query} THEN 1
WHEN users.email = %{query} THEN 2
ELSE 3
END
SQL
where( where(
table[:name].matches(pattern) table[:name].matches(pattern)
.or(table[:email].matches(pattern)) .or(table[:email].matches(pattern))
.or(table[:username].matches(pattern)) .or(table[:username].matches(pattern))
) ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, id: :desc)
end end
# searches user by given pattern # searches user by given pattern
......
require 'declarative_policy' require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin" desc "User is an instance admin"
......
...@@ -55,7 +55,7 @@ module Ci ...@@ -55,7 +55,7 @@ module Ci
def builds_for_shared_runner def builds_for_shared_runner
new_builds. new_builds.
# don't run projects which have not enabled shared runners and builds # don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }) joins(:project).where(projects: { shared_runners_enabled: true, pending_delete: false })
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
...@@ -67,7 +67,7 @@ module Ci ...@@ -67,7 +67,7 @@ module Ci
end end
def builds_for_specific_runner def builds_for_specific_runner
new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC') new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC')
end end
def running_builds_for_shared_runners def running_builds_for_shared_runners
......
...@@ -3,8 +3,8 @@ class GitHooksService ...@@ -3,8 +3,8 @@ class GitHooksService
attr_accessor :oldrev, :newrev, :ref attr_accessor :oldrev, :newrev, :ref
def execute(user, repo_path, oldrev, newrev, ref) def execute(user, project, oldrev, newrev, ref)
@repo_path = repo_path @project = project
@user = Gitlab::GlId.gl_id(user) @user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev @oldrev = oldrev
@newrev = newrev @newrev = newrev
...@@ -26,7 +26,7 @@ class GitHooksService ...@@ -26,7 +26,7 @@ class GitHooksService
private private
def run_hook(name) def run_hook(name)
hook = Gitlab::Git::Hook.new(name, @repo_path) hook = Gitlab::Git::Hook.new(name, @project)
hook.trigger(@user, oldrev, newrev, ref) hook.trigger(@user, oldrev, newrev, ref)
end end
end end
...@@ -120,7 +120,7 @@ class GitOperationService ...@@ -120,7 +120,7 @@ class GitOperationService
def with_hooks(ref, newrev, oldrev) def with_hooks(ref, newrev, oldrev)
GitHooksService.new.execute( GitHooksService.new.execute(
user, user,
repository.path_to_repo, repository.project,
oldrev, oldrev,
newrev, newrev,
ref) do |service| ref) do |service|
......
module Groups module Groups
class DestroyService < Groups::BaseService class DestroyService < Groups::BaseService
def async_execute def async_execute
# Soft delete via paranoia gem group.soft_delete_without_removing_associations
group.destroy
job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end end
...@@ -10,7 +9,7 @@ module Groups ...@@ -10,7 +9,7 @@ module Groups
def execute def execute
group.prepare_for_destroy group.prepare_for_destroy
group.projects.with_deleted.each do |project| group.projects.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup. # Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace # Skip repository removal because we remove directory with namespace
# that contain all these repositories # that contain all these repositories
......
...@@ -95,11 +95,12 @@ module Projects ...@@ -95,11 +95,12 @@ module Projects
end end
# Requires UnZip at least 6.00 Info-ZIP. # Requires UnZip at least 6.00 Info-ZIP.
# -qq be (very) quiet
# -n never overwrite existing files # -n never overwrite existing files
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*') site_path = File.join(SITE_PATH, '*')
build.artifacts_file.use_file do |artifacts_path| build.artifacts_file.use_file do |artifacts_path|
unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path})) unless system(*%W(unzip -qq -n #{artifacts_path} #{site_path} -d #{temp_path}))
raise 'pages failed to extract' raise 'pages failed to extract'
end end
end end
......
...@@ -35,7 +35,7 @@ module Users ...@@ -35,7 +35,7 @@ module Users
Groups::DestroyService.new(group, current_user).execute Groups::DestroyService.new(group, current_user).execute
end end
user.personal_projects.with_deleted.each do |project| user.personal_projects.each do |project|
# Skip repository removal because we remove directory with namespace # Skip repository removal because we remove directory with namespace
# that contain all this repositories # that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
%meta{ property: 'og:title', content: page_title } %meta{ property: 'og:title', content: page_title }
%meta{ property: 'og:description', content: page_description } %meta{ property: 'og:description', content: page_description }
%meta{ property: 'og:image', content: page_image } %meta{ property: 'og:image', content: page_image }
%meta{ property: 'og:image:width', content: '64' }
%meta{ property: 'og:image:height', content: '64' }
%meta{ property: 'og:url', content: request.base_url + request.fullpath } %meta{ property: 'og:url', content: request.base_url + request.fullpath }
-# Twitter Card - https://dev.twitter.com/cards/types/summary -# Twitter Card - https://dev.twitter.com/cards/types/summary
......
.nav-sidebar .nav-sidebar
= link_to admin_root_path, title: 'Admin Overview', class: 'context-header' do
.avatar-container.s40.settings-avatar
= icon('wrench')
.project-title Admin Area
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
......
.nav-sidebar .nav-sidebar
= link_to group_path(@group), title: 'Group', class: 'context-header' do
.avatar-container.s40.group-avatar
= image_tag group_icon(@group), class: "avatar s40 avatar-tile"
.group-title
= @group.name
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do = link_to group_path(@group), title: 'Home' do
......
.nav-sidebar .nav-sidebar
= link_to profile_path, title: 'Profile Settings', class: 'context-header' do
.avatar-container.s40.settings-avatar
= icon('user')
.project-title User Settings
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do = link_to profile_path, title: 'Profile Settings' do
......
.nav-sidebar .nav-sidebar
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
= link_to project_path(@project), title: 'Project', class: 'context-header' do
.avatar-container.s40.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
.project-title
= @project.name
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
......
...@@ -8,27 +8,28 @@ ...@@ -8,27 +8,28 @@
= render "head" = render "head"
%div{ class: container_class } %div{ class: container_class }
.row-content-block.second-block.content-component-block.flex-container-block .tree-holder
.tree-ref-holder .nav-block
= render 'shared/ref_switcher', destination: 'commits' .tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.tree-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
= link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref)
.control
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control .control
= link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
- elsif create_mr_button?(@repository.root_ref, @ref) = search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control .control
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' = link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss")
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss")
= render 'projects/commits/mirror_status' = render 'projects/commits/mirror_status'
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
- if plain - if plain
= link_text = link_text
- else - else
= add_diff_note_button(line_code, diff_file.position(line), type)
%a{ href: "##{line_code}", data: { linenumber: link_text } } %a{ href: "##{line_code}", data: { linenumber: link_text } }
- discussion = line_discussions.try(:first) - discussion = line_discussions.try(:first)
- if discussion && discussion.resolvable? && !plain - if discussion && discussion.resolvable? && !plain
...@@ -29,7 +30,7 @@ ...@@ -29,7 +30,7 @@
= link_text = link_text
- else - else
%a{ href: "##{line_code}", data: { linenumber: link_text } } %a{ href: "##{line_code}", data: { linenumber: link_text } }
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }< %td.line_content.noteable_line{ class: type }<
- if email - if email
%pre= line.text %pre= line.text
- else - else
......
/ Side-by-side diff view / Side-by-side diff view
.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table %table
- diff_file.parallel_diff_lines.each do |line| - diff_file.parallel_diff_lines.each do |line|
...@@ -18,11 +19,12 @@ ...@@ -18,11 +19,12 @@
- left_line_code = diff_file.line_code(left) - left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left) - left_position = diff_file.position(left)
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first) - discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable? - if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id } %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel
...@@ -38,11 +40,12 @@ ...@@ -38,11 +40,12 @@
- right_line_code = diff_file.line_code(right) - right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right) - right_position = diff_file.position(right)
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first) - discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable? - if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id } %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text)
- else - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
&nbsp; &nbsp;
- issue.labels.each do |label| - issue.labels.each do |label|
= link_to_label(label, subject: issue.project, css_class: 'label-link') = link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.weight - if issue.weight
%span.issuable-weight %span.issuable-weight
&nbsp; &nbsp;
......
...@@ -18,6 +18,9 @@ ...@@ -18,6 +18,9 @@
= render 'projects/last_push' = render 'projects/last_push'
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- new_merge_request_path = namespace_project_new_merge_request_path(merge_project.namespace, merge_project) if merge_project
- if @project.merge_requests.exists? - if @project.merge_requests.exists?
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
...@@ -33,4 +36,5 @@ ...@@ -33,4 +36,5 @@
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
- else - else
= render 'shared/empty_states/merge_requests', button_path: namespace_project_new_merge_request_path(@project.namespace, @project) = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%li %li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
#{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) #{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
%l %li
= link_to namespace_project_branches_path(@project.namespace, @project) do = link_to namespace_project_branches_path(@project.namespace, @project) do
#{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) #{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li %li
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
- if projects.any? - if projects.any?
%ul.projects-list %ul.projects-list
- projects.each_with_index do |project, i| - projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace, = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description forks: forks, show_last_commit_as_description: show_last_commit_as_description
......
...@@ -45,7 +45,7 @@ class StuckCiJobsWorker ...@@ -45,7 +45,7 @@ class StuckCiJobsWorker
def search(status, timeout) def search(status, timeout)
builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago) builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
builds.joins(:project).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build| builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
yield(build) yield(build)
end end
end end
......
---
title: Inserts exact matches of name, username and email to the top of the search
list
merge_request: 12525
author:
---
title: Removes deleted_at and pending_delete occurrences in Project related queries
merge_request: 12091
author:
---
title: Use authorize_update_pipeline_schedule in PipelineSchedulesController
merge_request: 11846
author:
---
title: Supplement Traditional Chinese in Taiwan translation of Project Page & Repository Page
merge_request: 12514
author: Huang Tao
---
title: Closes any open Autocomplete of the markdown editor when the form is closed
merge_request: 12521
author:
---
title: Rename all reserved paths that could have been created
merge_request: 11713
author:
---
title: Fix diff of requirements.txt file by not matching newlines as part of package
names
merge_request:
author:
---
title: Fix 'New merge request' button for users who don't have push access to canonical
project
merge_request:
author:
---
title: Limit OpenGraph image size to 64x64
merge_request:
author:
---
title: Strip trailing whitespace in relative submodule URL
merge_request:
author:
---
title: Fix head pipeline stored in merge request for external pipelines
merge_request: 12478
author:
---
title: Fixed sidebar not collapsing on merge requests in mobile screens
merge_request:
author:
---
title: Fix errors caused by attempts to report already blocked or deleted users
merge_request: 12502
author: Horacio Bertorello
---
title: Fixed issue boards closed list not showing all closed issues
merge_request:
author:
---
title: Fixed multi-line markdown tooltip buttons in issue edit form
merge_request:
author:
---
title: Defer project destroys within a namespace in Groups::DestroyService#async_execute
merge_request:
author:
---
title: Split pipelines as internal and external in the usage data
merge_request: 12277
author:
required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) unless Rails.env.test?
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)
unless current_version.valid? && required_version <= current_version unless current_version.valid? && required_version <= current_version
warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell." warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell."
end
end end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RenameAllReservedPathsAgain < ActiveRecord::Migration
include Gitlab::Database::RenameReservedPathsMigration::V1
DOWNTIME = false
disable_ddl_transaction!
TOP_LEVEL_ROUTES = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
PROJECT_WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
GROUP_ROUTES = %w[
activity
analytics
audit_events
avatar
edit
group_members
hooks
issues
labels
ldap
ldap_group_links
merge_requests
milestones
notification_setting
pipeline_quota
projects
subgroups
].freeze
def up
disable_statement_timeout
TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) }
PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) }
GROUP_ROUTES.each { |route| rename_child_paths(route) }
end
def down
disable_statement_timeout
revert_renames
end
end
...@@ -34,9 +34,9 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a ...@@ -34,9 +34,9 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a
passphrase to the SSH key, or the `before_script` will prompt for it. passphrase to the SSH key, or the `before_script` will prompt for it.
Then, create a new **Secret Variable** in your project settings on GitLab Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` following **Settings > Pipelines** and look for the "Secret Variables" section.
and in the **Value** field paste the content of your _private_ key that you As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the
created earlier. content of your _private_ key that you created earlier.
It is also good practice to check the server's own public key to make sure you It is also good practice to check the server's own public key to make sure you
are not being targeted by a man-in-the-middle attack. To do this, add another are not being targeted by a man-in-the-middle attack. To do this, add another
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
- [Polymorphic Associations](polymorphic_associations.md) - [Polymorphic Associations](polymorphic_associations.md)
- [Single Table Inheritance](single_table_inheritance.md) - [Single Table Inheritance](single_table_inheritance.md)
- [Background Migrations](background_migrations.md) - [Background Migrations](background_migrations.md)
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
## i18n ## i18n
......
# Storing SHA1 Hashes As Binary
Storing SHA1 hashes as strings is not very space efficient. A SHA1 as a string
requires at least 40 bytes, an additional byte to store the encoding, and
perhaps more space depending on the internals of PostgreSQL and MySQL.
On the other hand, if one were to store a SHA1 as binary one would only need 20
bytes for the actual SHA1, and 1 or 4 bytes of additional space (again depending
on database internals). This means that in the best case scenario we can reduce
the space usage by 50%.
To make this easier to work with you can include the concern `ShaAttribute` into
a model and define a SHA attribute using the `sha_attribute` class method. For
example:
```ruby
class Commit < ActiveRecord::Base
include ShaAttribute
sha_attribute :sha
end
```
This allows you to use the value of the `sha` attribute as if it were a string,
while storing it as binary. This means that you can do something like this,
without having to worry about converting data to the right binary format:
```ruby
commit = Commit.find_by(sha: '88c60307bd1f215095834f09a1a5cb18701ac8ad')
commit.sha = '971604de4cfa324d91c41650fabc129420c8d1cc'
commit.save
```
There is however one requirement: the column used to store the SHA has _must_ be
a binary type. For Rails this means you need to use the `:binary` type instead
of `:text` or `:string`.
doc/user/project/img/issue_board.png

74.7 KB | W: | H:

doc/user/project/img/issue_board.png

50.2 KB | W: | H:

doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -232,7 +232,7 @@ module SharedDiffNote ...@@ -232,7 +232,7 @@ module SharedDiffNote
end end
def click_parallel_diff_line(code, line_type) def click_parallel_diff_line(code, line_type)
find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover' find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover'
find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
end end
end end
...@@ -48,7 +48,8 @@ module API ...@@ -48,7 +48,8 @@ module API
yield if block_given? yield if block_given?
forbidden!('Project has been deleted!') unless job.project project = job.project
forbidden!('Project has been deleted!') if project.nil? || project.pending_delete?
forbidden!('Job has been erased!') if job.erased? forbidden!('Job has been erased!') if job.erased?
end end
......
require 'declarative_policy' require_dependency 'declarative_policy'
module API module API
# Projects API # Projects API
......
...@@ -30,7 +30,8 @@ module Ci ...@@ -30,7 +30,8 @@ module Ci
yield if block_given? yield if block_given?
forbidden!('Project has been deleted!') unless build.project project = build.project
forbidden!('Project has been deleted!') if project.nil? || project.pending_delete?
forbidden!('Build has been erased!') if build.erased? forbidden!('Build has been erased!') if build.erased?
end end
......
...@@ -29,6 +29,11 @@ module Gitlab ...@@ -29,6 +29,11 @@ module Gitlab
paths = Array(paths) paths = Array(paths)
RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level) RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level)
end end
def revert_renames
RenameProjects.new([], self).revert_renames
RenameNamespaces.new([], self).revert_renames
end
end end
end end
end end
......
...@@ -6,7 +6,10 @@ module Gitlab ...@@ -6,7 +6,10 @@ module Gitlab
attr_reader :paths, :migration attr_reader :paths, :migration
delegate :update_column_in_batches, delegate :update_column_in_batches,
:execute,
:replace_sql, :replace_sql,
:quote_string,
:say,
to: :migration to: :migration
def initialize(paths, migration) def initialize(paths, migration)
...@@ -26,24 +29,45 @@ module Gitlab ...@@ -26,24 +29,45 @@ module Gitlab
new_path = rename_path(namespace_path, old_path) new_path = rename_path(namespace_path, old_path)
new_full_path = join_routable_path(namespace_path, new_path) new_full_path = join_routable_path(namespace_path, new_path)
perform_rename(routable, old_full_path, new_full_path)
[old_full_path, new_full_path]
end
def perform_rename(routable, old_full_path, new_full_path)
# skips callbacks & validations # skips callbacks & validations
new_path = new_full_path.split('/').last
routable.class.where(id: routable) routable.class.where(id: routable)
.update_all(path: new_path) .update_all(path: new_path)
rename_routes(old_full_path, new_full_path) rename_routes(old_full_path, new_full_path)
[old_full_path, new_full_path]
end end
def rename_routes(old_full_path, new_full_path) def rename_routes(old_full_path, new_full_path)
routes = Route.arel_table
quoted_old_full_path = quote_string(old_full_path)
quoted_old_wildcard_path = quote_string("#{old_full_path}/%")
filter = if Database.mysql?
"lower(routes.path) = lower('#{quoted_old_full_path}') "\
"OR routes.path LIKE '#{quoted_old_wildcard_path}'"
else
"routes.id IN "\
"( SELECT routes.id FROM routes WHERE lower(routes.path) = lower('#{quoted_old_full_path}') "\
"UNION SELECT routes.id FROM routes WHERE routes.path ILIKE '#{quoted_old_wildcard_path}' )"
end
replace_statement = replace_sql(Route.arel_table[:path], replace_statement = replace_sql(Route.arel_table[:path],
old_full_path, old_full_path,
new_full_path) new_full_path)
update_column_in_batches(:routes, :path, replace_statement) do |table, query| update = Arel::UpdateManager.new(ActiveRecord::Base)
path_or_children = table[:path].matches_any([old_full_path, "#{old_full_path}/%"]) .table(routes)
query.where(path_or_children) .set([[routes[:path], replace_statement]])
end .where(Arel::Nodes::SqlLiteral.new(filter))
execute(update.to_sql)
end end
def rename_path(namespace_path, path_was) def rename_path(namespace_path, path_was)
...@@ -86,32 +110,74 @@ module Gitlab ...@@ -86,32 +110,74 @@ module Gitlab
def move_folders(directory, old_relative_path, new_relative_path) def move_folders(directory, old_relative_path, new_relative_path)
old_path = File.join(directory, old_relative_path) old_path = File.join(directory, old_relative_path)
return unless File.directory?(old_path) unless File.directory?(old_path)
say "#{old_path} doesn't exist, skipping"
return
end
new_path = File.join(directory, new_relative_path) new_path = File.join(directory, new_relative_path)
FileUtils.mv(old_path, new_path) FileUtils.mv(old_path, new_path)
end end
def remove_cached_html_for_projects(project_ids) def remove_cached_html_for_projects(project_ids)
update_column_in_batches(:projects, :description_html, nil) do |table, query| project_ids.each do |project_id|
query.where(table[:id].in(project_ids)) update_column_in_batches(:projects, :description_html, nil) do |table, query|
end query.where(table[:id].eq(project_id))
end
update_column_in_batches(:issues, :description_html, nil) do |table, query|
query.where(table[:project_id].in(project_ids)) update_column_in_batches(:issues, :description_html, nil) do |table, query|
query.where(table[:project_id].eq(project_id))
end
update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
query.where(table[:target_project_id].eq(project_id))
end
update_column_in_batches(:notes, :note_html, nil) do |table, query|
query.where(table[:project_id].eq(project_id))
end
update_column_in_batches(:milestones, :description_html, nil) do |table, query|
query.where(table[:project_id].eq(project_id))
end
end end
end
update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| def track_rename(type, old_path, new_path)
query.where(table[:target_project_id].in(project_ids)) key = redis_key_for_type(type)
Gitlab::Redis.with do |redis|
redis.lpush(key, [old_path, new_path].to_json)
redis.expire(key, 2.weeks.to_i)
end end
say "tracked rename: #{key}: #{old_path} -> #{new_path}"
end
update_column_in_batches(:notes, :note_html, nil) do |table, query| def reverts_for_type(type)
query.where(table[:project_id].in(project_ids)) key = redis_key_for_type(type)
Gitlab::Redis.with do |redis|
failed_reverts = []
while rename_info = redis.lpop(key)
path_before_rename, path_after_rename = JSON.parse(rename_info)
say "renaming #{type} from #{path_after_rename} back to #{path_before_rename}"
begin
yield(path_before_rename, path_after_rename)
rescue StandardError => e
failed_reverts << rename_info
say "Renaming #{type} from #{path_after_rename} back to "\
"#{path_before_rename} failed. Review the error and try "\
"again by running the `down` action. \n"\
"#{e.message}: \n #{e.backtrace.join("\n")}"
end
end
failed_reverts.each { |rename_info| redis.lpush(key, rename_info) }
end end
end
update_column_in_batches(:milestones, :description_html, nil) do |table, query| def redis_key_for_type(type)
query.where(table[:project_id].in(project_ids)) "rename:#{migration.name}:#{type}"
end
end end
def file_storage? def file_storage?
......
...@@ -26,6 +26,12 @@ module Gitlab ...@@ -26,6 +26,12 @@ module Gitlab
def rename_namespace(namespace) def rename_namespace(namespace)
old_full_path, new_full_path = rename_path_for_routable(namespace) old_full_path, new_full_path = rename_path_for_routable(namespace)
track_rename('namespace', old_full_path, new_full_path)
rename_namespace_dependencies(namespace, old_full_path, new_full_path)
end
def rename_namespace_dependencies(namespace, old_full_path, new_full_path)
move_repositories(namespace, old_full_path, new_full_path) move_repositories(namespace, old_full_path, new_full_path)
move_uploads(old_full_path, new_full_path) move_uploads(old_full_path, new_full_path)
move_pages(old_full_path, new_full_path) move_pages(old_full_path, new_full_path)
...@@ -33,6 +39,23 @@ module Gitlab ...@@ -33,6 +39,23 @@ module Gitlab
remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id)) remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id))
end end
def revert_renames
reverts_for_type('namespace') do |path_before_rename, current_path|
matches_path = MigrationClasses::Route.arel_table[:path].matches(current_path)
namespace = MigrationClasses::Namespace.joins(:route)
.where(matches_path).first&.becomes(MigrationClasses::Namespace)
if namespace
perform_rename(namespace, current_path, path_before_rename)
rename_namespace_dependencies(namespace, current_path, path_before_rename)
else
say "Couldn't rename namespace from #{current_path} back to #{path_before_rename}, "\
"namespace was renamed, or no longer exists at the expected path"
end
end
end
def rename_user(old_username, new_username) def rename_user(old_username, new_username)
MigrationClasses::User.where(username: old_username) MigrationClasses::User.where(username: old_username)
.update_all(username: new_username) .update_all(username: new_username)
......
...@@ -16,12 +16,37 @@ module Gitlab ...@@ -16,12 +16,37 @@ module Gitlab
def rename_project(project) def rename_project(project)
old_full_path, new_full_path = rename_path_for_routable(project) old_full_path, new_full_path = rename_path_for_routable(project)
track_rename('project', old_full_path, new_full_path)
move_project_folders(project, old_full_path, new_full_path)
end
def move_project_folders(project, old_full_path, new_full_path)
move_repository(project, old_full_path, new_full_path) move_repository(project, old_full_path, new_full_path)
move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
move_uploads(old_full_path, new_full_path) move_uploads(old_full_path, new_full_path)
move_pages(old_full_path, new_full_path) move_pages(old_full_path, new_full_path)
end end
def revert_renames
reverts_for_type('project') do |path_before_rename, current_path|
matches_path = MigrationClasses::Route.arel_table[:path].matches(current_path)
project = MigrationClasses::Project.joins(:route)
.where(matches_path).first
if project
perform_rename(project, current_path, path_before_rename)
move_project_folders(project, current_path, path_before_rename)
else
say "Couldn't rename project from #{current_path} back to "\
"#{path_before_rename}, project was renamed or no longer "\
"exists at the expected path."
end
end
end
def move_repository(project, old_path, new_path) def move_repository(project, old_path, new_path)
unless gitlab_shell.mv_repository(project.repository_storage_path, unless gitlab_shell.mv_repository(project.repository_storage_path,
old_path, old_path,
......
module Gitlab
module Database
BINARY_TYPE = if Gitlab::Database.postgresql?
# PostgreSQL defines its own class with slightly different
# behaviour from the default Binary type.
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea
else
ActiveRecord::Type::Binary
end
# Class for casting binary data to hexadecimal SHA1 hashes (and vice-versa).
#
# Using ShaAttribute allows you to store SHA1 values as binary while still
# using them as if they were stored as string values. This gives you the
# ease of use of string values, but without the storage overhead.
class ShaAttribute < BINARY_TYPE
PACK_FORMAT = 'H*'.freeze
# Casts binary data to a SHA1 in hexadecimal.
def type_cast_from_database(value)
value = super
value ? value.unpack(PACK_FORMAT)[0] : nil
end
# Casts a SHA1 in hexadecimal to the proper binary format.
def type_cast_for_database(value)
arg = value ? [value].pack(PACK_FORMAT) : nil
super(arg)
end
end
end
end
...@@ -7,8 +7,10 @@ module Gitlab ...@@ -7,8 +7,10 @@ module Gitlab
CommandError = Class.new(StandardError) CommandError = Class.new(StandardError)
class << self class << self
include Gitlab::EncodingHelper
def ref_name(ref) def ref_name(ref)
ref.sub(/\Arefs\/(tags|heads)\//, '') encode! ref.sub(/\Arefs\/(tags|heads)\//, '')
end end
def branch_name(ref) def branch_name(ref)
......
...@@ -4,9 +4,10 @@ module Gitlab ...@@ -4,9 +4,10 @@ module Gitlab
GL_PROTOCOL = 'web'.freeze GL_PROTOCOL = 'web'.freeze
attr_reader :name, :repo_path, :path attr_reader :name, :repo_path, :path
def initialize(name, repo_path) def initialize(name, project)
@name = name @name = name
@repo_path = repo_path @project = project
@repo_path = project.repository.path
@path = File.join(repo_path.strip, 'hooks', name) @path = File.join(repo_path.strip, 'hooks', name)
end end
...@@ -38,7 +39,8 @@ module Gitlab ...@@ -38,7 +39,8 @@ module Gitlab
vars = { vars = {
'GL_ID' => gl_id, 'GL_ID' => gl_id,
'PWD' => repo_path, 'PWD' => repo_path,
'GL_PROTOCOL' => GL_PROTOCOL 'GL_PROTOCOL' => GL_PROTOCOL,
'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false)
} }
options = { options = {
......
...@@ -113,9 +113,7 @@ module Gitlab ...@@ -113,9 +113,7 @@ module Gitlab
def local_branches(sort_by: nil) def local_branches(sort_by: nil)
gitaly_migrate(:local_branches) do |is_enabled| gitaly_migrate(:local_branches) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by).map do |gitaly_branch| gitaly_ref_client.local_branches(sort_by: sort_by)
Gitlab::Git::Branch.new(self, gitaly_branch.name, gitaly_branch)
end
else else
branches(filter: :local, sort_by: sort_by) branches(filter: :local, sort_by: sort_by)
end end
......
module Gitlab module Gitlab
module GitalyClient module GitalyClient
class Ref class Ref
include Gitlab::EncodingHelper
# 'repository' is a Gitlab::Git::Repository # 'repository' is a Gitlab::Git::Repository
def initialize(repository) def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository @gitaly_repo = repository.gitaly_repository
@storage = repository.storage @storage = repository.storage
end end
...@@ -16,13 +19,13 @@ module Gitlab ...@@ -16,13 +19,13 @@ module Gitlab
def branch_names def branch_names
request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :ref, :find_all_branch_names, request) response = GitalyClient.call(@storage, :ref, :find_all_branch_names, request)
consume_refs_response(response, prefix: 'refs/heads/') consume_refs_response(response) { |name| Gitlab::Git.branch_name(name) }
end end
def tag_names def tag_names
request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo) request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :ref, :find_all_tag_names, request) response = GitalyClient.call(@storage, :ref, :find_all_tag_names, request)
consume_refs_response(response, prefix: 'refs/tags/') consume_refs_response(response) { |name| Gitlab::Git.tag_name(name) }
end end
def find_ref_name(commit_id, ref_prefix) def find_ref_name(commit_id, ref_prefix)
...@@ -51,20 +54,28 @@ module Gitlab ...@@ -51,20 +54,28 @@ module Gitlab
private private
def consume_refs_response(response, prefix:) def consume_refs_response(response)
response.flat_map do |r| response.flat_map { |message| message.names.map { |name| yield(name) } }
r.names.map { |name| name.sub(/\A#{Regexp.escape(prefix)}/, '') }
end
end end
def sort_by_param(sort_by) def sort_by_param(sort_by)
sort_by = 'name' if sort_by == 'name_asc'
enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym)
raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value
enum_value enum_value
end end
def consume_branches_response(response) def consume_branches_response(response)
response.flat_map { |r| r.branches } response.flat_map do |message|
message.branches.map do |gitaly_branch|
Gitlab::Git::Branch.new(
@repository,
encode!(gitaly_branch.name.dup),
gitaly_branch.commit_id
)
end
end
end end
end end
end end
......
...@@ -20,7 +20,8 @@ module Gitlab ...@@ -20,7 +20,8 @@ module Gitlab
counts: { counts: {
boards: Board.count, boards: Board.count,
ci_builds: ::Ci::Build.count, ci_builds: ::Ci::Build.count,
ci_pipelines: ::Ci::Pipeline.count, ci_internal_pipelines: ::Ci::Pipeline.internal.count,
ci_external_pipelines: ::Ci::Pipeline.external.count,
ci_runners: ::Ci::Runner.count, ci_runners: ::Ci::Runner.count,
ci_triggers: ::Ci::Trigger.count, ci_triggers: ::Ci::Trigger.count,
ci_pipeline_schedules: ::Ci::PipelineSchedule.count, ci_pipeline_schedules: ::Ci::PipelineSchedule.count,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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