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:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only:
- /mysql/
- /-stable$/
- /-stable/
- master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce
......@@ -460,9 +460,10 @@ codeclimate:
services:
- docker:dind
script:
- docker pull stedolan/jq
- 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
- sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' 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
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts:
paths: [codeclimate.json]
......
......@@ -4,13 +4,13 @@ entry.
## 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
- Bring back branches badge to main project page. !12548
- 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 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)
......
......@@ -85,9 +85,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
this.verifyTopPosition();
})
.then(() => this.verifyTopPosition());
}
Build.prototype.canScroll = function () {
......@@ -176,7 +175,7 @@ window.Build = (function () {
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
topPostion += $flashError.outerHeight() + prependTopDefault;
}
this.$buildTrace.css({
......@@ -196,6 +195,7 @@ window.Build = (function () {
})
.done((log) => {
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) {
this.state = log.state;
}
......@@ -220,7 +220,11 @@ window.Build = (function () {
}
if (!log.complete) {
this.toggleScrollAnimation(true);
if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
Build.timeout = setTimeout(() => {
//eslint-disable-next-line
......@@ -229,7 +233,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
})
.then(() => this.verifyTopPosition());
}, 4000);
} else {
this.$buildRefreshAnimation.remove();
......
/* eslint-disable class-methods-use-this */
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
const UNFOLD_COUNT = 20;
let isBound = false;
......@@ -8,8 +9,10 @@ let isBound = false;
class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff();
$diffFile.filesCommentButton();
FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file));
......
......@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0)
.toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0);
.toggleClass('no-comment-btn', notesCount > 0);
},
toggleDiscussionsToggleState() {
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 */
/* global FilesCommentButton */
/* global notes */
let $commentButtonTemplate;
window.FilesCommentButton = (function() {
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;
COMMENT_BUTTON_CLASS = '.add-diff-note';
LINE_HOLDER_CLASS = '.line_holder';
LINE_NUMBER_CLASS = 'diff-line-num';
LINE_CONTENT_CLASS = 'line_content';
UNFOLDABLE_LINE_CLASS = 'js-unfold';
EMPTY_CELL_CLASS = 'empty-cell';
OLD_LINE_CLASS = 'old_line';
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
TEXT_FILE_SELECTOR = '.text-file';
function FilesCommentButton(filesContainerElement) {
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;
/* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
const EMPTY_CELL_CLASS = 'empty-cell';
const OLD_LINE_CLASS = 'old_line';
const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
const DIFF_CONTAINER_SELECTOR = '.files';
const DIFF_EXPANDED_CLASS = 'diff-expanded';
export default {
init($diffFile) {
/* 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
* will be true in all cases */
if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
}
textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
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);
if (typeof notes !== 'undefined' && !this.isParallelView) {
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
};
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
if (!this.isParallelView) {
if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement;
}
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
} else {
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
return hoveredElement;
}
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
.on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
}
};
},
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
};
showButton(isParallelView, e) {
const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
};
if (!this.validateButtonParent(buttonParentElement)) return;
return FilesCommentButton;
})();
buttonParentElement.classList.add('is-over');
buttonParentElement.nextElementSibling.classList.add('is-over');
},
$.fn.filesCommentButton = function() {
$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>');
hideButton(isParallelView, e) {
const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
if (!(this && (this.parent().data('can-create-note') != null))) {
return;
}
return this.each(function() {
if (!$.data(this, 'filesCommentButton')) {
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
buttonParentElement.classList.remove('is-over');
buttonParentElement.nextElementSibling.classList.remove('is-over');
},
getButtonParent(hoveredElement, isParallelView) {
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 {
this.cachedData = {};
}
destroy() {
this.input.each((i, input) => {
const $input = $(input);
$input.atwho('destroy');
});
}
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {
......
......@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
GLForm.prototype.destroy = function() {
// Clean form listeners
this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
return this.form.data('gl-form', null);
};
......@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form');
// 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'));
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,
members: this.enableGFM,
issues: this.enableGFM,
......
......@@ -39,6 +39,17 @@
runnerId() {
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>
......@@ -63,7 +74,7 @@
Retry
</a>
</div>
<div class="block">
<div :class="{block : renderBlock }">
<p
class="build-detail-row js-job-mr"
v-if="job.merge_request">
......
......@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.resetViewContainer();
this.mountPipelinesView();
} else {
this.expandView();
if (Breakpoints.get().getBreakpointSize() !== 'xs') {
this.expandView();
}
this.resetViewContainer();
this.destroyPipelinesView();
}
......
......@@ -829,6 +829,8 @@ export default class Notes {
*/
setupDiscussionNoteForm(dataHolder, form) {
// setup note target
const diffFileData = dataHolder.closest('.text-file');
var discussionID = dataHolder.data('discussionId');
if (discussionID) {
......@@ -839,9 +841,10 @@ export default class Notes {
form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType'));
form.find('#note_noteable_type').val(dataHolder.data('noteableType'));
form.find('#note_noteable_id').val(dataHolder.data('noteableId'));
form.find('#note_commit_id').val(dataHolder.data('commitId'));
form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
form.find('#note_commit_id').val(diffFileData.data('commitId'));
form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote
......
......@@ -10,6 +10,8 @@ import Cookies from 'js-cookie';
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$layoutNav = $('.layout-nav');
this.$subScroll = $('.sub-nav-scroll');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners();
......@@ -27,14 +29,14 @@ import Cookies from 'js-cookie';
Sidebar.prototype.addEventListeners = function() {
const $document = $(document);
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);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => debouncedSetSidebarHeight());
$document.on('scroll', () => slowerThrottledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
......@@ -213,7 +215,7 @@ import Cookies from 'js-cookie';
};
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();
if (diff > 0) {
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 */
import FilesCommentButton from './files_comment_button';
(function() {
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
......@@ -78,6 +80,8 @@
gl.diffNotesCompileComponents();
}
FilesCommentButton.init($(_this.file));
if (cb) cb();
};
})(this));
......
......@@ -64,6 +64,12 @@
*/
return new gl.GLForm($(this.$refs['gl-form']), true);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
};
</script>
......
......@@ -11,20 +11,19 @@ header.navbar-gitlab-new {
padding-left: 0;
.title-container {
align-items: stretch;
padding-top: 0;
overflow: visible;
}
.title {
display: block;
height: 100%;
display: flex;
padding-right: 0;
color: currentColor;
> a {
display: flex;
align-items: center;
height: 100%;
padding-top: 3px;
padding-right: $gl-padding;
padding-left: $gl-padding;
......
......@@ -5,17 +5,46 @@
$new-sidebar-width: 220px;
.page-with-new-sidebar {
@media (min-width: $screen-sm-min) {
padding-left: $new-sidebar-width;
}
// Override position: absolute
.right-sidebar {
position: fixed;
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 {
position: fixed;
z-index: 400;
......
......@@ -147,10 +147,9 @@
top: 35px;
left: 10px;
bottom: 0;
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px;
white-space: pre;
white-space: pre-wrap;
overflow: auto;
}
.environment-information {
......@@ -399,6 +398,7 @@
.build-light-text {
color: $gl-text-color-secondary;
word-wrap: break-word;
}
.build-gutter-toggle {
......
......@@ -20,8 +20,6 @@
}
.diff-content {
overflow: auto;
overflow-y: hidden;
background: $white-light;
color: $gl-text-color;
border-radius: 0 0 3px 3px;
......@@ -476,6 +474,7 @@
height: 19px;
width: 19px;
margin-left: -15px;
z-index: 100;
&:hover {
.diff-comment-avatar,
......@@ -491,7 +490,7 @@
transform: translateX((($i * $x-pos) - $x-pos));
&:hover {
transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2);
transform: translateX((($i * $x-pos) - $x-pos));
}
}
}
......@@ -542,6 +541,7 @@
height: 19px;
padding: 0;
transition: transform .1s ease-out;
z-index: 100;
svg {
position: absolute;
......@@ -555,10 +555,6 @@
fill: $white-light;
}
&:hover {
transform: scale(1.2);
}
&:focus {
outline: 0;
}
......
......@@ -659,8 +659,8 @@
@media(max-width: $screen-md-max) {
.task-status,
.issuable-due-date,
.project-ref-path,
.issuable-weight {
.issuable-weight,
.project-ref-path {
display: none;
}
}
......
......@@ -628,8 +628,14 @@ ul.notes {
* Line note button on the side of diffs
*/
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
}
}
.add-diff-note {
display: none;
opacity: 0;
margin-top: -2px;
border-radius: 50%;
background: $white-light;
......@@ -642,13 +648,11 @@ ul.notes {
width: 23px;
height: 23px;
border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover {
background: $blue-500;
border-color: $blue-600;
color: $white-light;
transform: scale(1.15);
}
&:active {
......
.tree-holder {
.nav-block {
margin: 10px 0;
......@@ -15,6 +16,11 @@
.btn-group {
margin-left: 10px;
}
.control {
float: left;
margin-left: 10px;
}
}
.tree-ref-holder {
......
class AbuseReportsController < ApplicationController
before_action :set_user, only: [:new]
def new
@abuse_report = AbuseReport.new
@abuse_report.user_id = params[:user_id]
@abuse_report.user_id = @user.id
@ref_url = params.fetch(:ref_url, '')
end
......@@ -27,4 +29,14 @@ class AbuseReportsController < ApplicationController
user_id
))
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
class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
prepend ::EE::Projects::MergeRequests::CreationsController
skip_before_action :merge_request
......
......@@ -4,6 +4,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include RendersNotes
include ToggleAwardEmoji
include IssuableCollections
prepend ::EE::Projects::MergeRequestsController
skip_before_action :merge_request, only: [:index, :bulk_update]
......
class Projects::PipelineSchedulesController < Projects::ApplicationController
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 :schedule, only: [:edit, :update, :destroy, :take_ownership]
......
......@@ -98,7 +98,7 @@ class ProjectsController < Projects::ApplicationController
end
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
respond_to do |format|
......
......@@ -83,6 +83,8 @@ class TodosFinder
if project?
@project = Project.find(params[:project_id])
@project = nil if @project.pending_delete?
unless Ability.allowed?(current_user, :read_project, @project)
@project = nil
end
......
......@@ -47,6 +47,18 @@ module NotesHelper
data
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)
return unless current_user
......
......@@ -73,6 +73,7 @@ module SubmoduleHelper
end
def relative_self_links(url, commit)
url.rstrip!
# Map relative links to a namespace and project
# For example:
# ../bar.git -> same namespace, repo bar
......
require 'declarative_policy'
require_dependency 'declarative_policy'
class Ability
class << self
......
......@@ -151,6 +151,7 @@ module Ci
where(id: max_id)
end
end
scope :internal, -> { where(source: internal_sources) }
def self.latest_status(ref = nil)
latest(ref).status
......@@ -174,6 +175,10 @@ module Ci
where.not(duration: nil).sum(:duration)
end
def self.internal_sources
sources.reject { |source| source == "external" }.values
end
def stages_count
statuses.select(:stage).distinct.count
end
......
module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
include HasVariable
belongs_to :project
validates :key,
presence: true,
uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
validates :key, uniqueness: { scope: :project_id }
scope :order_key_asc, -> { reorder(key: :asc) }
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
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
belongs_to :forked_to_project, class_name: 'Project'
belongs_to :forked_from_project, class_name: 'Project'
belongs_to :forked_to_project, -> { where.not(pending_delete: true) }, class_name: 'Project'
belongs_to :forked_from_project, -> { where.not(pending_delete: true) }, class_name: 'Project'
end
class Namespace < ActiveRecord::Base
acts_as_paranoid
acts_as_paranoid without_default_scope: true
prepend EE::Namespace
include CacheMarkdownField
......@@ -224,6 +224,12 @@ class Namespace < ActiveRecord::Base
parent.present?
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
def repository_storage_paths
......
......@@ -19,7 +19,7 @@ class NotificationSetting < ActiveRecord::Base
# pending delete).
#
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
EMAIL_EVENTS = [
......
......@@ -228,9 +228,8 @@ class Project < ActiveRecord::Base
has_many :uploads, as: :model, dependent: :destroy
# Scopes
default_scope { where(pending_delete: false) }
scope :with_deleted, -> { unscope(where: :pending_delete) }
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
......@@ -353,7 +352,16 @@ class Project < ActiveRecord::Base
after_transition started: :finished do |project, _|
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
......@@ -511,22 +519,6 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id)
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
end
......@@ -1461,7 +1453,7 @@ class Project < ActiveRecord::Base
def pending_delete_twin
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
##
......
......@@ -78,6 +78,7 @@ class RemoteMirror < ActiveRecord::Base
def sync
return unless project && enabled
return if project.pending_delete?
return if Gitlab::Geo.secondary?
RepositoryUpdateRemoteMirrorWorker.perform_in(BACKOFF_DELAY, self.id, Time.now) if project&.repository_exists?
......
......@@ -12,6 +12,7 @@ class User < ActiveRecord::Base
include TokenAuthenticatable
include IgnorableColumn
include FeatureGate
prepend EE::GeoAwareAvatar
prepend EE::User
......@@ -322,11 +323,20 @@ class User < ActiveRecord::Base
table = arel_table
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(
table[:name].matches(pattern)
.or(table[:email].matches(pattern))
.or(table[:username].matches(pattern))
)
).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, id: :desc)
end
# searches user by given pattern
......
require 'declarative_policy'
require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin"
......
......@@ -55,7 +55,7 @@ module Ci
def builds_for_shared_runner
new_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')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
......@@ -67,7 +67,7 @@ module Ci
end
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
def running_builds_for_shared_runners
......
......@@ -3,8 +3,8 @@ class GitHooksService
attr_accessor :oldrev, :newrev, :ref
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
def execute(user, project, oldrev, newrev, ref)
@project = project
@user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev
@newrev = newrev
......@@ -26,7 +26,7 @@ class GitHooksService
private
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)
end
end
......@@ -120,7 +120,7 @@ class GitOperationService
def with_hooks(ref, newrev, oldrev)
GitHooksService.new.execute(
user,
repository.path_to_repo,
repository.project,
oldrev,
newrev,
ref) do |service|
......
module Groups
class DestroyService < Groups::BaseService
def async_execute
# Soft delete via paranoia gem
group.destroy
group.soft_delete_without_removing_associations
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}")
end
......@@ -10,7 +9,7 @@ module Groups
def execute
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.
# Skip repository removal because we remove directory with namespace
# that contain all these repositories
......
......@@ -95,11 +95,12 @@ module Projects
end
# Requires UnZip at least 6.00 Info-ZIP.
# -qq be (very) quiet
# -n never overwrite existing files
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_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'
end
end
......
......@@ -35,7 +35,7 @@ module Users
Groups::DestroyService.new(group, current_user).execute
end
user.personal_projects.with_deleted.each do |project|
user.personal_projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
......
......@@ -11,6 +11,8 @@
%meta{ property: 'og:title', content: page_title }
%meta{ property: 'og:description', content: page_description }
%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 }
-# Twitter Card - https://dev.twitter.com/cards/types/summary
......
.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
= 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
......
.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
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do
......
.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
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
......
.nav-sidebar
- 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
= 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
......
......@@ -8,27 +8,28 @@
= render "head"
%div{ class: container_class }
.row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
.tree-holder
.nav-block
.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
= 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)
= 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 _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.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")
= 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'
......
......@@ -19,6 +19,7 @@
- if plain
= link_text
- else
= add_diff_note_button(line_code, diff_file.position(line), type)
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- discussion = line_discussions.try(:first)
- if discussion && discussion.resolvable? && !plain
......@@ -29,7 +30,7 @@
= link_text
- else
%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
%pre= line.text
- else
......
/ Side-by-side diff view
.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- diff_file.parallel_diff_lines.each do |line|
......@@ -18,11 +19,12 @@
- left_line_code = diff_file.line_code(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 } }
= add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable?
%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
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
......@@ -38,11 +40,12 @@
- right_line_code = diff_file.line_code(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 } }
= add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable?
%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
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
......
......@@ -36,6 +36,7 @@
&nbsp;
- issue.labels.each do |label|
= link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.weight
%span.issuable-weight
&nbsp;
......
......@@ -18,6 +18,9 @@
= 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?
%div{ class: container_class }
.top-area
......@@ -33,4 +36,5 @@
.merge-requests-holder
= render 'merge_requests'
- 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 @@
%li
= 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)})
%l
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
......
......@@ -13,7 +13,7 @@
- if projects.any?
%ul.projects-list
- 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,
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
......
......@@ -45,7 +45,7 @@ class StuckCiJobsWorker
def search(status, timeout)
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)
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)
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)
unless Rails.env.test?
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
warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell."
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."
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
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
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
and in the **Value** field paste the content of your _private_ key that you
created earlier.
following **Settings > Pipelines** and look for the "Secret Variables" section.
As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the
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
are not being targeted by a man-in-the-middle attack. To do this, add another
......
......@@ -55,6 +55,7 @@
- [Polymorphic Associations](polymorphic_associations.md)
- [Single Table Inheritance](single_table_inheritance.md)
- [Background Migrations](background_migrations.md)
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
## 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
end
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'
end
end
......@@ -48,7 +48,8 @@ module API
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?
end
......
require 'declarative_policy'
require_dependency 'declarative_policy'
module API
# Projects API
......
......@@ -30,7 +30,8 @@ module Ci
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?
end
......
......@@ -29,6 +29,11 @@ module Gitlab
paths = Array(paths)
RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level)
end
def revert_renames
RenameProjects.new([], self).revert_renames
RenameNamespaces.new([], self).revert_renames
end
end
end
end
......
......@@ -6,7 +6,10 @@ module Gitlab
attr_reader :paths, :migration
delegate :update_column_in_batches,
:execute,
:replace_sql,
:quote_string,
:say,
to: :migration
def initialize(paths, migration)
......@@ -26,24 +29,45 @@ module Gitlab
new_path = rename_path(namespace_path, old_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
new_path = new_full_path.split('/').last
routable.class.where(id: routable)
.update_all(path: new_path)
rename_routes(old_full_path, new_full_path)
[old_full_path, new_full_path]
end
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],
old_full_path,
new_full_path)
update_column_in_batches(:routes, :path, replace_statement) do |table, query|
path_or_children = table[:path].matches_any([old_full_path, "#{old_full_path}/%"])
query.where(path_or_children)
end
update = Arel::UpdateManager.new(ActiveRecord::Base)
.table(routes)
.set([[routes[:path], replace_statement]])
.where(Arel::Nodes::SqlLiteral.new(filter))
execute(update.to_sql)
end
def rename_path(namespace_path, path_was)
......@@ -86,32 +110,74 @@ module Gitlab
def move_folders(directory, old_relative_path, new_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)
FileUtils.mv(old_path, new_path)
end
def remove_cached_html_for_projects(project_ids)
update_column_in_batches(:projects, :description_html, nil) do |table, query|
query.where(table[:id].in(project_ids))
end
update_column_in_batches(:issues, :description_html, nil) do |table, query|
query.where(table[:project_id].in(project_ids))
project_ids.each do |project_id|
update_column_in_batches(:projects, :description_html, nil) do |table, query|
query.where(table[:id].eq(project_id))
end
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
update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
query.where(table[:target_project_id].in(project_ids))
def track_rename(type, old_path, new_path)
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
say "tracked rename: #{key}: #{old_path} -> #{new_path}"
end
update_column_in_batches(:notes, :note_html, nil) do |table, query|
query.where(table[:project_id].in(project_ids))
def reverts_for_type(type)
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
update_column_in_batches(:milestones, :description_html, nil) do |table, query|
query.where(table[:project_id].in(project_ids))
end
def redis_key_for_type(type)
"rename:#{migration.name}:#{type}"
end
def file_storage?
......
......@@ -26,6 +26,12 @@ module Gitlab
def rename_namespace(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_uploads(old_full_path, new_full_path)
move_pages(old_full_path, new_full_path)
......@@ -33,6 +39,23 @@ module Gitlab
remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id))
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)
MigrationClasses::User.where(username: old_username)
.update_all(username: new_username)
......
......@@ -16,12 +16,37 @@ module Gitlab
def rename_project(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}.wiki", "#{new_full_path}.wiki")
move_uploads(old_full_path, new_full_path)
move_pages(old_full_path, new_full_path)
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)
unless gitlab_shell.mv_repository(project.repository_storage_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
CommandError = Class.new(StandardError)
class << self
include Gitlab::EncodingHelper
def ref_name(ref)
ref.sub(/\Arefs\/(tags|heads)\//, '')
encode! ref.sub(/\Arefs\/(tags|heads)\//, '')
end
def branch_name(ref)
......
......@@ -4,9 +4,10 @@ module Gitlab
GL_PROTOCOL = 'web'.freeze
attr_reader :name, :repo_path, :path
def initialize(name, repo_path)
def initialize(name, project)
@name = name
@repo_path = repo_path
@project = project
@repo_path = project.repository.path
@path = File.join(repo_path.strip, 'hooks', name)
end
......@@ -38,7 +39,8 @@ module Gitlab
vars = {
'GL_ID' => gl_id,
'PWD' => repo_path,
'GL_PROTOCOL' => GL_PROTOCOL
'GL_PROTOCOL' => GL_PROTOCOL,
'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false)
}
options = {
......
......@@ -113,9 +113,7 @@ module Gitlab
def local_branches(sort_by: nil)
gitaly_migrate(:local_branches) do |is_enabled|
if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by).map do |gitaly_branch|
Gitlab::Git::Branch.new(self, gitaly_branch.name, gitaly_branch)
end
gitaly_ref_client.local_branches(sort_by: sort_by)
else
branches(filter: :local, sort_by: sort_by)
end
......
module Gitlab
module GitalyClient
class Ref
include Gitlab::EncodingHelper
# 'repository' is a Gitlab::Git::Repository
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
@storage = repository.storage
end
......@@ -16,13 +19,13 @@ module Gitlab
def branch_names
request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
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
def tag_names
request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo)
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
def find_ref_name(commit_id, ref_prefix)
......@@ -51,20 +54,28 @@ module Gitlab
private
def consume_refs_response(response, prefix:)
response.flat_map do |r|
r.names.map { |name| name.sub(/\A#{Regexp.escape(prefix)}/, '') }
end
def consume_refs_response(response)
response.flat_map { |message| message.names.map { |name| yield(name) } }
end
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)
raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value
enum_value
end
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
......
......@@ -20,7 +20,8 @@ module Gitlab
counts: {
boards: Board.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_triggers: ::Ci::Trigger.count,
ci_pipeline_schedules: ::Ci::PipelineSchedule.count,
......
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Huang Tao <htve@outlook.com>, 2017. #zanata
# Lin Jen-Shin <anonymous@domain.com>, 2017.
# Hazel Yang <anonymous@domain.com>, 2017.
# TzeKei Lee <anonymous@domain.com>, 2017.
# Jerry Ho <a29988122@gmail.com>, 2017.
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2017-05-04 19:24-0500\n"
"Last-Translator: HuangTao <htve@outlook.com>, 2017\n"
"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/751"
"77/zh_TW/)\n"
"POT-Creation-Date: 2017-06-15 21:59-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"PO-Revision-Date: 2017-06-28 11:13-0400\n"
"Last-Translator: Huang Tao <htve@outlook.com>\n"
"Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n"
"Language: zh-TW\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=1; plural=0\n"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} 在 %{commit_timeago} 送交"
msgid "About auto deploy"
msgstr "關於自動部署"
msgid "Active"
msgstr "啟用"
msgid "Activity"
msgstr "活動"
msgid "Add Changelog"
msgstr "新增更新日誌"
msgid "Add Contribution guide"
msgstr "新增協作指南"
msgid "Add License"
msgstr "新增授權條款"
msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr "請先新增 SSH 金鑰到您的個人帳號,才能使用 SSH 來上傳 (push) 或下載 (pull) 。"
msgid "Add new directory"
msgstr "新增目錄"
msgid "Archived project! Repository is read-only"
msgstr "此專案已封存!檔案庫 (repository) 為唯讀狀態"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "確定要刪除此流水線 (pipeline) 排程嗎?"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放檔案到此處或者 %{upload_link}"
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "分支 (branch) "
msgid ""
"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, "
"choose a GitLab CI Yaml template and commit your changes. "
"%{link_to_autodeploy_doc}"
msgstr ""
"已建立分支 (branch) <strong>%{branch_name}</strong> 。如要設定自動部署, 請選擇合適的 GitLab CI "
"Yaml 模板,然後記得要送交 (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n"
msgid "Branches"
msgstr "分支 (branch) "
msgid "Browse files"
msgstr "瀏覽檔案"
msgid "ByAuthor|by"
msgstr "作者:"
msgstr "作者:"
msgid "CI configuration"
msgstr "CI 組態"
msgid "Cancel"
msgstr ""
msgstr "取消"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑選到分支 (branch) "
msgid "ChangeTypeActionLabel|Revert in branch"
msgstr "還原分支 (branch) "
msgid "ChangeTypeAction|Cherry-pick"
msgstr "挑選"
msgid "ChangeTypeAction|Revert"
msgstr "還原"
msgid "Changelog"
msgstr "更新日誌"
msgid "Charts"
msgstr "統計圖"
msgid "Cherry-pick this commit"
msgstr "挑選此更動記錄 (commit) "
msgid "Cherry-pick this merge request"
msgstr "挑選此合併請求 (merge request) "
msgid "CiStatusLabel|canceled"
msgstr "已取消"
msgid "CiStatusLabel|created"
msgstr "已建立"
msgid "CiStatusLabel|failed"
msgstr "失敗"
msgid "CiStatusLabel|manual action"
msgstr "手動操作"
msgid "CiStatusLabel|passed"
msgstr "已通過"
msgid "CiStatusLabel|passed with warnings"
msgstr "通過,但有警告訊息"
msgid "CiStatusLabel|pending"
msgstr "等待中"
msgid "CiStatusLabel|skipped"
msgstr "已跳過"
msgid "CiStatusLabel|waiting for manual action"
msgstr "等待手動操作"
msgid "CiStatusText|blocked"
msgstr "已阻擋"
msgid "CiStatusText|canceled"
msgstr "已取消"
msgid "CiStatusText|created"
msgstr "已建立"
msgid "CiStatusText|failed"
msgstr "失敗"
msgid "CiStatusText|manual"
msgstr "手動操作"
msgid "CiStatusText|passed"
msgstr "已通過"
msgid "CiStatusText|pending"
msgstr "等待中"
msgid "CiStatusText|skipped"
msgstr "已跳過"
msgid "CiStatus|running"
msgstr "執行中"
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "送交"
msgstr[0] "更動記錄 (commit) "
msgid "Commit message"
msgstr "更動說明 (commit) "
msgid "CommitBoxTitle|Commit"
msgstr "送交"
msgid "CommitMessage|Add %{file_name}"
msgstr "建立 %{file_name}"
msgid "Commits"
msgstr "更動記錄 (commit) "
msgid "Commits|History"
msgstr "過去更動 (commit) "
msgid "Committed by"
msgstr "送交者為 "
msgid "Compare"
msgstr "比較"
msgid "Contribution guide"
msgstr "協作指南"
msgid "Contributors"
msgstr "協作者"
msgid "Copy URL to clipboard"
msgstr "複製網址到剪貼簿"
msgid "Copy commit SHA to clipboard"
msgstr "複製更動記錄 (commit) 的 SHA 值到剪貼簿"
msgid "Create New Directory"
msgstr "建立新目錄"
msgid "Create directory"
msgstr "建立目錄"
msgid "Create empty bare repository"
msgstr "建立一個新的 bare repository"
msgid "Create merge request"
msgstr "發出合併請求 (merge request) "
msgid "Create new..."
msgstr "建立..."
msgid "CreateNewFork|Fork"
msgstr "分支 (fork) "
msgid "CreateTag|Tag"
msgstr "建立標籤"
msgid "Cron Timezone"
msgstr "Cron 時區"
msgid "Cron syntax"
msgstr "Cron 語法"
msgid "Custom notification events"
msgstr "自訂事件通知"
msgid ""
"Custom notification levels are the same as participating levels. With custom "
"notification levels you will also receive notifications for select events. "
"To find out more, check out %{notification_link}."
msgstr ""
"自訂通知層級相當於參與度設定。使用自訂通知層級,您可以只收到特定的事件通知。請參照 %{notification_link} 以獲得更多訊息。"
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr "週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"
msgid "Cycle Analytics"
msgstr "週期分析"
msgid ""
"Cycle Analytics gives an overview of how much time it takes to go from idea "
"to production in your project."
msgstr "週期分析讓您可以有效的釐清專案從發想到產品推出所花的時間長短。"
msgid "CycleAnalyticsStage|Code"
msgstr "程式開發"
msgid "CycleAnalyticsStage|Issue"
msgstr "議題"
msgstr "議題 (issue) "
msgid "CycleAnalyticsStage|Plan"
msgstr "計劃"
msgid "CycleAnalyticsStage|Production"
msgstr "上線"
msgstr "營運"
msgid "CycleAnalyticsStage|Review"
msgstr "複閱"
msgid "CycleAnalyticsStage|Staging"
msgstr "預備"
msgstr "試營運"
msgid "CycleAnalyticsStage|Test"
msgstr "測試"
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 語法自訂排程"
msgid "Delete"
msgstr ""
msgstr "刪除"
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
msgid "Description"
msgstr ""
msgstr "描述"
msgid "Directory name"
msgstr "目錄名稱"
msgid "Don't show again"
msgstr "不再顯示"
msgid "Download"
msgstr "下載"
msgid "Download tar"
msgstr "下載 tar"
msgid "Download tar.bz2"
msgstr "下載 tar.bz2"
msgid "Download tar.gz"
msgstr "下載 tar.gz"
msgid "Download zip"
msgstr "下載 zip"
msgid "DownloadArtifacts|Download"
msgstr "下載"
msgid "DownloadCommit|Email Patches"
msgstr "電子郵件修補檔案 (patch)"
msgid "DownloadCommit|Plain Diff"
msgstr "差異檔 (diff)"
msgid "DownloadSource|Download"
msgstr "下載原始碼"
msgid "Edit"
msgstr ""
msgstr "編輯"
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
msgstr "編輯 %{id} 流水線 (pipeline) 排程"
msgid "Every day (at 4:00am)"
msgstr "每日執行(淩晨四點)"
msgid "Every month (on the 1st at 4:00am)"
msgstr "每月執行(每月一日淩晨四點)"
msgid "Every week (Sundays at 4:00am)"
msgstr "每週執行(週日淩晨 四點)"
msgid "Failed to change the owner"
msgstr ""
msgstr "無法變更所有權"
msgid "Failed to remove the pipeline schedule"
msgstr ""
msgstr "無法刪除流水線 (pipeline) 排程"
msgid "Filter"
msgstr ""
msgid "Files"
msgstr "檔案"
msgid "Find by path"
msgstr "以路徑搜尋"
msgid "Find file"
msgstr "搜尋檔案"
msgid "FirstPushedBy|First"
msgstr "首次推送"
msgstr "首次推送 (push) "
msgid "FirstPushedBy|pushed by"
msgstr "推送者:"
msgstr "推送者 (push) :"
msgid "Fork"
msgid_plural "Forks"
msgstr[0] "分支 (fork) "
msgid "ForkedFromProjectPath|Forked from"
msgstr "分支 (fork) 自"
msgid "From issue creation until deploy to production"
msgstr "從議題建立至線上部署"
msgstr "從議題 (issue) 建立直到部署至營運環境"
msgid "From merge request merge until deploy to production"
msgstr "從請求被合併後至線上部署"
msgstr "從請求被合併後 (merge request merged) 直到部署至營運環境"
msgid "Go to your fork"
msgstr "前往您的分支 (fork) "
msgid "GoToYourFork|Fork"
msgstr "前往您的分支 (fork) "
msgid "Home"
msgstr "首頁"
msgid "Housekeeping successfully started"
msgstr "已開始維護"
msgid "Import repository"
msgstr "匯入檔案庫 (repository)"
msgid "Interval Pattern"
msgstr ""
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分析簡介"
msgid "LFSStatus|Disabled"
msgstr "停用"
msgid "LFSStatus|Enabled"
msgstr "啟用"
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最 %d 天"
msgstr[0] "最 %d 天"
msgid "Last Pipeline"
msgstr ""
msgstr "最新流水線 (pipeline) "
msgid "Last Update"
msgstr "最後更新"
msgid "Last commit"
msgstr "最後更動記錄 (commit) "
msgid "Learn more in the"
msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "流水線 (pipeline) 排程說明文件"
msgid "Leave group"
msgstr "退出群組"
msgid "Leave project"
msgstr "退出專案"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多顯示 %d 個事件"
msgstr[0] "限制最多顯示 %d 個事件"
msgid "Median"
msgstr "中位數"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新增 SSH 金鑰"
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新議題"
msgstr[0] "建立議題 (issue) "
msgid "New Pipeline Schedule"
msgstr ""
msgstr "建立流水線 (pipeline) 排程"
msgid "New branch"
msgstr "新分支 (branch) "
msgid "New directory"
msgstr "新增目錄"
msgid "New file"
msgstr "新增檔案"
msgid "New issue"
msgstr "新增議題 (issue) "
msgid "New merge request"
msgstr "新增合併請求 (merge request) "
msgid "New schedule"
msgstr "新增排程"
msgid "New snippet"
msgstr "新文字片段"
msgid "New tag"
msgstr "新增標籤"
msgid "No repository"
msgstr "找不到檔案庫 (repository)"
msgid "No schedules"
msgstr ""
msgstr "沒有排程"
msgid "Not available"
msgstr "無法使用"
......@@ -130,135 +462,502 @@ msgstr "無法使用"
msgid "Not enough data"
msgstr "資料不足"
msgid "Notification events"
msgstr "事件通知"
msgid "NotificationEvent|Close issue"
msgstr "關閉議題 (issue) "
msgid "NotificationEvent|Close merge request"
msgstr "關閉合併請求 (merge request) "
msgid "NotificationEvent|Failed pipeline"
msgstr "流水線 (pipeline) 失敗"
msgid "NotificationEvent|Merge merge request"
msgstr "合併請求 (merge request) 被合併"
msgid "NotificationEvent|New issue"
msgstr "新增議題 (issue) "
msgid "NotificationEvent|New merge request"
msgstr "新增合併請求 (merge request) "
msgid "NotificationEvent|New note"
msgstr "新增評論"
msgid "NotificationEvent|Reassign issue"
msgstr "重新指派議題 (issue) "
msgid "NotificationEvent|Reassign merge request"
msgstr "重新指派合併請求 (merge request) "
msgid "NotificationEvent|Reopen issue"
msgstr "重啟議題 (issue)"
msgid "NotificationEvent|Successful pipeline"
msgstr "流水線 (pipeline) 成功完成"
msgid "NotificationLevel|Custom"
msgstr "自訂"
msgid "NotificationLevel|Disabled"
msgstr "停用"
msgid "NotificationLevel|Global"
msgstr "全域"
msgid "NotificationLevel|On mention"
msgstr "提及"
msgid "NotificationLevel|Participate"
msgstr "參與"
msgid "NotificationLevel|Watch"
msgstr "關注"
msgid "OfSearchInADropdown|Filter"
msgstr "篩選"
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
msgid "Options"
msgstr "選項"
msgid "Owner"
msgstr ""
msgstr "所有權"
msgid "Pipeline"
msgstr "流水線 (pipeline) "
msgid "Pipeline Health"
msgstr "流水線健康指標"
msgstr "流水線 (pipeline) 健康指數"
msgid "Pipeline Schedule"
msgstr ""
msgstr "流水線 (pipeline) 排程"
msgid "Pipeline Schedules"
msgstr ""
msgstr "流水線 (pipeline) 排程"
msgid "PipelineSchedules|Activated"
msgstr ""
msgstr "是否啟用"
msgid "PipelineSchedules|Active"
msgstr ""
msgstr "已啟用"
msgid "PipelineSchedules|All"
msgstr ""
msgstr "所有"
msgid "PipelineSchedules|Inactive"
msgstr ""
msgstr "未啟用"
msgid "PipelineSchedules|Next Run"
msgstr ""
msgstr "下次執行時間"
msgid "PipelineSchedules|None"
msgstr ""
msgstr ""
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr ""
msgstr "請簡單說明此流水線 (pipeline) "
msgid "PipelineSchedules|Take ownership"
msgstr ""
msgstr "取得所有權"
msgid "PipelineSchedules|Target"
msgstr ""
msgstr "目標"
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "自訂"
msgid "Pipeline|with stage"
msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
msgid "Project '%{project_name}' queued for deletion."
msgstr "專案 '%{project_name}' 已加入刪除佇列。"
msgid "Project '%{project_name}' was successfully created."
msgstr "專案 '%{project_name}' 建立完成。"
msgid "Project '%{project_name}' was successfully updated."
msgstr "專案 '%{project_name}' 更新完成。"
msgid "Project '%{project_name}' will be deleted."
msgstr "專案 '%{project_name}' 將被刪除。"
msgid "Project access must be granted explicitly to each user."
msgstr "專案權限必須一一指派給每個使用者。"
msgid "Project export could not be deleted."
msgstr "匯出的專案無法被刪除。"
msgid "Project export has been deleted."
msgstr "匯出的專案已被刪除。"
msgid ""
"Project export link has expired. Please generate a new export from your "
"project settings."
msgstr "專案的匯出連結已失效。請到專案設定中產生新的連結。"
msgid "Project export started. A download link will be sent by email."
msgstr "專案導出已開始。完成後下載連結會送到您的信箱。"
msgid "Project home"
msgstr "專案首頁"
msgid "ProjectFeature|Disabled"
msgstr "停用"
msgid "ProjectFeature|Everyone with access"
msgstr "任何人都可存取"
msgid "ProjectFeature|Only team members"
msgstr "只有團隊成員可以存取"
msgid "ProjectFileTree|Name"
msgstr "名稱"
msgid "ProjectLastActivity|Never"
msgstr "從未"
msgid "ProjectLifecycle|Stage"
msgstr "專案生命週期"
msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
msgid "Read more"
msgstr "了解更多"
msgstr "瞭解更多"
msgid "Readme"
msgstr "說明檔"
msgid "RefSwitcher|Branches"
msgstr "分支 (branch) "
msgid "RefSwitcher|Tags"
msgstr "標籤"
msgid "Related Commits"
msgstr "相關的送交"
msgstr "相關的更動記錄 (commit) "
msgid "Related Deployed Jobs"
msgstr "相關的部署作業"
msgid "Related Issues"
msgstr "相關的議題"
msgstr "相關的議題 (issue) "
msgid "Related Jobs"
msgstr "相關的作業"
msgid "Related Merge Requests"
msgstr "相關的合併請求"
msgstr "相關的合併請求 (merge request) "
msgid "Related Merged Requests"
msgstr "相關已合併的請求"
msgid "Remind later"
msgstr "稍後提醒"
msgid "Remove project"
msgstr "刪除專案"
msgid "Request Access"
msgstr "申請權限"
msgid "Revert this commit"
msgstr "還原此更動記錄 (commit)"
msgid "Revert this merge request"
msgstr "還原此合併請求 (merge request) "
msgid "Save pipeline schedule"
msgstr ""
msgstr "保存流水線 (pipeline) 排程"
msgid "Schedule a new pipeline"
msgstr ""
msgstr "建立流水線 (pipeline) 排程"
msgid "Scheduling Pipelines"
msgstr "流水線 (pipeline) 計劃"
msgid "Search branches and tags"
msgstr "搜尋分支 (branch) 和標籤"
msgid "Select Archive Format"
msgstr "選擇下載格式"
msgid "Select a timezone"
msgstr ""
msgstr "選擇時區"
msgid "Select target branch"
msgstr ""
msgstr "選擇目標分支 (branch) "
msgid "Set a password on your account to pull or push via %{protocol}"
msgstr "請先設定密碼,才能使用 %{protocol} 來上傳 (push) 或下載 (pull) 。"
msgid "Set up CI"
msgstr "設定 CI"
msgid "Set up Koding"
msgstr "設定 Koding"
msgid "Set up auto deploy"
msgstr "設定自動部署"
msgid "SetPasswordToCloneLink|set a password"
msgstr "設定密碼"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
msgid "Source code"
msgstr "原始碼"
msgid "StarProject|Star"
msgstr "收藏"
msgid "Start a %{new_merge_request} with these changes"
msgstr "以這些改動建立一個新的 %{new_merge_request} "
msgid "Switch branch/tag"
msgstr "切換分支 (branch) 或標籤"
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "標籤"
msgid "Tags"
msgstr "標籤"
msgid "Target Branch"
msgstr ""
msgstr "目標分支 (branch) "
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"
msgid ""
"The coding stage shows the time from the first commit to creating the merge "
"request. The data will automatically be added here once you create your "
"first merge request."
msgstr ""
"程式開發階段顯示從第一次更動記錄 (commit) 到建立合併請求 (merge request) 的時間。建立第一個合併請求後,資料將自動填入。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "與該階段相關的事件。"
msgstr "該階段中的相關事件集合。"
msgid "The fork relationship has been removed."
msgstr "分支與主幹間的關聯 (fork relationship) 已被刪除。"
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"
msgid ""
"The issue stage shows the time it takes from creating an issue to assigning "
"the issue to a milestone, or add the issue to a list on your Issue Board. "
"Begin creating issues to see data for this stage."
msgstr ""
"議題 (issue) 階段顯示從議題建立到設定里程碑所花的時間,或是議題被分類到議題看板 (issue board) "
"中所花的時間。建立第一個議題後,資料將自動填入。"
msgid "The phase of the development lifecycle."
msgstr "專案開發生命週期的各個階段。"
msgstr "專案開發週期的各個階段。"
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "計劃階段所顯示的是議題被排程後至第一個送交被推送的時間。一旦完成(或執行)首次的推送,資料將自動填入。"
msgid ""
"The pipelines schedule runs pipelines in the future, repeatedly, for "
"specific branches or tags. Those scheduled pipelines will inherit limited "
"project access based on their associated user."
msgstr ""
"在指定了特定分支 (branch) 或標籤後,此處的流水線 (pipeline) 排程會不斷地重複執行。\n"
"流水線排程的存取權限與專案本身相同。"
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr "上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"
msgid ""
"The planning stage shows the time from the previous step to pushing your "
"first commit. This time will be added automatically once you push your first "
"commit."
msgstr "計劃階段顯示從更動記錄 (commit) 被排程至第一個推送的時間。第一次推送之後,資料將自動填入。"
msgid ""
"The production stage shows the total time it takes between creating an issue "
"and deploying the code to production. The data will be automatically added "
"once you have completed the full idea to production cycle."
msgstr "營運階段顯示從建立議題 (issue) 到部署程式上線所花的時間。完成從發想到上線的完整開發週期後,資料將自動填入。"
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"
msgid "The project can be accessed by any logged in user."
msgstr "本專案可讓任何已登入的使用者存取"
msgid "The project can be accessed without any authentication."
msgstr "本專案可讓任何人存取"
msgid "The repository for this project does not exist."
msgstr "本專案沒有檔案庫 (repository) "
msgid ""
"The review stage shows the time from creating the merge request to merging "
"it. The data will automatically be added after you merge your first merge "
"request."
msgstr ""
"複閱階段顯示從合併請求 (merge request) 建立後至被合併的時間。當建立第一個合併請求 (merge request) 後,資料將自動填入。"
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr "預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"
msgid ""
"The staging stage shows the time between merging the MR and deploying code "
"to the production environment. The data will be automatically added once you "
"deploy to production for the first time."
msgstr "試營運段顯示從合併請求 (merge request) 被合併後至部署營運的時間。當第一次部署營運後,資料將自動填入"
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"
msgid ""
"The testing stage shows the time GitLab CI takes to run every pipeline for "
"the related merge request. The data will automatically be added after your "
"first pipeline finishes running."
msgstr ""
"測試階段顯示相關合併請求 (merge request) 的流水線 (pipeline) 所花的時間。當第一個流水線 (pipeline) "
"執行完畢後,資料將自動填入。"
msgid "The time taken by each data entry gathered by that stage."
msgstr "每筆該階段相關資料所花的時間。"
msgstr "該階段中每一個資料項目所花的時間。"
msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
msgid ""
"The value lying at the midpoint of a series of observed values. E.g., "
"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 ="
" 6."
msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"
msgid ""
"This means you can not push code until you create an empty repository or "
"import existing one."
msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個現存的檔案庫之前,您將無法上傳更新 (push) 。"
msgid "Time before an issue gets scheduled"
msgstr "議題等待排程的時間"
msgstr "議題 (issue) 被列入日程表的時間"
msgid "Time before an issue starts implementation"
msgstr "議題等待開始實作的時間"
msgstr "議題 (issue) 等待開始實作的時間"
msgid "Time between merge request creation and merge/close"
msgstr "合併請求被合併或是關閉的時間"
msgstr "合併請求 (merge request) 從建立到被合併或是關閉的時間"
msgid "Time until first merge request"
msgstr "第一個合併請求被建立前的時間"
msgstr "第一個合併請求 (merge request) 被建立前的時間"
msgid "Timeago|%s days ago"
msgstr " %s 天前"
msgid "Timeago|%s days remaining"
msgstr "剩下 %s 天"
msgid "Timeago|%s hours remaining"
msgstr "剩下 %s 小時"
msgid "Timeago|%s minutes ago"
msgstr " %s 分鐘前"
msgid "Timeago|%s minutes remaining"
msgstr "剩下 %s 分鐘"
msgid "Timeago|%s months ago"
msgstr " %s 個月前"
msgid "Timeago|%s months remaining"
msgstr "剩下 %s 月"
msgid "Timeago|%s seconds remaining"
msgstr "剩下 %s 秒"
msgid "Timeago|%s weeks ago"
msgstr " %s 週前"
msgid "Timeago|%s weeks remaining"
msgstr "剩下 %s 週"
msgid "Timeago|%s years ago"
msgstr " %s 年前"
msgid "Timeago|%s years remaining"
msgstr "剩下 %s 年"
msgid "Timeago|1 day remaining"
msgstr "剩下 1 天"
msgid "Timeago|1 hour remaining"
msgstr "剩下 1 小時"
msgid "Timeago|1 minute remaining"
msgstr "剩下 1 分鐘"
msgid "Timeago|1 month remaining"
msgstr "剩下 1 個月"
msgid "Timeago|1 week remaining"
msgstr "剩下 1 週"
msgid "Timeago|1 year remaining"
msgstr "剩下 1 年"
msgid "Timeago|Past due"
msgstr "逾期"
msgid "Timeago|a day ago"
msgstr " 1 天前"
msgid "Timeago|a month ago"
msgstr " 1 個月前"
msgid "Timeago|a week ago"
msgstr " 1 週前"
msgid "Timeago|a while"
msgstr "剛剛"
msgid "Timeago|a year ago"
msgstr " 1 年前"
msgid "Timeago|about %s hours ago"
msgstr "約 %s 小時前"
msgid "Timeago|about a minute ago"
msgstr "約 1 分鐘前"
msgid "Timeago|about an hour ago"
msgstr "約 1 小時前"
msgid "Timeago|in %s days"
msgstr " %s 天後"
msgid "Timeago|in %s hours"
msgstr " %s 小時後"
msgid "Timeago|in %s minutes"
msgstr " %s 分鐘後"
msgid "Timeago|in %s months"
msgstr " %s 個月後"
msgid "Timeago|in %s seconds"
msgstr " %s 秒後"
msgid "Timeago|in %s weeks"
msgstr " %s 週後"
msgid "Timeago|in %s years"
msgstr " %s 年後"
msgid "Timeago|in 1 day"
msgstr " 1 天後"
msgid "Timeago|in 1 hour"
msgstr " 1 小時後"
msgid "Timeago|in 1 minute"
msgstr " 1 分鐘後"
msgid "Timeago|in 1 month"
msgstr " 1 個月後"
msgid "Timeago|in 1 week"
msgstr " 1 週後"
msgid "Timeago|in 1 year"
msgstr " 1 年後"
msgid "Timeago|less than a minute ago"
msgstr "不到 1 分鐘前"
msgid "Time|hr"
msgid_plural "Time|hrs"
......@@ -275,7 +974,28 @@ msgid "Total Time"
msgstr "總時間"
msgid "Total test time for all commits/merges"
msgstr "所有送交和合併的總測試時間"
msgstr "合併 (merge) 與更動記錄 (commit) 的總測試時間"
msgid "Unstar"
msgstr "取消收藏"
msgid "Upload New File"
msgstr "上傳新檔案"
msgid "Upload file"
msgstr "上傳檔案"
msgid "Use your global notification setting"
msgstr "使用全域通知設定"
msgid "VisibilityLevel|Internal"
msgstr "內部"
msgid "VisibilityLevel|Private"
msgstr "私有"
msgid "VisibilityLevel|Public"
msgstr "公開"
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權限不足。如需查看相關資料,請向管理員申請權限。"
......@@ -283,12 +1003,85 @@ msgstr "權限不足。如需查看相關資料,請向管理員申請權限。
msgid "We don't have enough data to show this stage."
msgstr "因該階段的資料不足而無法顯示相關資訊"
msgid "You have reached your project limit"
msgid "Withdraw Access Request"
msgstr "取消權限申請"
msgid ""
"You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
msgstr ""
"即將要刪除 %{project_name_with_namespace}。\n"
"被刪除的專案完全無法救回來喔!\n"
"真的「100%確定」要這麼做嗎?"
msgid ""
"You are going to remove the fork relationship to source project "
"%{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr ""
"將要刪除本分支專案與主幹的所有關聯 (fork relationship) 。 %{forked_from_project} "
"真的「100%確定」要這麼做嗎?"
msgid ""
"You are going to transfer %{project_name_with_namespace} to another owner. "
"Are you ABSOLUTELY sure?"
msgstr "將要把 %{project_name_with_namespace} 的所有權轉移給另一個人。真的「100%確定」要這麼做嗎?"
msgid "You can only add files when you are on a branch"
msgstr "只能在分支 (branch) 上建立檔案"
msgid "You have reached your project limit"
msgstr "您已達到專案數量限制"
msgid "You must sign in to star a project"
msgstr "必須登入才能收藏專案"
msgid "You need permission."
msgstr "您需要相關的權限。"
msgstr "需要權限才能這麼做。"
msgid "You will not get any notifications via email"
msgstr "不會收到任何通知郵件"
msgid "You will only receive notifications for the events you choose"
msgstr "只接收您選擇的事件通知"
msgid ""
"You will only receive notifications for threads you have participated in"
msgstr "只接收參與主題的通知"
msgid "You will receive notifications for any activity"
msgstr "接收所有活動的通知"
msgid ""
"You will receive notifications only for comments in which you were "
"@mentioned"
msgstr "只接收評論中提及(@)您的通知"
msgid ""
"You won't be able to pull or push project code via %{protocol} until you "
"%{set_password_link} on your account"
msgstr ""
"在帳號上 %{set_password_link} 之前, 將無法使用 %{protocol} 上傳 (push) 或下載 (pull) 程式碼。"
msgid ""
"You won't be able to pull or push project code via SSH until you "
"%{add_ssh_key_link} to your profile"
msgstr "在個人帳號中 %{add_ssh_key_link} 之前, 將無法使用 SSH 上傳 (push) 或下載 (pull) 程式碼。"
msgid "Your name"
msgstr "您的名字"
msgid "day"
msgid_plural "days"
msgstr[0] "天"
msgid "new merge request"
msgstr "建立合併請求"
msgid "notification emails"
msgstr "通知信"
msgid "parent"
msgid_plural "parents"
msgstr[0] "上層"
......@@ -13,6 +13,31 @@ describe AbuseReportsController do
sign_in(reporter)
end
describe 'GET new' do
context 'when the user has already been deleted' do
it 'redirects the reporter to root_path' do
user_id = user.id
user.destroy
get :new, { user_id: user_id }
expect(response).to redirect_to root_path
expect(flash[:alert]).to eq('Cannot create the abuse report. The user has been deleted.')
end
end
context 'when the user has already been blocked' do
it 'redirects the reporter to the user\'s profile' do
user.block
get :new, { user_id: user.id }
expect(response).to redirect_to user
expect(flash[:alert]).to eq('Cannot create the abuse report. This user has been blocked.')
end
end
end
describe 'POST create' do
context 'with valid attributes' do
it 'saves the abuse report' do
......
......@@ -84,4 +84,57 @@ describe Projects::PipelineSchedulesController do
end
end
end
describe 'security' do
include AccessMatchersForController
describe 'GET edit' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:master).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
it { expect { go }.to be_denied_for(:visitor) }
def go
get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
end
end
describe 'GET take_ownership' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:master).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
it { expect { go }.to be_denied_for(:visitor) }
def go
post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
end
end
describe 'PUT update' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:master).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
it { expect { go }.to be_denied_for(:visitor) }
def go
put :update, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id,
schedule: { description: 'a' }
end
end
end
end
......@@ -12,7 +12,7 @@ feature 'Abuse reports', feature: true do
click_link 'Report abuse'
fill_in 'abuse_report_message', with: 'This user send spam'
fill_in 'abuse_report_message', with: 'This user sends spam'
click_button 'Send report'
expect(page).to have_content 'Thank you for your report'
......
......@@ -317,23 +317,6 @@ feature 'Dashboard Todos' do
end
end
context 'User has a Todo in a project pending deletion' do
before do
deleted_project = create(:project, :public, pending_delete: true)
create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author)
create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done)
sign_in(user)
visit dashboard_todos_path
end
it 'shows "All done" message' do
within('.todos-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 0'
expect(page).to have_selector('.todos-all-done', count: 1)
end
end
context 'User has a Build Failed todo' do
let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) }
......
......@@ -129,7 +129,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do
before do
large_diff.find('.diff-line-num', match: :prefer_exact).hover
large_diff.find('.add-diff-note').click
large_diff.find('.add-diff-note', match: :prefer_exact).click
large_diff.find('.note-textarea').send_keys comment_text
large_diff.find_button('Comment').click
wait_for_requests
......
require 'rails_helper'
describe 'Issue Sidebar on Mobile' do
include MobileHelpers
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
sign_in(user)
end
context 'mobile sidebar on merge requests', js: true do
before do
visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
end
it_behaves_like "issue sidebar stays collapsed on mobile"
end
context 'mobile sidebar on issues', js: true do
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it_behaves_like "issue sidebar stays collapsed on mobile"
end
end
......@@ -154,20 +154,6 @@ feature 'Issue Sidebar', feature: true do
end
end
context 'as a allowed mobile user', js: true do
before do
project.team << [user, :developer]
resize_screen_xs
visit_issue(project, issue)
end
context 'mobile sidebar' do
it 'collapses the sidebar for small screens' do
expect(page).not_to have_css('aside.right-sidebar.right-sidebar-collapsed')
end
end
end
context 'as a guest' do
before do
project.team << [user, :guest]
......
......@@ -17,10 +17,48 @@ describe 'Branches', feature: true do
it 'shows all the branches' do
visit namespace_project_branches_path(project.namespace, project)
repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
repository.branches_sorted_by(:name).first(20).each do |branch|
expect(page).to have_content("#{branch.name}")
end
expect(page).to have_content("Protected branches can be managed in project settings")
end
it 'sorts the branches by name' do
visit namespace_project_branches_path(project.namespace, project)
click_button "Name" # Open sorting dropdown
click_link "Name"
sorted = repository.branches_sorted_by(:name).first(20).map do |branch|
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
end
it 'sorts the branches by last updated' do
visit namespace_project_branches_path(project.namespace, project)
click_button "Name" # Open sorting dropdown
click_link "Last updated"
sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch|
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
end
it 'sorts the branches by oldest updated' do
visit namespace_project_branches_path(project.namespace, project)
click_button "Name" # Open sorting dropdown
click_link "Oldest updated"
sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch|
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
end
it 'avoids a N+1 query in branches index' do
control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count
......
......@@ -98,6 +98,6 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
end
def project_hook_exists?(project)
Gitlab::Git::Hook.new('post-receive', project.repository.path).exists?
Gitlab::Git::Hook.new('post-receive', project).exists?
end
end
......@@ -170,6 +170,11 @@ describe SubmoduleHelper do
expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"])
end
it 'with trailing whitespace' do
result = relative_self_links('../test.git ', commit_id)
expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"])
end
it 'two levels down' do
result = relative_self_links('../../test.git', commit_id)
expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"])
......
......@@ -6,6 +6,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
before do
allow(migration).to receive(:say)
TestEnv.clean_test_path
end
def migration_namespace(namespace)
......@@ -153,6 +154,30 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
describe '#perform_rename' do
describe 'for namespaces' do
let(:namespace) { create(:namespace, path: 'the-path') }
it 'renames the path' do
subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed')
expect(namespace.reload.path).to eq('renamed')
end
it 'renames all the routes for the namespace' do
child = create(:group, path: 'child', parent: namespace)
project = create(:project, namespace: child, path: 'the-project')
other_one = create(:namespace, path: 'the-path-is-similar')
subject.perform_rename(migration_namespace(namespace), 'the-path', 'renamed')
expect(namespace.reload.route.path).to eq('renamed')
expect(child.reload.route.path).to eq('renamed/child')
expect(project.reload.route.path).to eq('renamed/child/the-project')
expect(other_one.reload.route.path).to eq('the-path-is-similar')
end
end
end
describe '#move_pages' do
it 'moves the pages directory' do
expect(subject).to receive(:move_folders)
......@@ -203,4 +228,53 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
expect(File.exist?(expected_file)).to be(true)
end
end
describe '#track_rename', redis: true do
it 'tracks a rename in redis' do
key = 'rename:FakeRenameReservedPathMigrationV1:namespace'
subject.track_rename('namespace', 'path/to/namespace', 'path/to/renamed')
old_path, new_path = [nil, nil]
Gitlab::Redis.with do |redis|
rename_info = redis.lpop(key)
old_path, new_path = JSON.parse(rename_info)
end
expect(old_path).to eq('path/to/namespace')
expect(new_path).to eq('path/to/renamed')
end
end
describe '#reverts_for_type', redis: true do
it 'yields for each tracked rename' do
subject.track_rename('project', 'old_path', 'new_path')
subject.track_rename('project', 'old_path2', 'new_path2')
subject.track_rename('namespace', 'namespace_path', 'new_namespace_path')
expect { |b| subject.reverts_for_type('project', &b) }
.to yield_successive_args(%w(old_path2 new_path2), %w(old_path new_path))
expect { |b| subject.reverts_for_type('namespace', &b) }
.to yield_with_args('namespace_path', 'new_namespace_path')
end
it 'keeps the revert in redis if it failed' do
subject.track_rename('project', 'old_path', 'new_path')
subject.reverts_for_type('project') do
raise 'whatever happens, keep going!'
end
key = 'rename:FakeRenameReservedPathMigrationV1:project'
stored_renames = nil
rename_count = 0
Gitlab::Redis.with do |redis|
stored_renames = redis.lrange(key, 0, 1)
rename_count = redis.llen(key)
end
expect(rename_count).to eq(1)
expect(JSON.parse(stored_renames.first)).to eq(%w(old_path new_path))
end
end
end
......@@ -3,9 +3,11 @@ require 'spec_helper'
describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
let(:namespace) { create(:group, name: 'the-path') }
before do
allow(migration).to receive(:say)
TestEnv.clean_test_path
end
def migration_namespace(namespace)
......@@ -137,8 +139,6 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
end
describe "#rename_namespace" do
let(:namespace) { create(:group, name: 'the-path') }
it 'renames paths & routes for the namespace' do
expect(subject).to receive(:rename_path_for_routable)
.with(namespace)
......@@ -149,11 +149,27 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
expect(namespace.reload.path).to eq('the-path0')
end
it 'tracks the rename' do
expect(subject).to receive(:track_rename)
.with('namespace', 'the-path', 'the-path0')
subject.rename_namespace(namespace)
end
it 'renames things related to the namespace' do
expect(subject).to receive(:rename_namespace_dependencies)
.with(namespace, 'the-path', 'the-path0')
subject.rename_namespace(namespace)
end
end
describe '#rename_namespace_dependencies' do
it "moves the the repository for a project in the namespace" do
create(:project, namespace: namespace, path: "the-path-project")
expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git")
subject.rename_namespace(namespace)
subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0')
expect(File.directory?(expected_repo)).to be(true)
end
......@@ -161,13 +177,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
it "moves the uploads for the namespace" do
expect(subject).to receive(:move_uploads).with("the-path", "the-path0")
subject.rename_namespace(namespace)
subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0')
end
it "moves the pages for the namespace" do
expect(subject).to receive(:move_pages).with("the-path", "the-path0")
subject.rename_namespace(namespace)
subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0')
end
it 'invalidates the markdown cache of related projects' do
......@@ -175,13 +191,13 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
expect(subject).to receive(:remove_cached_html_for_projects).with([project.id])
subject.rename_namespace(namespace)
subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0')
end
it "doesn't rename users for other namespaces" do
expect(subject).not_to receive(:rename_user)
subject.rename_namespace(namespace)
subject.rename_namespace_dependencies(namespace, 'the-path', 'the-path0')
end
it 'renames the username of a namespace for a user' do
......@@ -189,7 +205,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
expect(subject).to receive(:rename_user).with('the-path', 'the-path0')
subject.rename_namespace(user.namespace)
subject.rename_namespace_dependencies(user.namespace, 'the-path', 'the-path0')
end
end
......@@ -224,4 +240,50 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
subject.rename_namespaces(type: :child)
end
end
describe '#revert_renames', redis: true do
it 'renames the routes back to the previous values' do
project = create(:project, path: 'a-project', namespace: namespace)
subject.rename_namespace(namespace)
expect(subject).to receive(:perform_rename)
.with(
kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace),
'the-path0',
'the-path'
).and_call_original
subject.revert_renames
expect(namespace.reload.path).to eq('the-path')
expect(namespace.reload.route.path).to eq('the-path')
expect(project.reload.route.path).to eq('the-path/a-project')
end
it 'moves the repositories back to their original place' do
project = create(:project, path: 'a-project', namespace: namespace)
project.create_repository
subject.rename_namespace(namespace)
expected_path = File.join(TestEnv.repos_path, 'the-path', 'a-project.git')
expect(subject).to receive(:rename_namespace_dependencies)
.with(
kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Namespace),
'the-path0',
'the-path'
).and_call_original
subject.revert_renames
expect(File.directory?(expected_path)).to be_truthy
end
it "doesn't break when the namespace was renamed" do
subject.rename_namespace(namespace)
namespace.update_attributes!(path: 'renamed-afterwards')
expect { subject.revert_renames }.not_to raise_error
end
end
end
......@@ -3,9 +3,15 @@ require 'spec_helper'
describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
let(:project) do
create(:empty_project,
path: 'the-path',
namespace: create(:namespace, path: 'known-parent' ))
end
before do
allow(migration).to receive(:say)
TestEnv.clean_test_path
end
describe '#projects_for_paths' do
......@@ -47,12 +53,6 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
end
describe '#rename_project' do
let(:project) do
create(:empty_project,
path: 'the-path',
namespace: create(:namespace, path: 'known-parent' ))
end
it 'renames path & route for the project' do
expect(subject).to receive(:rename_path_for_routable)
.with(project)
......@@ -63,27 +63,42 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
expect(project.reload.path).to eq('the-path0')
end
it 'tracks the rename' do
expect(subject).to receive(:track_rename)
.with('project', 'known-parent/the-path', 'known-parent/the-path0')
subject.rename_project(project)
end
it 'renames the folders for the project' do
expect(subject).to receive(:move_project_folders).with(project, 'known-parent/the-path', 'known-parent/the-path0')
subject.rename_project(project)
end
end
describe '#move_project_folders' do
it 'moves the wiki & the repo' do
expect(subject).to receive(:move_repository)
.with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki')
expect(subject).to receive(:move_repository)
.with(project, 'known-parent/the-path', 'known-parent/the-path0')
subject.rename_project(project)
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
it 'moves uploads' do
expect(subject).to receive(:move_uploads)
.with('known-parent/the-path', 'known-parent/the-path0')
subject.rename_project(project)
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
it 'moves pages' do
expect(subject).to receive(:move_pages)
.with('known-parent/the-path', 'known-parent/the-path0')
subject.rename_project(project)
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
end
......@@ -99,4 +114,47 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
expect(File.directory?(expected_path)).to be(true)
end
end
describe '#revert_renames', redis: true do
it 'renames the routes back to the previous values' do
subject.rename_project(project)
expect(subject).to receive(:perform_rename)
.with(
kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project),
'known-parent/the-path0',
'known-parent/the-path'
).and_call_original
subject.revert_renames
expect(project.reload.path).to eq('the-path')
expect(project.route.path).to eq('known-parent/the-path')
end
it 'moves the repositories back to their original place' do
project.create_repository
subject.rename_project(project)
expected_path = File.join(TestEnv.repos_path, 'known-parent', 'the-path.git')
expect(subject).to receive(:move_project_folders)
.with(
kind_of(Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Project),
'known-parent/the-path0',
'known-parent/the-path'
).and_call_original
subject.revert_renames
expect(File.directory?(expected_path)).to be_truthy
end
it "doesn't break when the project was renamed" do
subject.rename_project(project)
project.update_attributes!(path: 'renamed-afterwards')
expect { subject.revert_renames }.not_to raise_error
end
end
end
......@@ -51,4 +51,26 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do
subject.rename_root_paths('the-path')
end
end
describe '#revert_renames' do
it 'renames namespaces' do
rename_namespaces = double
expect(described_class::RenameNamespaces)
.to receive(:new).with([], subject)
.and_return(rename_namespaces)
expect(rename_namespaces).to receive(:revert_renames)
subject.revert_renames
end
it 'renames projects' do
rename_projects = double
expect(described_class::RenameProjects)
.to receive(:new).with([], subject)
.and_return(rename_projects)
expect(rename_projects).to receive(:revert_renames)
subject.revert_renames
end
end
end
require 'spec_helper'
describe Gitlab::Database::ShaAttribute do
let(:sha) do
'9a573a369a5bfbb9a4a36e98852c21af8a44ea8b'
end
let(:binary_sha) do
[sha].pack('H*')
end
let(:binary_from_db) do
if Gitlab::Database.postgresql?
"\\x#{sha}"
else
binary_sha
end
end
let(:attribute) { described_class.new }
describe '#type_cast_from_database' do
it 'converts the binary SHA to a String' do
expect(attribute.type_cast_from_database(binary_from_db)).to eq(sha)
end
end
describe '#type_cast_for_database' do
it 'converts a SHA String to binary data' do
expect(attribute.type_cast_for_database(sha).to_s).to eq(binary_sha)
end
end
end
......@@ -4,18 +4,20 @@ require 'fileutils'
describe Gitlab::Git::Hook, lib: true do
describe "#trigger" do
let(:project) { create(:project, :repository) }
let(:repo_path) { project.repository.path }
let(:user) { create(:user) }
let(:gl_id) { Gitlab::GlId.gl_id(user) }
def create_hook(name)
FileUtils.mkdir_p(File.join(project.repository.path, 'hooks'))
File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f|
FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f|
f.write('exit 0')
end
end
def create_failing_hook(name)
FileUtils.mkdir_p(File.join(project.repository.path, 'hooks'))
File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f|
FileUtils.mkdir_p(File.join(repo_path, 'hooks'))
File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f|
f.write(<<-HOOK)
echo 'regular message from the hook'
echo 'error message from the hook' 1>&2
......@@ -27,13 +29,29 @@ describe Gitlab::Git::Hook, lib: true do
['pre-receive', 'post-receive', 'update'].each do |hook_name|
context "when triggering a #{hook_name} hook" do
context "when the hook is successful" do
let(:hook_path) { File.join(repo_path, 'hooks', hook_name) }
let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) }
let(:env) do
{
'GL_ID' => gl_id,
'PWD' => repo_path,
'GL_PROTOCOL' => 'web',
'GL_REPOSITORY' => gl_repository
}
end
it "returns success with no errors" do
create_hook(hook_name)
hook = Gitlab::Git::Hook.new(hook_name, project.repository.path)
hook = Gitlab::Git::Hook.new(hook_name, project)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
if hook_name != 'update'
expect(Open3).to receive(:popen3)
.with(env, hook_path, chdir: repo_path).and_call_original
end
status, errors = hook.trigger(gl_id, blank, blank, ref)
expect(status).to be true
expect(errors).to be_blank
end
......@@ -42,11 +60,11 @@ describe Gitlab::Git::Hook, lib: true do
context "when the hook is unsuccessful" do
it "returns failure with errors" do
create_failing_hook(hook_name)
hook = Gitlab::Git::Hook.new(hook_name, project.repository.path)
hook = Gitlab::Git::Hook.new(hook_name, project)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
status, errors = hook.trigger(gl_id, blank, blank, ref)
expect(status).to be false
expect(errors).to eq("error message from the hook\n")
end
......@@ -56,11 +74,11 @@ describe Gitlab::Git::Hook, lib: true do
context "when the hook doesn't exist" do
it "returns success with no errors" do
hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path)
hook = Gitlab::Git::Hook.new('unknown_hook', project)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref)
status, errors = hook.trigger(gl_id, blank, blank, ref)
expect(status).to be true
expect(errors).to be_nil
end
......
......@@ -26,6 +26,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
it 'returns UTF-8' do
expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8'))
end
context 'with gitaly enabled' do
before do
stub_gitaly
......@@ -123,6 +127,11 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'has SeedRepo::Repo::BRANCHES.size elements' do
expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size)
end
it 'returns UTF-8' do
expect(subject.first.encoding).to eq(Encoding.find('UTF-8'))
end
it { is_expected.to include("master") }
it { is_expected.not_to include("branch-from-space") }
......@@ -158,10 +167,15 @@ describe Gitlab::Git::Repository, seed_helper: true do
subject { repository.tag_names }
it { is_expected.to be_kind_of Array }
it 'has SeedRepo::Repo::TAGS.size elements' do
expect(subject.size).to eq(SeedRepo::Repo::TAGS.size)
end
it 'returns UTF-8' do
expect(subject.first.encoding).to eq(Encoding.find('UTF-8'))
end
describe '#last' do
subject { super().last }
it { is_expected.to eq("v1.2.1") }
......@@ -1276,6 +1290,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
Gitlab::GitalyClient.clear_stubs!
end
it 'returns a Branch with UTF-8 fields' do
branches = @repo.local_branches.to_a
expect(branches.size).to be > 0
utf_8 = Encoding.find('utf-8')
branches.each do |branch|
expect(branch.name.encoding).to eq(utf_8)
expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil?
end
end
it 'gets the branches from GitalyClient' do
expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches)
.and_return([])
......
......@@ -69,6 +69,15 @@ describe Gitlab::GitalyClient::Ref do
client.local_branches(sort_by: 'updated_desc')
end
it 'translates known mismatches on sort param values' do
expect_any_instance_of(Gitaly::Ref::Stub)
.to receive(:find_local_branches)
.with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash))
.and_return([])
client.local_branches(sort_by: 'name_asc')
end
it 'raises an argument error if an invalid sort_by parameter is passed' do
expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError)
end
......
......@@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer, services: true do
it 'has the webhooks' do
restorer.restore
expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist
expect(Gitlab::Git::Hook.new('post-receive', project)).to exist
end
end
end
......@@ -37,7 +37,8 @@ describe Gitlab::UsageData do
expect(count_data.keys).to match_array(%i(
boards
ci_builds
ci_pipelines
ci_internal_pipelines
ci_external_pipelines
ci_runners
ci_triggers
ci_pipeline_schedules
......
......@@ -676,6 +676,12 @@ describe Ci::Pipeline, models: true do
end
end
describe '.internal_sources' do
subject { described_class.internal_sources }
it { is_expected.to be_an(Array) }
end
describe '#status' do
let(:build) do
create(:ci_build, :created, pipeline: pipeline, name: 'test')
......
......@@ -3,14 +3,8 @@ require 'spec_helper'
describe Ci::Variable, models: true do
subject { build(:ci_variable) }
let(:secret_value) { 'secret' }
it { is_expected.to validate_presence_of(:key) }
it { is_expected.to include_module(HasVariable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:key).is_at_most(255) }
it { is_expected.to allow_value('foo').for(:key) }
it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) }
describe '.unprotected' do
subject { described_class.unprotected }
......@@ -33,36 +27,4 @@ describe Ci::Variable, models: true do
end
end
end
describe '#value' do
before do
subject.value = secret_value
end
it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil
end
it 'stores an iv for value' do
expect(subject.encrypted_value_iv).not_to be_nil
end
it 'stores a salt for value' do
expect(subject.encrypted_value_salt).not_to be_nil
end
it 'fails to decrypt if iv is incorrect' do
subject.encrypted_value_iv = SecureRandom.hex
subject.instance_variable_set(:@value, nil)
expect { subject.value }
.to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end
end
describe '#to_runner_variable' do
it 'returns a hash for the runner' do
expect(subject.to_runner_variable)
.to eq(key: subject.key, value: subject.value, public: false)
end
end
end
require 'spec_helper'
describe HasVariable do
subject { build(:ci_variable) }
it { is_expected.to validate_presence_of(:key) }
it { is_expected.to validate_length_of(:key).is_at_most(255) }
it { is_expected.to allow_value('foo').for(:key) }
it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) }
describe '#value' do
before do
subject.value = 'secret'
end
it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil
end
it 'stores an iv for value' do
expect(subject.encrypted_value_iv).not_to be_nil
end
it 'stores a salt for value' do
expect(subject.encrypted_value_salt).not_to be_nil
end
it 'fails to decrypt if iv is incorrect' do
subject.encrypted_value_iv = SecureRandom.hex
subject.instance_variable_set(:@value, nil)
expect { subject.value }
.to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end
end
describe '#to_runner_variable' do
it 'returns a hash for the runner' do
expect(subject.to_runner_variable)
.to eq(key: subject.key, value: subject.value, public: false)
end
end
end
require 'spec_helper'
describe ShaAttribute do
let(:model) { Class.new { include ShaAttribute } }
before do
columns = [
double(:column, name: 'name', type: :text),
double(:column, name: 'sha1', type: :binary)
]
allow(model).to receive(:columns).and_return(columns)
end
describe '#sha_attribute' do
it 'defines a SHA attribute for a binary column' do
expect(model).to receive(:attribute)
.with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
model.sha_attribute(:sha1)
end
it 'raises ArgumentError when the column type is not :binary' do
expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
end
end
end
......@@ -335,6 +335,36 @@ describe Namespace, models: true do
end
end
describe '#users_with_descendants', :nested_groups do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
it 'returns member users on every nest level without duplication' do
group.add_developer(user_a)
nested_group.add_developer(user_b)
deep_nested_group.add_developer(user_a)
expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a)
end
end
describe '#soft_delete_without_removing_associations' do
let(:project1) { create(:project_empty_repo, namespace: namespace) }
it 'updates the deleted_at timestamp but preserves projects' do
namespace.soft_delete_without_removing_associations
expect(Project.all).to include(project1)
expect(namespace.deleted_at).not_to be_nil
end
end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
expect(namespace.user_ids_for_project_authorizations)
......
......@@ -326,15 +326,6 @@ describe Project, models: true do
end
end
describe 'default_scope' do
it 'excludes projects pending deletion from the results' do
project = create(:empty_project)
create(:empty_project, pending_delete: true)
expect(Project.all).to eq [project]
end
end
describe 'project token' do
it 'sets an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
......@@ -1421,6 +1412,16 @@ describe Project, models: true do
expect(relation.search(project.namespace.name)).to eq([project])
end
describe 'with pending_delete project' do
let(:pending_delete_project) { create(:empty_project, pending_delete: true) }
it 'shows pending deletion project' do
search_result = described_class.search(pending_delete_project.name)
expect(search_result).to eq([pending_delete_project])
end
end
end
describe '#rename_repo' do
......@@ -1785,6 +1786,40 @@ describe Project, models: true do
end
end
describe 'project import state transitions' do
context 'state transition: [:started] => [:finished]' do
let(:housekeeping_service) { spy }
before do
allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service }
end
it 'performs housekeeping when an import of a fresh project is completed' do
project = create(:project_empty_repo, :import_started, import_type: :github)
project.import_finish
expect(housekeeping_service).to have_received(:execute)
end
it 'does not perform housekeeping when project repository does not exist' do
project = create(:empty_project, :import_started, import_type: :github)
project.import_finish
expect(housekeeping_service).not_to have_received(:execute)
end
it 'does not perform housekeeping when project does not have a valid import type' do
project = create(:empty_project, :import_started, import_type: nil)
project.import_finish
expect(housekeeping_service).not_to have_received(:execute)
end
end
end
describe '#latest_successful_builds_for' do
def create_pipeline(status = 'success')
create(:ci_pipeline, project: project,
......
......@@ -780,7 +780,7 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
it 'runs without errors' do
expect_any_instance_of(GitHooksService).to receive(:execute)
.with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
.with(user, project, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
......@@ -823,12 +823,7 @@ describe Repository, models: true do
service = GitHooksService.new
expect(GitHooksService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
.with(
user,
repository.path_to_repo,
old_rev,
new_rev,
'refs/heads/feature')
.with(user, project, old_rev, new_rev, 'refs/heads/feature')
.and_yield(service).and_return(true)
end
......@@ -1544,9 +1539,9 @@ describe Repository, models: true do
it 'passes commit SHA to pre-receive and update hooks,\
and tag SHA to post-receive hook' do
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo)
update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo)
post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo)
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
update_hook = Gitlab::Git::Hook.new('update', project)
post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
allow(Gitlab::Git::Hook).to receive(:new)
.and_return(pre_receive_hook, update_hook, post_receive_hook)
......
......@@ -782,42 +782,49 @@ describe User, models: true do
end
describe '.search' do
let(:user) { create(:user) }
let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') }
let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') }
it 'returns users with a matching name' do
expect(described_class.search(user.name)).to eq([user])
end
describe 'name matching' do
it 'returns users with a matching name with exact match first' do
expect(described_class.search(user.name)).to eq([user, user2])
end
it 'returns users with a partially matching name' do
expect(described_class.search(user.name[0..2])).to eq([user])
end
it 'returns users with a partially matching name' do
expect(described_class.search(user.name[0..2])).to eq([user2, user])
end
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search(user.name.upcase)).to eq([user])
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search(user2.name.upcase)).to eq([user2])
end
end
it 'returns users with a matching Email' do
expect(described_class.search(user.email)).to eq([user])
end
describe 'email matching' do
it 'returns users with a matching Email' do
expect(described_class.search(user.email)).to eq([user, user2])
end
it 'returns users with a partially matching Email' do
expect(described_class.search(user.email[0..2])).to eq([user])
end
it 'returns users with a partially matching Email' do
expect(described_class.search(user.email[0..2])).to eq([user2, user])
end
it 'returns users with a matching Email regardless of the casing' do
expect(described_class.search(user.email.upcase)).to eq([user])
it 'returns users with a matching Email regardless of the casing' do
expect(described_class.search(user2.email.upcase)).to eq([user2])
end
end
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user])
end
describe 'username matching' do
it 'returns users with a matching username' do
expect(described_class.search(user.username)).to eq([user, user2])
end
it 'returns users with a partially matching username' do
expect(described_class.search(user.username[0..2])).to eq([user])
end
it 'returns users with a partially matching username' do
expect(described_class.search(user.username[0..2])).to eq([user2, user])
end
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search(user.username.upcase)).to eq([user])
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search(user2.username.upcase)).to eq([user2])
end
end
end
......
......@@ -12,7 +12,6 @@ describe GitHooksService, services: true do
@oldrev = sample_commit.parent_id
@newrev = sample_commit.id
@ref = 'refs/heads/feature'
@repo_path = project.repository.path_to_repo
end
describe '#execute' do
......@@ -21,7 +20,7 @@ describe GitHooksService, services: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }
service.execute(user, project, @blankrev, @newrev, @ref) { }
end
end
......@@ -31,7 +30,7 @@ describe GitHooksService, services: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, @repo_path, @blankrev, @newrev, @ref)
service.execute(user, project, @blankrev, @newrev, @ref)
end.to raise_error(GitHooksService::PreReceiveError)
end
end
......@@ -43,7 +42,7 @@ describe GitHooksService, services: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, @repo_path, @blankrev, @newrev, @ref)
service.execute(user, project, @blankrev, @newrev, @ref)
end.to raise_error(GitHooksService::PreReceiveError)
end
end
......
......@@ -15,6 +15,14 @@ describe Groups::DestroyService, services: true do
group.add_user(user, Gitlab::Access::OWNER)
end
def destroy_group(group, user, async)
if async
Groups::DestroyService.new(group, user).async_execute
else
Groups::DestroyService.new(group, user).execute
end
end
shared_examples 'group destruction' do |async|
context 'database records' do
before do
......@@ -30,30 +38,14 @@ describe Groups::DestroyService, services: true do
context 'file system' do
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed dir will be removed
# Run sidekiq immediately to check that renamed dir will be removed
Sidekiq::Testing.inline! { destroy_group(group, user, async) }
end
it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
end
context 'Sidekiq fake' do
before do
# Don't run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_group(group, user, async) }
it 'verifies that paths have been deleted' do
expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey
expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
end
it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
end
end
def destroy_group(group, user, async)
if async
Groups::DestroyService.new(group, user).async_execute
else
Groups::DestroyService.new(group, user).execute
end
end
end
......@@ -61,6 +53,26 @@ describe Groups::DestroyService, services: true do
describe 'asynchronous delete' do
it_behaves_like 'group destruction', true
context 'Sidekiq fake' do
before do
# Don't run Sidekiq to verify that group and projects are not actually destroyed
Sidekiq::Testing.fake! { destroy_group(group, user, true) }
end
after do
# Clean up stale directories
gitlab_shell.rm_namespace(project.repository_storage_path, group.path)
gitlab_shell.rm_namespace(project.repository_storage_path, remove_path)
end
it 'verifies original paths and projects still exist' do
expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy
expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
expect(Project.unscoped.count).to eq(1)
expect(Group.unscoped.count).to eq(2)
end
end
context 'potential race conditions' do
context "when the `GroupDestroyWorker` task runs immediately" do
around(:each) do |example|
......
class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration
include Gitlab::Database::RenameReservedPathsMigration::V1
def version
'20170316163845'
end
def name
"FakeRenameReservedPathMigrationV1"
end
end
# AccessMatchersForController
#
# For testing authorize_xxx in controller.
module AccessMatchersForController
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 302].freeze
EXPECTED_STATUS_CODE_DENIED = [401, 404].freeze
def emulate_user(role, membership = nil)
case role
when :admin
user = create(:admin)
sign_in(user)
when :user
user = create(:user)
sign_in(user)
when :external
user = create(:user, external: true)
sign_in(user)
when :visitor
user = nil
when User
user = role
sign_in(user)
when *Gitlab::Access.sym_options_with_owner.keys # owner, master, developer, reporter, guest
raise ArgumentError, "cannot emulate #{role} without membership parent" unless membership
user = create_user_by_membership(role, membership)
sign_in(user)
else
raise ArgumentError, "cannot emulate user #{role}"
end
user
end
def create_user_by_membership(role, membership)
if role == :owner && membership.owner
user = membership.owner
else
user = create(:user)
membership.public_send(:"add_#{role}", user)
end
user
end
def description_for(role, type, expected, result)
"be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}"
end
matcher :be_allowed_for do |role|
match do |action|
emulate_user(role, @membership)
action.call
EXPECTED_STATUS_CODE_ALLOWED.include?(response.status)
end
chain :of do |membership|
@membership = membership
end
description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) }
supports_block_expectations
end
matcher :be_denied_for do |role|
match do |action|
emulate_user(role, @membership)
action.call
EXPECTED_STATUS_CODE_DENIED.include?(response.status)
end
chain :of do |membership|
@membership = membership
end
description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) }
supports_block_expectations
end
end
shared_examples 'issue sidebar stays collapsed on mobile' do
before do
resize_screen_xs
end
it 'keeps the sidebar collapsed' do
expect(page).not_to have_css('.right-sidebar.right-sidebar-collapsed')
end
end
......@@ -122,18 +122,21 @@ module TestEnv
end
def setup_gitlab_shell
unless File.directory?(Gitlab.config.gitlab_shell.path)
unless system('rake', 'gitlab:shell:install')
raise 'Can`t clone gitlab-shell'
end
shell_needs_update = component_needs_update?(Gitlab.config.gitlab_shell.path,
Gitlab::Shell.version_required)
unless !shell_needs_update || system('rake', 'gitlab:shell:install')
raise 'Can`t clone gitlab-shell'
end
end
def setup_gitaly
socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
gitaly_needs_update = component_needs_update?(gitaly_dir,
Gitlab::GitalyClient.expected_server_version)
unless !gitaly_needs_update?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
raise "Can't clone gitaly"
end
......@@ -263,13 +266,13 @@ module TestEnv
end
end
def gitaly_needs_update?(gitaly_dir)
gitaly_version = File.read(File.join(gitaly_dir, 'VERSION')).strip
def component_needs_update?(component_folder, expected_version)
version = File.read(File.join(component_folder, 'VERSION')).strip
# Notice that this will always yield true when using branch versions
# (`=branch_name`), but that actually makes sure the server is always based
# on the latest branch revision.
gitaly_version != Gitlab::GitalyClient.expected_server_version
version != expected_version
rescue Errno::ENOENT
true
end
......
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