Merge remote-tracking branch 'origin/master' into mc-ui
This commit is contained in:
commit
e6f3461c0b
168 changed files with 14335 additions and 723 deletions
22
CHANGELOG
22
CHANGELOG
|
@ -40,6 +40,8 @@ v 8.11.0 (unreleased)
|
|||
- Various redundant database indexes have been removed
|
||||
- Update `timeago` plugin to use multiple string/locale settings
|
||||
- Remove unused images (ClemMakesApps)
|
||||
- Get issue and merge request description templates from repositories
|
||||
- Add hover state to todos !5361 (winniehell)
|
||||
- Limit git rev-list output count to one in forced push check
|
||||
- Show deployment status on merge requests with external URLs
|
||||
- Clean up unused routes (Josef Strzibny)
|
||||
|
@ -73,6 +75,7 @@ v 8.11.0 (unreleased)
|
|||
- The overhead of instrumented method calls has been reduced
|
||||
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
|
||||
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
|
||||
- Add pipeline events hook
|
||||
- Bump gitlab_git to speedup DiffCollection iterations
|
||||
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
|
||||
- Make branches sortable without push permission !5462 (winniehell)
|
||||
|
@ -82,6 +85,7 @@ v 8.11.0 (unreleased)
|
|||
- Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
|
||||
- Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
|
||||
- Fix search for notes which belongs to deleted objects
|
||||
- Allow Akismet to be trained by submitting issues as spam or ham !5538
|
||||
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
|
||||
- Add the `sprockets-es6` gem
|
||||
|
@ -93,6 +97,7 @@ v 8.11.0 (unreleased)
|
|||
- Add commit stats in commit api. !5517 (dixpac)
|
||||
- Add CI configuration button on project page
|
||||
- Make error pages responsive (Takuya Noguchi)
|
||||
- The performance of the project dropdown used for moving issues has been improved
|
||||
- Fix skip_repo parameter being ignored when destroying a namespace
|
||||
- Change requests_profiles resource constraint to catch virtually any file
|
||||
- Bump gitlab_git to lazy load compare commits
|
||||
|
@ -116,6 +121,12 @@ v 8.11.0 (unreleased)
|
|||
- Speed up todos queries by limiting the projects set we join with
|
||||
- Ensure file editing in UI does not overwrite commited changes without warning user
|
||||
|
||||
v 8.10.6
|
||||
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
|
||||
- Restore "Largest repository" sort option on Admin > Projects page. !5797
|
||||
- Fix privilege escalation via project export.
|
||||
- Require administrator privileges to perform a project import.
|
||||
|
||||
v 8.10.5
|
||||
- Add a data migration to fix some missing timestamps in the members table. !5670
|
||||
- Revert the "Defend against 'Host' header injection" change in the source NGINX templates. !5706
|
||||
|
@ -136,6 +147,9 @@ v 8.10.3
|
|||
- Fix importer for GitHub Pull Requests when a branch was removed. !5573
|
||||
- Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584
|
||||
- Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
|
||||
- Fix label already exist error message in the right sidebar.
|
||||
|
||||
v 8.10.3 (unreleased)
|
||||
|
||||
v 8.10.2
|
||||
- User can now search branches by name. !5144
|
||||
|
@ -283,6 +297,7 @@ v 8.10.0
|
|||
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
|
||||
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
|
||||
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
|
||||
- Made project list visibility icon fixed width
|
||||
- Set import_url validation to be more strict
|
||||
- Memoize MR merged/closed events retrieval
|
||||
- Don't render discussion notes when requesting diff tab through AJAX
|
||||
|
@ -329,6 +344,10 @@ v 8.10.0
|
|||
- Fix migration corrupting import data for old version upgrades
|
||||
- Show tooltip on GitLab export link in new project page
|
||||
|
||||
v 8.9.7
|
||||
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
|
||||
- Require administrator privileges to perform a project import.
|
||||
|
||||
v 8.9.6
|
||||
- Fix importing of events under notes for GitLab projects. !5154
|
||||
- Fix log statements in import/export. !5129
|
||||
|
@ -594,6 +613,9 @@ v 8.9.0
|
|||
- Add tooltip to pin/unpin navbar
|
||||
- Add new sub nav style to Wiki and Graphs sub navigation
|
||||
|
||||
v 8.8.8
|
||||
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
|
||||
|
||||
v 8.8.7
|
||||
- Fix privilege escalation issue with OAuth external users.
|
||||
- Ensure references to private repos aren't shown to logged-out users.
|
||||
|
|
|
@ -338,7 +338,7 @@ GEM
|
|||
httparty (0.13.7)
|
||||
json (~> 1.8)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.7.0.1)
|
||||
httpclient (2.8.2)
|
||||
i18n (0.7.0)
|
||||
ice_nine (0.11.1)
|
||||
influxdb (0.2.3)
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
licensePath: "/api/:version/licenses/:key",
|
||||
gitignorePath: "/api/:version/gitignores/:key",
|
||||
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
|
||||
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
|
||||
|
||||
group: function(group_id, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupPath);
|
||||
url = url.replace(':id', group_id);
|
||||
var url = Api.buildUrl(Api.groupPath)
|
||||
.replace(':id', group_id);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -24,8 +25,7 @@
|
|||
});
|
||||
},
|
||||
groups: function(query, skip_ldap, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupsPath);
|
||||
var url = Api.buildUrl(Api.groupsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -39,8 +39,7 @@
|
|||
});
|
||||
},
|
||||
namespaces: function(query, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.namespacesPath);
|
||||
var url = Api.buildUrl(Api.namespacesPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -54,8 +53,7 @@
|
|||
});
|
||||
},
|
||||
projects: function(query, order, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.projectsPath);
|
||||
var url = Api.buildUrl(Api.projectsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -70,9 +68,8 @@
|
|||
});
|
||||
},
|
||||
newLabel: function(project_id, data, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.labelsPath);
|
||||
url = url.replace(':id', project_id);
|
||||
var url = Api.buildUrl(Api.labelsPath)
|
||||
.replace(':id', project_id);
|
||||
data.private_token = gon.api_token;
|
||||
return $.ajax({
|
||||
url: url,
|
||||
|
@ -86,9 +83,8 @@
|
|||
});
|
||||
},
|
||||
groupProjects: function(group_id, query, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupProjectsPath);
|
||||
url = url.replace(':id', group_id);
|
||||
var url = Api.buildUrl(Api.groupProjectsPath)
|
||||
.replace(':id', group_id);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -102,8 +98,8 @@
|
|||
});
|
||||
},
|
||||
licenseText: function(key, data, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.licensePath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.licensePath)
|
||||
.replace(':key', key);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: data
|
||||
|
@ -112,19 +108,32 @@
|
|||
});
|
||||
},
|
||||
gitignoreText: function(key, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.gitignorePath)
|
||||
.replace(':key', key);
|
||||
return $.get(url, function(gitignore) {
|
||||
return callback(gitignore);
|
||||
});
|
||||
},
|
||||
gitlabCiYml: function(key, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.gitlabCiYmlPath)
|
||||
.replace(':key', key);
|
||||
return $.get(url, function(file) {
|
||||
return callback(file);
|
||||
});
|
||||
},
|
||||
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
|
||||
var url = Api.buildUrl(Api.issuableTemplatePath)
|
||||
.replace(':key', key)
|
||||
.replace(':type', type)
|
||||
.replace(':project_path', projectPath)
|
||||
.replace(':namespace_path', namespacePath);
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
}).done(function(file) {
|
||||
callback(null, file);
|
||||
}).error(callback);
|
||||
},
|
||||
buildUrl: function(url) {
|
||||
if (gon.relative_url_root != null) {
|
||||
url = gon.relative_url_root + url;
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
/*= require date.format */
|
||||
/*= require_directory ./behaviors */
|
||||
/*= require_directory ./blob */
|
||||
/*= require_directory ./templates */
|
||||
/*= require_directory ./commit */
|
||||
/*= require_directory ./extensions */
|
||||
/*= require_directory ./lib/utils */
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
}
|
||||
this.onClick = bind(this.onClick, this);
|
||||
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
|
||||
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
|
||||
this.buildDropdown();
|
||||
this.bindEvents();
|
||||
this.onFilenameUpdate();
|
||||
|
@ -60,11 +61,26 @@
|
|||
return this.requestFile(item);
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.requestFile = function(item) {};
|
||||
TemplateSelector.prototype.requestFile = function(item) {
|
||||
// This `requestFile` method is an abstract method that should
|
||||
// be added by all subclasses.
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.requestFileSuccess = function(file) {
|
||||
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
|
||||
this.editor.setValue(file.content, 1);
|
||||
return this.editor.focus();
|
||||
if (!skipFocus) this.editor.focus();
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.startLoadingSpinner = function() {
|
||||
this.dropdownIcon
|
||||
.addClass('fa-spinner fa-spin')
|
||||
.removeClass('fa-chevron-down');
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.stopLoadingSpinner = function() {
|
||||
this.dropdownIcon
|
||||
.addClass('fa-chevron-down')
|
||||
.removeClass('fa-spinner fa-spin');
|
||||
};
|
||||
|
||||
return TemplateSelector;
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.issue-form'));
|
||||
new IssuableForm($('.issue-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:merge_requests:new':
|
||||
case 'projects:merge_requests:edit':
|
||||
|
@ -62,6 +63,7 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.merge-request-form'));
|
||||
new IssuableForm($('.merge-request-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:tags:new':
|
||||
new ZenMode();
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
this.render = bind(this.render, this);
|
||||
this.VIEW_TYPE = $('input#view[type=hidden]').val();
|
||||
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
|
||||
$(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
|
||||
$(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
|
||||
}
|
||||
|
||||
FilesCommentButton.prototype.render = function(e) {
|
||||
|
|
|
@ -70,13 +70,15 @@
|
|||
name: newLabelField.val(),
|
||||
color: newColorField.val()
|
||||
}, function(label) {
|
||||
var errors;
|
||||
$newLabelCreateButton.enable();
|
||||
if (label.message != null) {
|
||||
errors = _.map(label.message, function(value, key) {
|
||||
return key + " " + value[0];
|
||||
});
|
||||
return $newLabelError.html(errors.join("<br/>")).show();
|
||||
var errorText = label.message;
|
||||
if (_.isObject(label.message)) {
|
||||
errorText = _.map(label.message, function(value, key) {
|
||||
return key + " " + value[0];
|
||||
}).join('<br/>');
|
||||
}
|
||||
return $newLabelError.html(errorText).show();
|
||||
} else {
|
||||
return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
|
||||
}
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
|
||||
// Enable submit button
|
||||
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
|
||||
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]');
|
||||
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]');
|
||||
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
|
||||
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
|
||||
|
||||
if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
|
||||
this.$form.find('input[type="submit"]').removeAttr('disabled');
|
||||
|
|
|
@ -39,12 +39,14 @@
|
|||
_method: 'PATCH',
|
||||
id: this.$wrap.data('banchId'),
|
||||
protected_branch: {
|
||||
merge_access_level_attributes: {
|
||||
merge_access_levels_attributes: [{
|
||||
id: this.$allowedToMergeDropdown.data('access-level-id'),
|
||||
access_level: $allowedToMergeInput.val()
|
||||
},
|
||||
push_access_level_attributes: {
|
||||
}],
|
||||
push_access_levels_attributes: [{
|
||||
id: this.$allowedToPushDropdown.data('access-level-id'),
|
||||
access_level: $allowedToPushInput.val()
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
success: () => {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*= require ../blob/template_selector */
|
||||
|
||||
((global) => {
|
||||
class IssuableTemplateSelector extends TemplateSelector {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.projectPath = this.dropdown.data('project-path');
|
||||
this.namespacePath = this.dropdown.data('namespace-path');
|
||||
this.issuableType = this.wrapper.data('issuable-type');
|
||||
this.titleInput = $(`#${this.issuableType}_title`);
|
||||
|
||||
let initialQuery = {
|
||||
name: this.dropdown.data('selected')
|
||||
};
|
||||
|
||||
if (initialQuery.name) this.requestFile(initialQuery);
|
||||
|
||||
$('.reset-template', this.dropdown.parent()).on('click', () => {
|
||||
if (this.currentTemplate) this.setInputValueToTemplateContent();
|
||||
});
|
||||
}
|
||||
|
||||
requestFile(query) {
|
||||
this.startLoadingSpinner();
|
||||
Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
|
||||
this.currentTemplate = currentTemplate;
|
||||
if (err) return; // Error handled by global AJAX error handler
|
||||
this.stopLoadingSpinner();
|
||||
this.setInputValueToTemplateContent();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValueToTemplateContent() {
|
||||
// `this.requestFileSuccess` sets the value of the description input field
|
||||
// to the content of the template selected.
|
||||
if (this.titleInput.val() === '') {
|
||||
// If the title has not yet been set, focus the title input and
|
||||
// skip focusing the description input by setting `true` as the 2nd
|
||||
// argument to `requestFileSuccess`.
|
||||
this.requestFileSuccess(this.currentTemplate, true);
|
||||
this.titleInput.focus();
|
||||
} else {
|
||||
this.requestFileSuccess(this.currentTemplate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelector = IssuableTemplateSelector;
|
||||
})(window);
|
|
@ -0,0 +1,29 @@
|
|||
((global) => {
|
||||
class IssuableTemplateSelectors {
|
||||
constructor(opts = {}) {
|
||||
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
|
||||
this.editor = opts.editor || this.initEditor();
|
||||
|
||||
this.$dropdowns.each((i, dropdown) => {
|
||||
let $dropdown = $(dropdown);
|
||||
new IssuableTemplateSelector({
|
||||
pattern: /(\.md)/,
|
||||
data: $dropdown.data('data'),
|
||||
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
|
||||
dropdown: $dropdown,
|
||||
editor: this.editor
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initEditor() {
|
||||
let editor = $('.markdown-area');
|
||||
// Proxy ace-editor's .setValue to jQuery's .val
|
||||
editor.setValue = editor.val;
|
||||
editor.getValue = editor.val;
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
|
||||
})(window);
|
|
@ -164,6 +164,10 @@
|
|||
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
|
||||
}
|
||||
|
||||
&.btn-spam {
|
||||
@include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
|
||||
}
|
||||
|
||||
&.btn-danger,
|
||||
&.btn-remove,
|
||||
&.btn-red {
|
||||
|
|
|
@ -56,9 +56,13 @@
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
margin-top: -4px;
|
||||
margin-top: -6px;
|
||||
color: $dropdown-toggle-icon-color;
|
||||
font-size: 10px;
|
||||
&.fa-spinner {
|
||||
font-size: 16px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, {
|
||||
|
@ -406,6 +410,7 @@
|
|||
font-size: 14px;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
table-layout: fixed;
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-family: $monospace_font;
|
||||
font-size: $code_font_size !important;
|
||||
font-size: $code_font_size;
|
||||
line-height: $code_line_height !important;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
|
@ -20,13 +20,20 @@
|
|||
border-left: 1px solid;
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
font-family: $monospace_font;
|
||||
white-space: pre;
|
||||
white-space: normal;
|
||||
word-wrap: normal;
|
||||
padding: 0;
|
||||
|
||||
.line {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 19px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -395,3 +395,12 @@
|
|||
display: inline-block;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.js-issuable-selector-wrap {
|
||||
.js-issuable-selector {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: $screen-sm-max) {
|
||||
margin-bottom: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 15px;
|
||||
max-width: 480px;
|
||||
max-width: 700px;
|
||||
|
||||
> p {
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -20,10 +20,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
.todo {
|
||||
.todos-list > .todo {
|
||||
// workaround because we cannot use border-colapse
|
||||
border-top: 1px solid transparent;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
|
||||
&:hover {
|
||||
background-color: $row-hover;
|
||||
border-color: $row-hover-border;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// overwrite border style of .content-list
|
||||
&:last-child {
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: $row-hover-border;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-actions {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
-webkit-flex: auto;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
|
@ -43,8 +76,6 @@
|
|||
}
|
||||
|
||||
.todo-body {
|
||||
margin-right: 174px;
|
||||
|
||||
.todo-note {
|
||||
word-wrap: break-word;
|
||||
|
||||
|
@ -90,6 +121,12 @@
|
|||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.todo {
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
.todo-title {
|
||||
white-space: normal;
|
||||
|
@ -98,10 +135,6 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-body {
|
||||
margin: 0;
|
||||
border-left: 2px solid #ddd;
|
||||
|
|
|
@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
|
|||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_ham
|
||||
spam_log = SpamLog.find(params[:id])
|
||||
|
||||
if HamService.new(spam_log).mark_as_ham!
|
||||
redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
|
||||
else
|
||||
redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class AutocompleteController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [:users]
|
||||
before_action :load_project, only: [:users]
|
||||
before_action :find_users, only: [:users]
|
||||
|
||||
def users
|
||||
|
@ -34,19 +35,13 @@ class AutocompleteController < ApplicationController
|
|||
|
||||
def projects
|
||||
project = Project.find_by_id(params[:project_id])
|
||||
|
||||
projects = current_user.authorized_projects
|
||||
projects = projects.search(params[:search]) if params[:search].present?
|
||||
projects = projects.select do |project|
|
||||
current_user.can?(:admin_issue, project)
|
||||
end
|
||||
projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
|
||||
|
||||
no_project = {
|
||||
id: 0,
|
||||
name_with_namespace: 'No project',
|
||||
}
|
||||
projects.unshift(no_project)
|
||||
projects.delete(project)
|
||||
projects.unshift(no_project) unless params[:offset_id].present?
|
||||
|
||||
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
|
||||
end
|
||||
|
@ -55,11 +50,8 @@ class AutocompleteController < ApplicationController
|
|||
|
||||
def find_users
|
||||
@users =
|
||||
if params[:project_id].present?
|
||||
project = Project.find(params[:project_id])
|
||||
return render_404 unless can?(current_user, :read_project, project)
|
||||
|
||||
project.team.users
|
||||
if @project
|
||||
@project.team.users
|
||||
elsif params[:group_id].present?
|
||||
group = Group.find(params[:group_id])
|
||||
return render_404 unless can?(current_user, :read_group, group)
|
||||
|
@ -71,4 +63,18 @@ class AutocompleteController < ApplicationController
|
|||
User.none
|
||||
end
|
||||
end
|
||||
|
||||
def load_project
|
||||
@project ||= begin
|
||||
if params[:project_id].present?
|
||||
project = Project.find(params[:project_id])
|
||||
return render_404 unless can?(current_user, :read_project, project)
|
||||
project
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def projects_finder
|
||||
MoveToProjectFinder.new(current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,11 +7,16 @@ module ServiceParams
|
|||
:build_key, :server, :teamcity_url, :drone_url, :build_type,
|
||||
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
|
||||
:colorize_messages, :channels,
|
||||
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
|
||||
:note_events, :build_events, :wiki_page_events,
|
||||
:notify_only_broken_builds, :add_pusher,
|
||||
:send_from_committer_email, :disable_diffs, :external_wiki_url,
|
||||
:notify, :color,
|
||||
# We're using `issues_events` and `merge_requests_events`
|
||||
# in the view so we still need to explicitly state them
|
||||
# here. `Service#event_names` would only give
|
||||
# `issue_events` and `merge_request_events` (singular!)
|
||||
# See app/helpers/services_helper.rb for how we
|
||||
# make those event names plural as special case.
|
||||
:issues_events, :merge_requests_events,
|
||||
:notify_only_broken_builds, :notify_only_broken_pipelines,
|
||||
:add_pusher, :send_from_committer_email, :disable_diffs,
|
||||
:external_wiki_url, :notify, :color,
|
||||
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
|
||||
:jira_issue_transition_id]
|
||||
|
||||
|
@ -19,9 +24,7 @@ module ServiceParams
|
|||
FILTER_BLANK_PARAMS = [:password]
|
||||
|
||||
def service_params
|
||||
dynamic_params = []
|
||||
dynamic_params.concat(@service.event_channel_names)
|
||||
|
||||
dynamic_params = @service.event_channel_names + @service.event_names
|
||||
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
|
||||
|
||||
if service_params[:service].is_a?(Hash)
|
||||
|
|
25
app/controllers/concerns/spammable_actions.rb
Normal file
25
app/controllers/concerns/spammable_actions.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
module SpammableActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authorize_submit_spammable!, only: :mark_as_spam
|
||||
end
|
||||
|
||||
def mark_as_spam
|
||||
if SpamService.new(spammable).mark_as_spam!
|
||||
redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
|
||||
else
|
||||
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def spammable
|
||||
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
|
||||
end
|
||||
|
||||
def authorize_submit_spammable!
|
||||
access_denied! unless current_user.admin?
|
||||
end
|
||||
end
|
|
@ -1,5 +1,6 @@
|
|||
class Import::GitlabProjectsController < Import::BaseController
|
||||
before_action :verify_gitlab_project_import_enabled
|
||||
before_action :authenticate_admin!
|
||||
|
||||
def new
|
||||
@namespace_id = project_params[:namespace_id]
|
||||
|
@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController
|
|||
:path, :namespace_id, :file
|
||||
)
|
||||
end
|
||||
|
||||
def authenticate_admin!
|
||||
render_404 unless current_user.is_admin?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController
|
|||
def hook_params
|
||||
params.require(:hook).permit(
|
||||
:build_events,
|
||||
:pipeline_events,
|
||||
:enable_ssl_verification,
|
||||
:issues_events,
|
||||
:merge_requests_events,
|
||||
|
|
|
@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
include IssuableActions
|
||||
include ToggleAwardEmoji
|
||||
include IssuableCollections
|
||||
include SpammableActions
|
||||
|
||||
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
|
||||
before_action :module_enabled
|
||||
|
@ -185,6 +186,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
alias_method :subscribable_resource, :issue
|
||||
alias_method :issuable, :issue
|
||||
alias_method :awardable, :issue
|
||||
alias_method :spammable, :issue
|
||||
|
||||
def authorize_read_issue!
|
||||
return render_404 unless can?(current_user, :read_issue, @issue)
|
||||
|
|
|
@ -9,16 +9,16 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
|
|||
|
||||
def index
|
||||
@protected_branch = @project.protected_branches.new
|
||||
load_protected_branches_gon_variables
|
||||
load_gon_index
|
||||
end
|
||||
|
||||
def create
|
||||
@protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
|
||||
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
|
||||
if @protected_branch.persisted?
|
||||
redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
|
||||
else
|
||||
load_protected_branches
|
||||
load_protected_branches_gon_variables
|
||||
load_gon_index
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
@protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
|
||||
@protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
|
||||
|
||||
if @protected_branch.valid?
|
||||
respond_to do |format|
|
||||
|
@ -58,17 +58,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
|
|||
|
||||
def protected_branch_params
|
||||
params.require(:protected_branch).permit(:name,
|
||||
merge_access_level_attributes: [:access_level],
|
||||
push_access_level_attributes: [:access_level])
|
||||
merge_access_levels_attributes: [:access_level, :id],
|
||||
push_access_levels_attributes: [:access_level, :id])
|
||||
end
|
||||
|
||||
def load_protected_branches
|
||||
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
|
||||
end
|
||||
|
||||
def load_protected_branches_gon_variables
|
||||
gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } },
|
||||
push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } },
|
||||
merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } })
|
||||
def access_levels_options
|
||||
{
|
||||
push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
|
||||
merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
|
||||
}
|
||||
end
|
||||
|
||||
def load_gon_index
|
||||
params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
|
||||
gon.push(params.merge(access_levels_options))
|
||||
end
|
||||
end
|
||||
|
|
19
app/controllers/projects/templates_controller.rb
Normal file
19
app/controllers/projects/templates_controller.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
class Projects::TemplatesController < Projects::ApplicationController
|
||||
before_action :authenticate_user!, :get_template_class
|
||||
|
||||
def show
|
||||
template = @template_type.find(params[:key], project)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: template.to_json }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_template_class
|
||||
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
|
||||
@template_type = template_types[params[:template_type]]
|
||||
render json: [], status: 404 unless @template_type
|
||||
end
|
||||
end
|
14
app/finders/move_to_project_finder.rb
Normal file
14
app/finders/move_to_project_finder.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class MoveToProjectFinder
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def execute(from_project, search: nil, offset_id: nil)
|
||||
projects = @user.projects_where_can_admin_issues
|
||||
projects = projects.search(search) if search.present?
|
||||
projects = projects.excluding_project(from_project)
|
||||
|
||||
# to ask for Project#name_with_namespace
|
||||
projects.includes(namespace: :owner)
|
||||
end
|
||||
end
|
|
@ -182,17 +182,42 @@ module BlobHelper
|
|||
}
|
||||
end
|
||||
|
||||
def selected_template(issuable)
|
||||
templates = issuable_templates(issuable)
|
||||
params[:issuable_template] if templates.include?(params[:issuable_template])
|
||||
end
|
||||
|
||||
def can_add_template?(issuable)
|
||||
names = issuable_templates(issuable)
|
||||
names.empty? && can?(current_user, :push_code, @project) && !@project.private?
|
||||
end
|
||||
|
||||
def merge_request_template_names
|
||||
@merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
|
||||
end
|
||||
|
||||
def issue_template_names
|
||||
@issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
|
||||
end
|
||||
|
||||
def issuable_templates(issuable)
|
||||
@issuable_templates ||=
|
||||
if issuable.is_a?(Issue)
|
||||
issue_template_names
|
||||
elsif issuable.is_a?(MergeRequest)
|
||||
merge_request_template_names
|
||||
end
|
||||
end
|
||||
|
||||
def ref_project
|
||||
@ref_project ||= @target_project || @project
|
||||
end
|
||||
|
||||
def gitignore_names
|
||||
@gitignore_names ||=
|
||||
Gitlab::Template::Gitignore.categories.keys.map do |k|
|
||||
[k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
|
||||
end.to_h
|
||||
@gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
|
||||
end
|
||||
|
||||
def gitlab_ci_ymls
|
||||
@gitlab_ci_ymls ||=
|
||||
Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
|
||||
[k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
|
||||
end.to_h
|
||||
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
|
||||
end
|
||||
end
|
||||
|
|
|
@ -344,7 +344,7 @@ module Ci
|
|||
|
||||
def execute_hooks
|
||||
return unless project
|
||||
build_data = Gitlab::BuildDataBuilder.build(self)
|
||||
build_data = Gitlab::DataBuilder::Build.build(self)
|
||||
project.execute_hooks(build_data.dup, :build_hooks)
|
||||
project.execute_services(build_data.dup, :build_hooks)
|
||||
project.running_or_pending_build_count(force: true)
|
||||
|
|
|
@ -19,6 +19,8 @@ module Ci
|
|||
|
||||
after_save :keep_around_commits
|
||||
|
||||
delegate :stages, to: :statuses
|
||||
|
||||
state_machine :status, initial: :created do
|
||||
event :enqueue do
|
||||
transition created: :pending
|
||||
|
@ -56,6 +58,10 @@ module Ci
|
|||
before_transition do |pipeline|
|
||||
pipeline.update_duration
|
||||
end
|
||||
|
||||
after_transition do |pipeline, transition|
|
||||
pipeline.execute_hooks unless transition.loopback?
|
||||
end
|
||||
end
|
||||
|
||||
# ref can't be HEAD or SHA, can only be branch/tag name
|
||||
|
@ -243,8 +249,18 @@ module Ci
|
|||
self.duration = statuses.latest.duration
|
||||
end
|
||||
|
||||
def execute_hooks
|
||||
data = pipeline_data
|
||||
project.execute_hooks(data, :pipeline_hooks)
|
||||
project.execute_services(data, :pipeline_hooks)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pipeline_data
|
||||
Gitlab::DataBuilder::Pipeline.build(self)
|
||||
end
|
||||
|
||||
def latest_builds_status
|
||||
return 'failed' unless yaml_errors.blank?
|
||||
|
||||
|
|
7
app/models/concerns/protected_branch_access.rb
Normal file
7
app/models/concerns/protected_branch_access.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module ProtectedBranchAccess
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def humanize
|
||||
self.class.human_access_levels[self.access_level]
|
||||
end
|
||||
end
|
|
@ -1,9 +1,32 @@
|
|||
module Spammable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
def attr_spammable(attr, options = {})
|
||||
spammable_attrs << [attr.to_s, options]
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
has_one :user_agent_detail, as: :subject, dependent: :destroy
|
||||
|
||||
attr_accessor :spam
|
||||
|
||||
after_validation :check_for_spam, on: :create
|
||||
|
||||
cattr_accessor :spammable_attrs, instance_accessor: false do
|
||||
[]
|
||||
end
|
||||
|
||||
delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
|
||||
end
|
||||
|
||||
def submittable_as_spam?
|
||||
if user_agent_detail
|
||||
user_agent_detail.submittable?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def spam?
|
||||
|
@ -13,4 +36,33 @@ module Spammable
|
|||
def check_for_spam
|
||||
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
|
||||
end
|
||||
|
||||
def spam_title
|
||||
attr = self.class.spammable_attrs.find do |_, options|
|
||||
options.fetch(:spam_title, false)
|
||||
end
|
||||
|
||||
public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
|
||||
end
|
||||
|
||||
def spam_description
|
||||
attr = self.class.spammable_attrs.find do |_, options|
|
||||
options.fetch(:spam_description, false)
|
||||
end
|
||||
|
||||
public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
|
||||
end
|
||||
|
||||
def spammable_text
|
||||
result = self.class.spammable_attrs.map do |attr|
|
||||
public_send(attr.first)
|
||||
end
|
||||
|
||||
result.reject(&:blank?).join("\n")
|
||||
end
|
||||
|
||||
# Override in Spammable if further checks are necessary
|
||||
def check_for_spam?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,5 +5,6 @@ class ProjectHook < WebHook
|
|||
scope :note_hooks, -> { where(note_events: true) }
|
||||
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
|
||||
scope :build_hooks, -> { where(build_events: true) }
|
||||
scope :pipeline_hooks, -> { where(pipeline_events: true) }
|
||||
scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base
|
|||
default_value_for :merge_requests_events, false
|
||||
default_value_for :tag_push_events, false
|
||||
default_value_for :build_events, false
|
||||
default_value_for :pipeline_events, false
|
||||
default_value_for :enable_ssl_verification, true
|
||||
|
||||
scope :push_hooks, -> { where(push_events: true) }
|
||||
|
|
|
@ -36,6 +36,9 @@ class Issue < ActiveRecord::Base
|
|||
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
|
||||
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
|
||||
|
||||
attr_spammable :title, spam_title: true
|
||||
attr_spammable :description, spam_description: true
|
||||
|
||||
state_machine :state, initial: :opened do
|
||||
event :close do
|
||||
transition [:reopened, :opened] => :closed
|
||||
|
@ -262,4 +265,9 @@ class Issue < ActiveRecord::Base
|
|||
def overdue?
|
||||
due_date.try(:past?) || false
|
||||
end
|
||||
|
||||
# Only issues on public projects should be checked for spam
|
||||
def check_for_spam?
|
||||
project.public?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -197,6 +197,8 @@ class Project < ActiveRecord::Base
|
|||
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
|
||||
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
|
||||
|
||||
scope :excluding_project, ->(project) { where.not(id: project) }
|
||||
|
||||
state_machine :import_status, initial: :none do
|
||||
event :import_start do
|
||||
transition [:none, :finished] => :started
|
||||
|
|
|
@ -51,8 +51,7 @@ class BuildsEmailService < Service
|
|||
end
|
||||
|
||||
def test_data(project = nil, user = nil)
|
||||
build = project.builds.last
|
||||
Gitlab::BuildDataBuilder.build(build)
|
||||
Gitlab::DataBuilder::Build.build(project.builds.last)
|
||||
end
|
||||
|
||||
def fields
|
||||
|
|
|
@ -5,11 +5,14 @@ class ProtectedBranch < ActiveRecord::Base
|
|||
validates :name, presence: true
|
||||
validates :project, presence: true
|
||||
|
||||
has_one :merge_access_level, dependent: :destroy
|
||||
has_one :push_access_level, dependent: :destroy
|
||||
has_many :merge_access_levels, dependent: :destroy
|
||||
has_many :push_access_levels, dependent: :destroy
|
||||
|
||||
accepts_nested_attributes_for :push_access_level
|
||||
accepts_nested_attributes_for :merge_access_level
|
||||
validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
|
||||
validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
|
||||
|
||||
accepts_nested_attributes_for :push_access_levels
|
||||
accepts_nested_attributes_for :merge_access_levels
|
||||
|
||||
def commit
|
||||
project.commit(self.name)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
|
||||
include ProtectedBranchAccess
|
||||
|
||||
belongs_to :protected_branch
|
||||
delegate :project, to: :protected_branch
|
||||
|
||||
|
@ -17,8 +19,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
|
|||
|
||||
project.team.max_member_access(user.id) >= access_level
|
||||
end
|
||||
|
||||
def humanize
|
||||
self.class.human_access_levels[self.access_level]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
|
||||
include ProtectedBranchAccess
|
||||
|
||||
belongs_to :protected_branch
|
||||
delegate :project, to: :protected_branch
|
||||
|
||||
|
@ -20,8 +22,4 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
|
|||
|
||||
project.team.max_member_access(user.id) >= access_level
|
||||
end
|
||||
|
||||
def humanize
|
||||
self.class.human_access_levels[self.access_level]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,6 +36,7 @@ class Service < ActiveRecord::Base
|
|||
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
|
||||
scope :note_hooks, -> { where(note_events: true, active: true) }
|
||||
scope :build_hooks, -> { where(build_events: true, active: true) }
|
||||
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
|
||||
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
|
||||
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
|
||||
|
||||
|
@ -79,13 +80,17 @@ class Service < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def test_data(project, user)
|
||||
Gitlab::PushDataBuilder.build_sample(project, user)
|
||||
Gitlab::DataBuilder::Push.build_sample(project, user)
|
||||
end
|
||||
|
||||
def event_channel_names
|
||||
[]
|
||||
end
|
||||
|
||||
def event_names
|
||||
supported_events.map { |event| "#{event}_events" }
|
||||
end
|
||||
|
||||
def event_field(event)
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
|
|||
user.block
|
||||
user.destroy
|
||||
end
|
||||
|
||||
def text
|
||||
[title, description].join("\n")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -429,6 +429,13 @@ class User < ActiveRecord::Base
|
|||
owned_groups.select(:id), namespace.id).joins(:namespace)
|
||||
end
|
||||
|
||||
# Returns projects which user can admin issues on (for example to move an issue to that project).
|
||||
#
|
||||
# This logic is duplicated from `Ability#project_abilities` into a SQL form.
|
||||
def projects_where_can_admin_issues
|
||||
authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
|
||||
end
|
||||
|
||||
def is_admin?
|
||||
admin
|
||||
end
|
||||
|
|
9
app/models/user_agent_detail.rb
Normal file
9
app/models/user_agent_detail.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class UserAgentDetail < ActiveRecord::Base
|
||||
belongs_to :subject, polymorphic: true
|
||||
|
||||
validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
|
||||
|
||||
def submittable?
|
||||
!submitted?
|
||||
end
|
||||
end
|
79
app/services/akismet_service.rb
Normal file
79
app/services/akismet_service.rb
Normal file
|
@ -0,0 +1,79 @@
|
|||
class AkismetService
|
||||
attr_accessor :owner, :text, :options
|
||||
|
||||
def initialize(owner, text, options = {})
|
||||
@owner = owner
|
||||
@text = text
|
||||
@options = options
|
||||
end
|
||||
|
||||
def is_spam?
|
||||
return false unless akismet_enabled?
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
created_at: DateTime.now,
|
||||
author: owner.name,
|
||||
author_email: owner.email,
|
||||
referrer: options[:referrer],
|
||||
}
|
||||
|
||||
begin
|
||||
is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
|
||||
is_spam || is_blatant
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def submit_ham
|
||||
return false unless akismet_enabled?
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
author: owner.name,
|
||||
author_email: owner.email
|
||||
}
|
||||
|
||||
begin
|
||||
akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def submit_spam
|
||||
return false unless akismet_enabled?
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
author: owner.name,
|
||||
author_email: owner.email
|
||||
}
|
||||
|
||||
begin
|
||||
akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def akismet_client
|
||||
@akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
|
||||
Gitlab.config.gitlab.url)
|
||||
end
|
||||
|
||||
def akismet_enabled?
|
||||
current_application_settings.akismet_enabled
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
class CreateSpamLogService < BaseService
|
||||
def initialize(project, user, params)
|
||||
super(project, user, params)
|
||||
end
|
||||
|
||||
def execute
|
||||
spam_params = params.merge({ user_id: @current_user.id,
|
||||
project_id: @project.id } )
|
||||
spam_log = SpamLog.new(spam_params)
|
||||
spam_log.save
|
||||
spam_log
|
||||
end
|
||||
end
|
|
@ -39,7 +39,12 @@ class DeleteBranchService < BaseService
|
|||
end
|
||||
|
||||
def build_push_data(branch)
|
||||
Gitlab::PushDataBuilder
|
||||
.build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
|
||||
Gitlab::DataBuilder::Push.build(
|
||||
project,
|
||||
current_user,
|
||||
branch.target.sha,
|
||||
Gitlab::Git::BLANK_SHA,
|
||||
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
|
||||
[])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,12 @@ class DeleteTagService < BaseService
|
|||
end
|
||||
|
||||
def build_push_data(tag)
|
||||
Gitlab::PushDataBuilder
|
||||
.build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
|
||||
Gitlab::DataBuilder::Push.build(
|
||||
project,
|
||||
current_user,
|
||||
tag.target.sha,
|
||||
Gitlab::Git::BLANK_SHA,
|
||||
"#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
|
||||
[])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -91,12 +91,12 @@ class GitPushService < BaseService
|
|||
|
||||
params = {
|
||||
name: @project.default_branch,
|
||||
push_access_level_attributes: {
|
||||
push_access_levels_attributes: [{
|
||||
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
},
|
||||
merge_access_level_attributes: {
|
||||
}],
|
||||
merge_access_levels_attributes: [{
|
||||
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
ProtectedBranches::CreateService.new(@project, current_user, params).execute
|
||||
|
@ -138,13 +138,23 @@ class GitPushService < BaseService
|
|||
end
|
||||
|
||||
def build_push_data
|
||||
@push_data ||= Gitlab::PushDataBuilder.
|
||||
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
|
||||
@push_data ||= Gitlab::DataBuilder::Push.build(
|
||||
@project,
|
||||
current_user,
|
||||
params[:oldrev],
|
||||
params[:newrev],
|
||||
params[:ref],
|
||||
push_commits)
|
||||
end
|
||||
|
||||
def build_push_data_system_hook
|
||||
@push_data_system ||= Gitlab::PushDataBuilder.
|
||||
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
|
||||
@push_data_system ||= Gitlab::DataBuilder::Push.build(
|
||||
@project,
|
||||
current_user,
|
||||
params[:oldrev],
|
||||
params[:newrev],
|
||||
params[:ref],
|
||||
[])
|
||||
end
|
||||
|
||||
def push_to_existing_branch?
|
||||
|
|
|
@ -34,12 +34,24 @@ class GitTagPushService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
Gitlab::PushDataBuilder.
|
||||
build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
|
||||
Gitlab::DataBuilder::Push.build(
|
||||
project,
|
||||
current_user,
|
||||
params[:oldrev],
|
||||
params[:newrev],
|
||||
params[:ref],
|
||||
commits,
|
||||
message)
|
||||
end
|
||||
|
||||
def build_system_push_data
|
||||
Gitlab::PushDataBuilder.
|
||||
build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
|
||||
Gitlab::DataBuilder::Push.build(
|
||||
project,
|
||||
current_user,
|
||||
params[:oldrev],
|
||||
params[:newrev],
|
||||
params[:ref],
|
||||
[],
|
||||
'')
|
||||
end
|
||||
end
|
||||
|
|
26
app/services/ham_service.rb
Normal file
26
app/services/ham_service.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
class HamService
|
||||
attr_accessor :spam_log
|
||||
|
||||
def initialize(spam_log)
|
||||
@spam_log = spam_log
|
||||
end
|
||||
|
||||
def mark_as_ham!
|
||||
if akismet.submit_ham
|
||||
spam_log.update_attribute(:submitted_as_ham, true)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def akismet
|
||||
@akismet ||= AkismetService.new(
|
||||
spam_log.user,
|
||||
spam_log.text,
|
||||
ip_address: spam_log.source_ip,
|
||||
user_agent: spam_log.user_agent
|
||||
)
|
||||
end
|
||||
end
|
|
@ -3,29 +3,34 @@ module Issues
|
|||
def execute
|
||||
filter_params
|
||||
label_params = params.delete(:label_ids)
|
||||
request = params.delete(:request)
|
||||
api = params.delete(:api)
|
||||
issue = project.issues.new(params)
|
||||
issue.author = params[:author] || current_user
|
||||
@request = params.delete(:request)
|
||||
@api = params.delete(:api)
|
||||
@issue = project.issues.new(params)
|
||||
@issue.author = params[:author] || current_user
|
||||
|
||||
issue.spam = spam_check_service.execute(request, api)
|
||||
@issue.spam = spam_service.check(@api)
|
||||
|
||||
if issue.save
|
||||
issue.update_attributes(label_ids: label_params)
|
||||
notification_service.new_issue(issue, current_user)
|
||||
todo_service.new_issue(issue, current_user)
|
||||
event_service.open_issue(issue, current_user)
|
||||
issue.create_cross_references!(current_user)
|
||||
execute_hooks(issue, 'open')
|
||||
if @issue.save
|
||||
@issue.update_attributes(label_ids: label_params)
|
||||
notification_service.new_issue(@issue, current_user)
|
||||
todo_service.new_issue(@issue, current_user)
|
||||
event_service.open_issue(@issue, current_user)
|
||||
user_agent_detail_service.create
|
||||
@issue.create_cross_references!(current_user)
|
||||
execute_hooks(@issue, 'open')
|
||||
end
|
||||
|
||||
issue
|
||||
@issue
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def spam_check_service
|
||||
SpamCheckService.new(project, current_user, params)
|
||||
def spam_service
|
||||
SpamService.new(@issue, @request)
|
||||
end
|
||||
|
||||
def user_agent_detail_service
|
||||
UserAgentDetailService.new(@issue, @request)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ module Notes
|
|||
end
|
||||
|
||||
def hook_data
|
||||
Gitlab::NoteDataBuilder.build(@note, @note.author)
|
||||
Gitlab::DataBuilder::Note.build(@note, @note.author)
|
||||
end
|
||||
|
||||
def execute_note_hooks
|
||||
|
|
|
@ -5,23 +5,7 @@ module ProtectedBranches
|
|||
def execute
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
||||
|
||||
protected_branch = project.protected_branches.new(params)
|
||||
|
||||
ProtectedBranch.transaction do
|
||||
protected_branch.save!
|
||||
|
||||
if protected_branch.push_access_level.blank?
|
||||
protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER)
|
||||
end
|
||||
|
||||
if protected_branch.merge_access_level.blank?
|
||||
protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER)
|
||||
end
|
||||
end
|
||||
|
||||
protected_branch
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
protected_branch
|
||||
project.protected_branches.create(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
class SpamCheckService < BaseService
|
||||
include Gitlab::AkismetHelper
|
||||
|
||||
attr_accessor :request, :api
|
||||
|
||||
def execute(request, api)
|
||||
@request, @api = request, api
|
||||
return false unless request || check_for_spam?(project)
|
||||
return false unless is_spam?(request.env, current_user, text)
|
||||
|
||||
create_spam_log
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def text
|
||||
[params[:title], params[:description]].reject(&:blank?).join("\n")
|
||||
end
|
||||
|
||||
def spam_log_attrs
|
||||
{
|
||||
user_id: current_user.id,
|
||||
project_id: project.id,
|
||||
title: params[:title],
|
||||
description: params[:description],
|
||||
source_ip: client_ip(request.env),
|
||||
user_agent: user_agent(request.env),
|
||||
noteable_type: 'Issue',
|
||||
via_api: api
|
||||
}
|
||||
end
|
||||
|
||||
def create_spam_log
|
||||
CreateSpamLogService.new(project, current_user, spam_log_attrs).execute
|
||||
end
|
||||
end
|
78
app/services/spam_service.rb
Normal file
78
app/services/spam_service.rb
Normal file
|
@ -0,0 +1,78 @@
|
|||
class SpamService
|
||||
attr_accessor :spammable, :request, :options
|
||||
|
||||
def initialize(spammable, request = nil)
|
||||
@spammable = spammable
|
||||
@request = request
|
||||
@options = {}
|
||||
|
||||
if @request
|
||||
@options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
|
||||
@options[:user_agent] = @request.env['HTTP_USER_AGENT']
|
||||
@options[:referrer] = @request.env['HTTP_REFERRER']
|
||||
else
|
||||
@options[:ip_address] = @spammable.ip_address
|
||||
@options[:user_agent] = @spammable.user_agent
|
||||
end
|
||||
end
|
||||
|
||||
def check(api = false)
|
||||
return false unless request && check_for_spam?
|
||||
|
||||
return false unless akismet.is_spam?
|
||||
|
||||
create_spam_log(api)
|
||||
true
|
||||
end
|
||||
|
||||
def mark_as_spam!
|
||||
return false unless spammable.submittable_as_spam?
|
||||
|
||||
if akismet.submit_spam
|
||||
spammable.user_agent_detail.update_attribute(:submitted, true)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def akismet
|
||||
@akismet ||= AkismetService.new(
|
||||
spammable_owner,
|
||||
spammable.spammable_text,
|
||||
options
|
||||
)
|
||||
end
|
||||
|
||||
def spammable_owner
|
||||
@user ||= User.find(spammable_owner_id)
|
||||
end
|
||||
|
||||
def spammable_owner_id
|
||||
@owner_id ||=
|
||||
if spammable.respond_to?(:author_id)
|
||||
spammable.author_id
|
||||
elsif spammable.respond_to?(:creator_id)
|
||||
spammable.creator_id
|
||||
end
|
||||
end
|
||||
|
||||
def check_for_spam?
|
||||
spammable.check_for_spam?
|
||||
end
|
||||
|
||||
def create_spam_log(api)
|
||||
SpamLog.create(
|
||||
{
|
||||
user_id: spammable_owner_id,
|
||||
title: spammable.spam_title,
|
||||
description: spammable.spam_description,
|
||||
source_ip: options[:ip_address],
|
||||
user_agent: options[:user_agent],
|
||||
noteable_type: spammable.class.to_s,
|
||||
via_api: api
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
class TestHookService
|
||||
def execute(hook, current_user)
|
||||
data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user)
|
||||
data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user)
|
||||
hook.execute(data, 'push_hooks')
|
||||
end
|
||||
end
|
||||
|
|
13
app/services/user_agent_detail_service.rb
Normal file
13
app/services/user_agent_detail_service.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class UserAgentDetailService
|
||||
attr_accessor :spammable, :request
|
||||
|
||||
def initialize(spammable, request)
|
||||
@spammable, @request = spammable, request
|
||||
end
|
||||
|
||||
def create
|
||||
return unless request
|
||||
|
||||
spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
|
||||
end
|
||||
end
|
|
@ -24,6 +24,11 @@
|
|||
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
|
||||
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
|
||||
%td
|
||||
- if spam_log.submitted_as_ham?
|
||||
.btn.btn-xs.disabled
|
||||
Submitted as ham
|
||||
- else
|
||||
= link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
|
||||
- if user && !user.blocked?
|
||||
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
|
||||
- else
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
|
||||
= author_avatar(todo, size: 40)
|
||||
|
||||
.todo-item.todo-block
|
||||
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
|
||||
.todo-title.title
|
||||
- unless todo.build_failed?
|
||||
= todo_target_state_pill(todo)
|
||||
|
@ -19,13 +20,13 @@
|
|||
|
||||
· #{time_ago_with_tooltip(todo.created_at)}
|
||||
|
||||
- if todo.pending?
|
||||
.todo-actions.pull-right
|
||||
= link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
|
||||
Done
|
||||
= icon('spinner spin')
|
||||
|
||||
.todo-body
|
||||
.todo-note
|
||||
.md
|
||||
= event_note(todo.body, project: todo.project)
|
||||
|
||||
- if todo.pending?
|
||||
.todo-actions
|
||||
= link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
|
||||
Done
|
||||
= icon('spinner spin')
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
.encoding-selector
|
||||
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
|
||||
|
||||
.file-content.code
|
||||
.file-editor.code
|
||||
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
|
||||
- if local_assigns[:path]
|
||||
.js-edit-mode-pane#preview.hide
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.col-md-8.col-lg-7
|
||||
%strong.light-header= hook.url
|
||||
%div
|
||||
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger|
|
||||
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
|
||||
- if hook.send(trigger)
|
||||
%span.label.label-gray.deploy-project-label= trigger.titleize
|
||||
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
|
||||
|
|
|
@ -37,14 +37,19 @@
|
|||
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
|
||||
%li
|
||||
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
|
||||
- if @issue.submittable_as_spam? && current_user.admin?
|
||||
%li
|
||||
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
|
||||
|
||||
- if can?(current_user, :create_issue, @project)
|
||||
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
|
||||
New issue
|
||||
- if can?(current_user, :update_issue, @issue)
|
||||
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
|
||||
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
|
||||
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
|
||||
Edit
|
||||
- if @issue.submittable_as_spam? && current_user.admin?
|
||||
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
|
||||
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
|
||||
|
||||
|
||||
.issue-details.issuable-details
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
= link_to "#", class: 'btn js-toggle-button import_git' do
|
||||
= icon('git', text: 'Repo by URL')
|
||||
%div{ class: 'import_gitlab_project' }
|
||||
- if gitlab_project_import_enabled?
|
||||
- if gitlab_project_import_enabled? && current_user.is_admin?
|
||||
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
|
||||
= icon('gitlab', text: 'GitLab export')
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
Protect a branch
|
||||
.panel-body
|
||||
.form-horizontal
|
||||
= form_errors(@protected_branch)
|
||||
.form-group
|
||||
= f.label :name, class: 'col-md-2 text-right' do
|
||||
Branch:
|
||||
|
@ -18,19 +19,19 @@
|
|||
%code production/*
|
||||
are supported
|
||||
.form-group
|
||||
%label.col-md-2.text-right{ for: 'merge_access_level_attributes' }
|
||||
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
|
||||
Allowed to merge:
|
||||
.col-md-10
|
||||
= dropdown_tag('Select',
|
||||
options: { toggle_class: 'js-allowed-to-merge wide',
|
||||
data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]', input_id: 'merge_access_level_attributes' }})
|
||||
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
|
||||
.form-group
|
||||
%label.col-md-2.text-right{ for: 'push_access_level_attributes' }
|
||||
%label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
|
||||
Allowed to push:
|
||||
.col-md-10
|
||||
= dropdown_tag('Select',
|
||||
options: { toggle_class: 'js-allowed-to-push wide',
|
||||
data: { field_name: 'protected_branch[push_access_level_attributes][access_level]', input_id: 'push_access_level_attributes' }})
|
||||
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
|
||||
|
||||
.panel-footer
|
||||
= f.submit 'Protect', class: 'btn-create btn', disabled: true
|
||||
|
|
|
@ -13,16 +13,9 @@
|
|||
= time_ago_with_tooltip(commit.committed_date)
|
||||
- else
|
||||
(branch was removed from repository)
|
||||
%td
|
||||
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level
|
||||
= dropdown_tag( (protected_branch.merge_access_level.humanize || 'Select') ,
|
||||
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
|
||||
data: { field_name: "allowed_to_merge_#{protected_branch.id}" }})
|
||||
%td
|
||||
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level
|
||||
= dropdown_tag( (protected_branch.push_access_level.humanize || 'Select') ,
|
||||
options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
|
||||
data: { field_name: "allowed_to_push_#{protected_branch.id}" }})
|
||||
|
||||
= render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
|
||||
|
||||
- if can_admin_project
|
||||
%td
|
||||
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
%td
|
||||
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
|
||||
= dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
|
||||
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
|
||||
data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
|
||||
%td
|
||||
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
|
||||
= dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
|
||||
options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
|
||||
data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
|
|
@ -45,7 +45,7 @@
|
|||
.filter-item.inline
|
||||
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
|
||||
.filter-item.inline.labels-filter
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
|
||||
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
|
||||
.filter-item.inline
|
||||
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
|
||||
%ul
|
||||
|
|
|
@ -2,7 +2,22 @@
|
|||
|
||||
.form-group
|
||||
= f.label :title, class: 'control-label'
|
||||
.col-sm-10
|
||||
|
||||
- issuable_template_names = issuable_templates(issuable)
|
||||
|
||||
- if issuable_template_names.any?
|
||||
.col-sm-3.col-lg-2
|
||||
.js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } }
|
||||
- title = selected_template(issuable) || "Choose a template"
|
||||
|
||||
= dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
|
||||
title: title, filter: true, placeholder: 'Filter', footer_content: true,
|
||||
data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
|
||||
%ul.dropdown-footer-list
|
||||
%li
|
||||
%a.reset-template
|
||||
Reset template
|
||||
%div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
|
||||
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
|
||||
class: 'form-control pad', required: true
|
||||
|
||||
|
@ -23,6 +38,13 @@
|
|||
to prevent a
|
||||
%strong Work In Progress
|
||||
merge request from being merged before it's ready.
|
||||
|
||||
- if can_add_template?(issuable)
|
||||
%p.help-block
|
||||
Add
|
||||
= link_to "issuable templates", help_page_path('workflow/description_templates')
|
||||
to help your contributors communicate effectively!
|
||||
|
||||
.form-group.detail-page-description
|
||||
= f.label :description, 'Description', class: 'control-label'
|
||||
.col-sm-10
|
||||
|
|
|
@ -29,49 +29,56 @@
|
|||
= f.label :push_events, class: 'list-label' do
|
||||
%strong Push events
|
||||
%p.light
|
||||
This url will be triggered by a push to the repository
|
||||
This URL will be triggered by a push to the repository
|
||||
%li
|
||||
= f.check_box :tag_push_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :tag_push_events, class: 'list-label' do
|
||||
%strong Tag push events
|
||||
%p.light
|
||||
This url will be triggered when a new tag is pushed to the repository
|
||||
This URL will be triggered when a new tag is pushed to the repository
|
||||
%li
|
||||
= f.check_box :note_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :note_events, class: 'list-label' do
|
||||
%strong Comments
|
||||
%p.light
|
||||
This url will be triggered when someone adds a comment
|
||||
This URL will be triggered when someone adds a comment
|
||||
%li
|
||||
= f.check_box :issues_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :issues_events, class: 'list-label' do
|
||||
%strong Issues events
|
||||
%p.light
|
||||
This url will be triggered when an issue is created/updated/merged
|
||||
This URL will be triggered when an issue is created/updated/merged
|
||||
%li
|
||||
= f.check_box :merge_requests_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :merge_requests_events, class: 'list-label' do
|
||||
%strong Merge Request events
|
||||
%p.light
|
||||
This url will be triggered when a merge request is created/updated/merged
|
||||
This URL will be triggered when a merge request is created/updated/merged
|
||||
%li
|
||||
= f.check_box :build_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :build_events, class: 'list-label' do
|
||||
%strong Build events
|
||||
%p.light
|
||||
This url will be triggered when the build status changes
|
||||
This URL will be triggered when the build status changes
|
||||
%li
|
||||
= f.check_box :pipeline_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :pipeline_events, class: 'list-label' do
|
||||
%strong Pipeline events
|
||||
%p.light
|
||||
This URL will be triggered when the pipeline status changes
|
||||
%li
|
||||
= f.check_box :wiki_page_events, class: 'pull-left'
|
||||
.prepend-left-20
|
||||
= f.label :wiki_page_events, class: 'list-label' do
|
||||
%strong Wiki Page events
|
||||
%p.light
|
||||
This url will be triggered when a wiki page is created/updated
|
||||
This URL will be triggered when a wiki page is created/updated
|
||||
.form-group
|
||||
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
|
||||
.checkbox
|
||||
|
|
|
@ -252,7 +252,11 @@ Rails.application.routes.draw do
|
|||
resource :impersonation, only: :destroy
|
||||
|
||||
resources :abuse_reports, only: [:index, :destroy]
|
||||
resources :spam_logs, only: [:index, :destroy]
|
||||
resources :spam_logs, only: [:index, :destroy] do
|
||||
member do
|
||||
post :mark_as_ham
|
||||
end
|
||||
end
|
||||
|
||||
resources :applications
|
||||
|
||||
|
@ -524,6 +528,11 @@ Rails.application.routes.draw do
|
|||
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
|
||||
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
|
||||
|
||||
#
|
||||
# Templates
|
||||
#
|
||||
get '/templates/:template_type/:key' => 'templates#show', as: :template
|
||||
|
||||
scope do
|
||||
get(
|
||||
'/blob/*id/diff',
|
||||
|
@ -815,6 +824,7 @@ Rails.application.routes.draw do
|
|||
member do
|
||||
post :toggle_subscription
|
||||
post :toggle_award_emoji
|
||||
post :mark_as_spam
|
||||
get :referenced_merge_requests
|
||||
get :related_branches
|
||||
get :can_create_branch
|
||||
|
|
18
db/migrate/20160727163552_create_user_agent_details.rb
Normal file
18
db/migrate/20160727163552_create_user_agent_details.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
class CreateUserAgentDetails < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :user_agent_details do |t|
|
||||
t.string :user_agent, null: false
|
||||
t.string :ip_address, null: false
|
||||
t.integer :subject_id, null: false
|
||||
t.string :subject_type, null: false
|
||||
t.boolean :submitted, default: false, null: false
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
class AddPipelineEventsToWebHooks < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_column_with_default(:web_hooks, :pipeline_events, :boolean,
|
||||
default: false, allow_null: false)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column(:web_hooks, :pipeline_events)
|
||||
end
|
||||
end
|
16
db/migrate/20160728103734_add_pipeline_events_to_services.rb
Normal file
16
db/migrate/20160728103734_add_pipeline_events_to_services.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class AddPipelineEventsToServices < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_column_with_default(:services, :pipeline_events, :boolean,
|
||||
default: false, allow_null: false)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column(:services, :pipeline_events)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class RemoveProjectIdFromSpamLogs < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = true
|
||||
|
||||
# When a migration requires downtime you **must** uncomment the following
|
||||
# constant and define a short and easy to understand explanation as to why the
|
||||
# migration requires downtime.
|
||||
DOWNTIME_REASON = 'Removing a column that contains data that is not used anywhere.'
|
||||
|
||||
# When using the methods "add_concurrent_index" or "add_column_with_default"
|
||||
# you must disable the use of transactions as these methods can not run in an
|
||||
# existing transaction. When using "add_concurrent_index" make sure that this
|
||||
# method is the _only_ method called in the migration, any other changes
|
||||
# should go in a separate migration. This ensures that upon failure _only_ the
|
||||
# index creation fails and can be retried or reverted easily.
|
||||
#
|
||||
# To disable transactions uncomment the following line and remove these
|
||||
# comments:
|
||||
# disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
remove_column :spam_logs, :project_id, :integer
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
# When a migration requires downtime you **must** uncomment the following
|
||||
# constant and define a short and easy to understand explanation as to why the
|
||||
# migration requires downtime.
|
||||
# DOWNTIME_REASON = ''
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -897,6 +897,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
|
|||
t.string "category", default: "common", null: false
|
||||
t.boolean "default", default: false
|
||||
t.boolean "wiki_page_events", default: true
|
||||
t.boolean "pipeline_events", default: false, null: false
|
||||
end
|
||||
|
||||
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
|
||||
|
@ -926,12 +927,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do
|
|||
t.string "source_ip"
|
||||
t.string "user_agent"
|
||||
t.boolean "via_api"
|
||||
t.integer "project_id"
|
||||
t.string "noteable_type"
|
||||
t.string "title"
|
||||
t.text "description"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "submitted_as_ham", default: false, null: false
|
||||
end
|
||||
|
||||
create_table "subscriptions", force: :cascade do |t|
|
||||
|
@ -999,6 +1000,16 @@ ActiveRecord::Schema.define(version: 20160810142633) do
|
|||
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
|
||||
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
|
||||
|
||||
create_table "user_agent_details", force: :cascade do |t|
|
||||
t.string "user_agent", null: false
|
||||
t.string "ip_address", null: false
|
||||
t.integer "subject_id", null: false
|
||||
t.string "subject_type", null: false
|
||||
t.boolean "submitted", default: false, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
|
@ -1100,6 +1111,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
|
|||
t.boolean "build_events", default: false, null: false
|
||||
t.boolean "wiki_page_events", default: false, null: false
|
||||
t.string "token"
|
||||
t.boolean "pipeline_events", default: false, null: false
|
||||
end
|
||||
|
||||
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
|
||||
|
|
|
@ -30,7 +30,11 @@
|
|||
- [Rake tasks](rake_tasks.md) for development
|
||||
- [Shell commands](shell_commands.md) in the GitLab codebase
|
||||
- [Sidekiq debugging](sidekiq_debugging.md)
|
||||
|
||||
## Databases
|
||||
|
||||
- [What requires downtime?](what_requires_downtime.md)
|
||||
- [Adding database indexes](adding_database_indexes.md)
|
||||
|
||||
## Compliance
|
||||
|
||||
|
|
123
doc/development/adding_database_indexes.md
Normal file
123
doc/development/adding_database_indexes.md
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Adding Database Indexes
|
||||
|
||||
Indexes can be used to speed up database queries, but when should you add a new
|
||||
index? Traditionally the answer to this question has been to add an index for
|
||||
every column used for filtering or joining data. For example, consider the
|
||||
following query:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM projects
|
||||
WHERE user_id = 2;
|
||||
```
|
||||
|
||||
Here we are filtering by the `user_id` column and as such a developer may decide
|
||||
to index this column.
|
||||
|
||||
While in certain cases indexing columns using the above approach may make sense
|
||||
it can actually have a negative impact. Whenever you write data to a table any
|
||||
existing indexes need to be updated. The more indexes there are the slower this
|
||||
can potentially become. Indexes can also take up quite some disk space depending
|
||||
on the amount of data indexed and the index type. For example, PostgreSQL offers
|
||||
"GIN" indexes which can be used to index certain data types that can not be
|
||||
indexed by regular btree indexes. These indexes however generally take up more
|
||||
data and are slower to update compared to btree indexes.
|
||||
|
||||
Because of all this one should not blindly add a new index for every column used
|
||||
to filter data by. Instead one should ask themselves the following questions:
|
||||
|
||||
1. Can I write my query in such a way that it re-uses as many existing indexes
|
||||
as possible?
|
||||
2. Is the data going to be large enough that using an index will actually be
|
||||
faster than just iterating over the rows in the table?
|
||||
3. Is the overhead of maintaining the index worth the reduction in query
|
||||
timings?
|
||||
|
||||
We'll explore every question in detail below.
|
||||
|
||||
## Re-using Queries
|
||||
|
||||
The first step is to make sure your query re-uses as many existing indexes as
|
||||
possible. For example, consider the following query:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM todos
|
||||
WHERE user_id = 123
|
||||
AND state = 'open';
|
||||
```
|
||||
|
||||
Now imagine we already have an index on the `user_id` column but not on the
|
||||
`state` column. One may think this query will perform badly due to `state` being
|
||||
unindexed. In reality the query may perform just fine given the index on
|
||||
`user_id` can filter out enough rows.
|
||||
|
||||
The best way to determine if indexes are re-used is to run your query using
|
||||
`EXPLAIN ANALYZE`. Depending on any extra tables that may be joined and
|
||||
other columns being used for filtering you may find an extra index is not going
|
||||
to make much (if any) difference. On the other hand you may determine that the
|
||||
index _may_ make a difference.
|
||||
|
||||
In short:
|
||||
|
||||
1. Try to write your query in such a way that it re-uses as many existing
|
||||
indexes as possible.
|
||||
2. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
|
||||
ideal query.
|
||||
|
||||
## Data Size
|
||||
|
||||
A database may decide not to use an index despite it existing in case a regular
|
||||
sequence scan (= simply iterating over all existing rows) is faster. This is
|
||||
especially the case for small tables.
|
||||
|
||||
If a table is expected to grow in size and you expect your query has to filter
|
||||
out a lot of rows you may want to consider adding an index. If the table size is
|
||||
very small (e.g. only a handful of rows) or any existing indexes filter out
|
||||
enough rows you may _not_ want to add a new index.
|
||||
|
||||
## Maintenance Overhead
|
||||
|
||||
Indexes have to be updated on every table write. In case of PostgreSQL _all_
|
||||
existing indexes will be updated whenever data is written to a table. As a
|
||||
result of this having many indexes on the same table will slow down writes.
|
||||
|
||||
Because of this one should ask themselves: is the reduction in query performance
|
||||
worth the overhead of maintaining an extra index?
|
||||
|
||||
If adding an index reduces SELECT timings by 5 milliseconds but increases
|
||||
INSERT/UPDATE/DELETE timings by 10 milliseconds then the index may not be worth
|
||||
it. On the other hand, if SELECT timings are reduced but INSERT/UPDATE/DELETE
|
||||
timings are not affected you may want to add the index after all.
|
||||
|
||||
## Finding Unused Indexes
|
||||
|
||||
To see which indexes are unused you can run the following query:
|
||||
|
||||
```sql
|
||||
SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
|
||||
FROM pg_stat_all_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND idx_scan = 0
|
||||
AND idx_tup_read = 0
|
||||
AND idx_tup_fetch = 0
|
||||
ORDER BY pg_relation_size(indexrelname::regclass) desc;
|
||||
```
|
||||
|
||||
This query outputs a list containing all indexes that are never used and sorts
|
||||
them by indexes sizes in descending order. This query can be useful to
|
||||
determine if any previously indexes are useful after all. More information on
|
||||
the meaning of the various columns can be found at
|
||||
<https://www.postgresql.org/docs/current/static/monitoring-stats.html>.
|
||||
|
||||
Because the output of this query relies on the actual usage of your database it
|
||||
may be affected by factors such as (but not limited to):
|
||||
|
||||
* Certain queries never being executed, thus not being able to use certain
|
||||
indexes.
|
||||
* Certain tables having little data, resulting in PostgreSQL using sequence
|
||||
scans instead of index scans.
|
||||
|
||||
In other words, this data is only reliable for a frequently used database with
|
||||
plenty of data and with as many GitLab features enabled (and being used) as
|
||||
possible.
|
|
@ -15,11 +15,14 @@ repository and maintained by GitLab UX designers.
|
|||
## Navigation
|
||||
|
||||
GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
|
||||
This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo
|
||||
and the current user's profile picture. The content section contains a header and the content itself.
|
||||
The header describes the current GitLab page and what navigation is
|
||||
available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the
|
||||
project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group.
|
||||
This menu will be visible regardless of what page you visit.
|
||||
The content section contains a header and the content itself. The header describes the current GitLab page and what navigation is
|
||||
available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
|
||||
project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
|
||||
|
||||
You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
|
||||
along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
|
||||
|
||||
|
||||
### Adding new tab to header navigation
|
||||
|
||||
|
@ -99,3 +102,6 @@ Do not use both green and blue button in one form.
|
|||
display counts in the UI.
|
||||
|
||||
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
|
||||
[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
|
||||
[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
|
||||
[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
|
|
@ -22,14 +22,37 @@ To use Akismet:
|
|||
|
||||
2. Sign-in or create a new account.
|
||||
|
||||
3. Click on "Show" to reveal the API key.
|
||||
3. Click on **Show** to reveal the API key.
|
||||
|
||||
4. Go to Applications Settings on Admin Area (`admin/application_settings`)
|
||||
|
||||
5. Check the `Enable Akismet` checkbox
|
||||
5. Check the **Enable Akismet** checkbox
|
||||
|
||||
6. Fill in the API key from step 3.
|
||||
|
||||
7. Save the configuration.
|
||||
|
||||
![Screenshot of Akismet settings](img/akismet_settings.png)
|
||||
|
||||
|
||||
## Training
|
||||
|
||||
> *Note:* Training the Akismet filter is only available in 8.11 and above.
|
||||
|
||||
As a way to better recognize between spam and ham, you can train the Akismet
|
||||
filter whenever there is a false positive or false negative.
|
||||
|
||||
When an entry is recognized as spam, it is rejected and added to the Spam Logs.
|
||||
From here you can review if they are really spam. If one of them is not really
|
||||
spam, you can use the **Submit as ham** button to tell Akismet that it falsely
|
||||
recognized an entry as spam.
|
||||
|
||||
![Screenshot of Spam Logs](img/spam_log.png)
|
||||
|
||||
If an entry that is actually spam was not recognized as such, you will be able
|
||||
to also submit this to Akismet. The **Submit as spam** button will only appear
|
||||
to admin users.
|
||||
|
||||
![Screenshot of Issue](img/submit_issue.png)
|
||||
|
||||
Training Akismet will help it to recognize spam more accurately in the future.
|
||||
|
|
BIN
doc/integration/img/spam_log.png
Normal file
BIN
doc/integration/img/spam_log.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 183 KiB |
BIN
doc/integration/img/submit_issue.png
Normal file
BIN
doc/integration/img/submit_issue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
|
@ -7,8 +7,7 @@
|
|||
> than that of the exporter.
|
||||
> - For existing installations, the project import option has to be enabled in
|
||||
> application settings (`/admin/application_settings`) under 'Import sources'.
|
||||
> Ask your administrator if you don't see the **GitLab export** button when
|
||||
> creating a new project.
|
||||
> You will have to be an administrator to enable and use the import functionality.
|
||||
> - You can find some useful raketasks if you are an administrator in the
|
||||
> [import_export](../../../administration/raketasks/project_import_export.md)
|
||||
> raketask.
|
||||
|
|
|
@ -754,6 +754,174 @@ X-Gitlab-Event: Wiki Page Hook
|
|||
}
|
||||
```
|
||||
|
||||
## Pipeline events
|
||||
|
||||
Triggered on status change of Pipeline.
|
||||
|
||||
**Request Header**:
|
||||
|
||||
```
|
||||
X-Gitlab-Event: Pipeline Hook
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"object_kind": "pipeline",
|
||||
"object_attributes":{
|
||||
"id": 31,
|
||||
"ref": "master",
|
||||
"tag": false,
|
||||
"sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
|
||||
"before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
|
||||
"status": "success",
|
||||
"stages":[
|
||||
"build",
|
||||
"test",
|
||||
"deploy"
|
||||
],
|
||||
"created_at": "2016-08-12 15:23:28 UTC",
|
||||
"finished_at": "2016-08-12 15:26:29 UTC",
|
||||
"duration": 63
|
||||
},
|
||||
"user":{
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
|
||||
},
|
||||
"project":{
|
||||
"name": "Gitlab Test",
|
||||
"description": "Atque in sunt eos similique dolores voluptatem.",
|
||||
"web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
|
||||
"avatar_url": null,
|
||||
"git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
|
||||
"git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
|
||||
"namespace": "Gitlab Org",
|
||||
"visibility_level": 20,
|
||||
"path_with_namespace": "gitlab-org/gitlab-test",
|
||||
"default_branch": "master"
|
||||
},
|
||||
"commit":{
|
||||
"id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
|
||||
"message": "test\n",
|
||||
"timestamp": "2016-08-12T17:23:21+02:00",
|
||||
"url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
|
||||
"author":{
|
||||
"name": "User",
|
||||
"email": "user@gitlab.com"
|
||||
}
|
||||
},
|
||||
"builds":[
|
||||
{
|
||||
"id": 380,
|
||||
"stage": "deploy",
|
||||
"name": "production",
|
||||
"status": "skipped",
|
||||
"created_at": "2016-08-12 15:23:28 UTC",
|
||||
"started_at": null,
|
||||
"finished_at": null,
|
||||
"when": "manual",
|
||||
"manual": true,
|
||||
"user":{
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
|
||||
},
|
||||
"runner": null,
|
||||
"artifacts_file":{
|
||||
"filename": null,
|
||||
"size": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 377,
|
||||
"stage": "test",
|
||||
"name": "test-image",
|
||||
"status": "success",
|
||||
"created_at": "2016-08-12 15:23:28 UTC",
|
||||
"started_at": "2016-08-12 15:26:12 UTC",
|
||||
"finished_at": null,
|
||||
"when": "on_success",
|
||||
"manual": false,
|
||||
"user":{
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
|
||||
},
|
||||
"runner": null,
|
||||
"artifacts_file":{
|
||||
"filename": null,
|
||||
"size": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 378,
|
||||
"stage": "test",
|
||||
"name": "test-build",
|
||||
"status": "success",
|
||||
"created_at": "2016-08-12 15:23:28 UTC",
|
||||
"started_at": "2016-08-12 15:26:12 UTC",
|
||||
"finished_at": "2016-08-12 15:26:29 UTC",
|
||||
"when": "on_success",
|
||||
"manual": false,
|
||||
"user":{
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
|
||||
},
|
||||
"runner": null,
|
||||
"artifacts_file":{
|
||||
"filename": null,
|
||||
"size": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 376,
|
||||
"stage": "build",
|
||||
"name": "build-image",
|
||||
"status": "success",
|
||||
"created_at": "2016-08-12 15:23:28 UTC",
|
||||
"started_at": "2016-08-12 15:24:56 UTC",
|
||||
"finished_at": "2016-08-12 15:25:26 UTC",
|
||||
"when": "on_success",
|
||||
"manual": false,
|
||||
"user":{
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
|
||||
},
|
||||
"runner": null,
|
||||
"artifacts_file":{
|
||||
"filename": null,
|
||||
"size": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 379,
|
||||
"stage": "deploy",
|
||||
"name": "staging",
|
||||
"status": "created",
|
||||
"created_at": "2016-08-12 15:23:28 UTC",
|
||||
"started_at": null,
|
||||
"finished_at": null,
|
||||
"when": "on_success",
|
||||
"manual": false,
|
||||
"user":{
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
|
||||
},
|
||||
"runner": null,
|
||||
"artifacts_file":{
|
||||
"filename": null,
|
||||
"size": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Example webhook receiver
|
||||
|
||||
If you want to see GitLab's webhooks in action for testing purposes you can use
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
- [Share projects with other groups](share_projects_with_other_groups.md)
|
||||
- [Web Editor](web_editor.md)
|
||||
- [Releases](releases.md)
|
||||
- [Issuable Templates](issuable_templates.md)
|
||||
- [Milestones](milestones.md)
|
||||
- [Merge Requests](merge_requests.md)
|
||||
- [Revert changes](revert_changes.md)
|
||||
|
|
12
doc/workflow/description_templates.md
Normal file
12
doc/workflow/description_templates.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Description templates
|
||||
|
||||
Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
|
||||
|
||||
Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
|
||||
|
||||
Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
|
||||
|
||||
![Description templates](img/description_templates.png)
|
||||
|
||||
_Example:_
|
||||
`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
|
BIN
doc/workflow/img/description_templates.png
Normal file
BIN
doc/workflow/img/description_templates.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
|
@ -9,7 +9,7 @@ Background:
|
|||
@javascript
|
||||
Scenario: I should see New Projects page
|
||||
Then I see "New Project" page
|
||||
Then I see all possible import optios
|
||||
Then I see all possible import options
|
||||
|
||||
@javascript
|
||||
Scenario: I should see instructions on how to import from Git URL
|
||||
|
|
|
@ -14,14 +14,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
|
|||
expect(page).to have_content('Project name')
|
||||
end
|
||||
|
||||
step 'I see all possible import optios' do
|
||||
step 'I see all possible import options' do
|
||||
expect(page).to have_link('GitHub')
|
||||
expect(page).to have_link('Bitbucket')
|
||||
expect(page).to have_link('GitLab.com')
|
||||
expect(page).to have_link('Gitorious.org')
|
||||
expect(page).to have_link('Google Code')
|
||||
expect(page).to have_link('Repo by URL')
|
||||
expect(page).to have_link('GitLab export')
|
||||
end
|
||||
|
||||
step 'I click on "Import project from GitHub"' do
|
||||
|
|
|
@ -61,22 +61,27 @@ module API
|
|||
name: @branch.name
|
||||
}
|
||||
|
||||
unless developers_can_merge.nil?
|
||||
protected_branch_params.merge!({
|
||||
merge_access_level_attributes: {
|
||||
access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}
|
||||
})
|
||||
# If `developers_can_merge` is switched off, _all_ `DEVELOPER`
|
||||
# merge_access_levels need to be deleted.
|
||||
if developers_can_merge == false
|
||||
protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
|
||||
end
|
||||
|
||||
unless developers_can_push.nil?
|
||||
protected_branch_params.merge!({
|
||||
push_access_level_attributes: {
|
||||
access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}
|
||||
})
|
||||
# If `developers_can_push` is switched off, _all_ `DEVELOPER`
|
||||
# push_access_levels need to be deleted.
|
||||
if developers_can_push == false
|
||||
protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
|
||||
end
|
||||
|
||||
protected_branch_params.merge!(
|
||||
merge_access_levels_attributes: [{
|
||||
access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}],
|
||||
push_access_levels_attributes: [{
|
||||
access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
|
||||
}]
|
||||
)
|
||||
|
||||
if protected_branch
|
||||
service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
|
||||
service.execute(protected_branch)
|
||||
|
|
|
@ -48,7 +48,8 @@ module API
|
|||
|
||||
class ProjectHook < Hook
|
||||
expose :project_id, :push_events
|
||||
expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
|
||||
expose :issues_events, :merge_requests_events, :tag_push_events
|
||||
expose :note_events, :build_events, :pipeline_events
|
||||
expose :enable_ssl_verification
|
||||
end
|
||||
|
||||
|
@ -129,12 +130,14 @@ module API
|
|||
|
||||
expose :developers_can_push do |repo_branch, options|
|
||||
project = options[:project]
|
||||
project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER }
|
||||
access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
|
||||
access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
|
||||
end
|
||||
|
||||
expose :developers_can_merge do |repo_branch, options|
|
||||
project = options[:project]
|
||||
project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER }
|
||||
access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
|
||||
access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -344,7 +347,8 @@ module API
|
|||
|
||||
class ProjectService < Grape::Entity
|
||||
expose :id, :title, :created_at, :updated_at, :active
|
||||
expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
|
||||
expose :push_events, :issues_events, :merge_requests_events
|
||||
expose :tag_push_events, :note_events, :build_events, :pipeline_events
|
||||
# Expose serialized properties
|
||||
expose :properties do |service, options|
|
||||
field_names = service.fields.
|
||||
|
|
|
@ -3,8 +3,6 @@ module API
|
|||
class Issues < Grape::API
|
||||
before { authenticate! }
|
||||
|
||||
helpers ::Gitlab::AkismetHelper
|
||||
|
||||
helpers do
|
||||
def filter_issues_state(issues, state)
|
||||
case state
|
||||
|
|
|
@ -45,6 +45,7 @@ module API
|
|||
:tag_push_events,
|
||||
:note_events,
|
||||
:build_events,
|
||||
:pipeline_events,
|
||||
:enable_ssl_verification
|
||||
]
|
||||
@hook = user_project.hooks.new(attrs)
|
||||
|
@ -78,6 +79,7 @@ module API
|
|||
:tag_push_events,
|
||||
:note_events,
|
||||
:build_events,
|
||||
:pipeline_events,
|
||||
:enable_ssl_verification
|
||||
]
|
||||
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
module API
|
||||
class Templates < Grape::API
|
||||
TEMPLATE_TYPES = {
|
||||
gitignores: Gitlab::Template::Gitignore,
|
||||
gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
|
||||
GLOBAL_TEMPLATE_TYPES = {
|
||||
gitignores: Gitlab::Template::GitignoreTemplate,
|
||||
gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
|
||||
}.freeze
|
||||
|
||||
TEMPLATE_TYPES.each do |template, klass|
|
||||
helpers do
|
||||
def render_response(template_type, template)
|
||||
not_found!(template_type.to_s.singularize) unless template
|
||||
present template, with: Entities::Template
|
||||
end
|
||||
end
|
||||
|
||||
GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
|
||||
# Get the list of the available template
|
||||
#
|
||||
# Example Request:
|
||||
# GET /gitignores
|
||||
# GET /gitlab_ci_ymls
|
||||
get template.to_s do
|
||||
get template_type.to_s do
|
||||
present klass.all, with: Entities::TemplatesList
|
||||
end
|
||||
|
||||
# Get the text for a specific template
|
||||
# Get the text for a specific template present in local filesystem
|
||||
#
|
||||
# Parameters:
|
||||
# name (required) - The name of a template
|
||||
|
@ -23,13 +30,10 @@ module API
|
|||
# Example Request:
|
||||
# GET /gitignores/Elixir
|
||||
# GET /gitlab_ci_ymls/Ruby
|
||||
get "#{template}/:name" do
|
||||
get "#{template_type}/:name" do
|
||||
required_attributes! [:name]
|
||||
|
||||
new_template = klass.find(params[:name])
|
||||
not_found!(template.to_s.singularize) unless new_template
|
||||
|
||||
present new_template, with: Entities::Template
|
||||
render_response(template_type, new_template)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
module Gitlab
|
||||
module AkismetHelper
|
||||
def akismet_enabled?
|
||||
current_application_settings.akismet_enabled
|
||||
end
|
||||
|
||||
def akismet_client
|
||||
@akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
|
||||
Gitlab.config.gitlab.url)
|
||||
end
|
||||
|
||||
def client_ip(env)
|
||||
env['action_dispatch.remote_ip'].to_s
|
||||
end
|
||||
|
||||
def user_agent(env)
|
||||
env['HTTP_USER_AGENT']
|
||||
end
|
||||
|
||||
def check_for_spam?(project)
|
||||
akismet_enabled? && project.public?
|
||||
end
|
||||
|
||||
def is_spam?(environment, user, text)
|
||||
client = akismet_client
|
||||
ip_address = client_ip(environment)
|
||||
user_agent = user_agent(environment)
|
||||
|
||||
params = {
|
||||
type: 'comment',
|
||||
text: text,
|
||||
created_at: DateTime.now,
|
||||
author: user.name,
|
||||
author_email: user.email,
|
||||
referrer: environment['HTTP_REFERER'],
|
||||
}
|
||||
|
||||
begin
|
||||
is_spam, is_blatant = client.check(ip_address, user_agent, params)
|
||||
is_spam || is_blatant
|
||||
rescue => e
|
||||
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
module Gitlab
|
||||
class BuildDataBuilder
|
||||
class << self
|
||||
module DataBuilder
|
||||
module Build
|
||||
extend self
|
||||
|
||||
def build(build)
|
||||
project = build.project
|
||||
commit = build.pipeline
|
|
@ -1,6 +1,8 @@
|
|||
module Gitlab
|
||||
class NoteDataBuilder
|
||||
class << self
|
||||
module DataBuilder
|
||||
module Note
|
||||
extend self
|
||||
|
||||
# Produce a hash of post-receive data
|
||||
#
|
||||
# For all notes:
|
62
lib/gitlab/data_builder/pipeline.rb
Normal file
62
lib/gitlab/data_builder/pipeline.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
module Gitlab
|
||||
module DataBuilder
|
||||
module Pipeline
|
||||
extend self
|
||||
|
||||
def build(pipeline)
|
||||
{
|
||||
object_kind: 'pipeline',
|
||||
object_attributes: hook_attrs(pipeline),
|
||||
user: pipeline.user.try(:hook_attrs),
|
||||
project: pipeline.project.hook_attrs(backward: false),
|
||||
commit: pipeline.commit.try(:hook_attrs),
|
||||
builds: pipeline.builds.map(&method(:build_hook_attrs))
|
||||
}
|
||||
end
|
||||
|
||||
def hook_attrs(pipeline)
|
||||
{
|
||||
id: pipeline.id,
|
||||
ref: pipeline.ref,
|
||||
tag: pipeline.tag,
|
||||
sha: pipeline.sha,
|
||||
before_sha: pipeline.before_sha,
|
||||
status: pipeline.status,
|
||||
stages: pipeline.stages,
|
||||
created_at: pipeline.created_at,
|
||||
finished_at: pipeline.finished_at,
|
||||
duration: pipeline.duration
|
||||
}
|
||||
end
|
||||
|
||||
def build_hook_attrs(build)
|
||||
{
|
||||
id: build.id,
|
||||
stage: build.stage,
|
||||
name: build.name,
|
||||
status: build.status,
|
||||
created_at: build.created_at,
|
||||
started_at: build.started_at,
|
||||
finished_at: build.finished_at,
|
||||
when: build.when,
|
||||
manual: build.manual?,
|
||||
user: build.user.try(:hook_attrs),
|
||||
runner: build.runner && runner_hook_attrs(build.runner),
|
||||
artifacts_file: {
|
||||
filename: build.artifacts_file.filename,
|
||||
size: build.artifacts_size
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def runner_hook_attrs(runner)
|
||||
{
|
||||
id: runner.id,
|
||||
description: runner.description,
|
||||
active: runner.active?,
|
||||
is_shared: runner.is_shared?
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue