Commit f673f1e3 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into artifacts-from-ref-and-build-name

* upstream/master: (109 commits)
  Update CHANGELOG for 8.10.6, 8.9.7, and 8.8.8
  Updated Akismet documentation
  Add hover state to todos (!5361)
  Load issues and merge requests templates from repository
  Backport EE assertions in protected branch related specs.
  Revert "Merge branch '19957-write-tests-for-adding-comments-for-different-line-types-in-diff' into 'master'"
  Fix a missed `before_action` for `AutocompleteController`.
  Backport `AutocompleteController#load_project` from EE!581.
  Fix API::BranchesSpec.
  Fix failing tests relating to backporting ee!581.
  Revert unrelevant changes
  Fix the protected branches factory.
  Improve EE compatibility with protected branch access levels.
  Move the "update" portion of the protected branch view into a partial.
  Don't select an access level if already selected.
  Backport changes from gitlab-org/gitlab-ee!581 to CE.
  Further refactor and syntax fixes.
  Upgrade httpclient gem from 2.7.0.1 to 2.8.2.
  Make rubocop happy
  Make rubocop happy
  ...
parents 1c88ed7a 1b338d59
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.11.0 (unreleased) v 8.11.0 (unreleased)
- Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar) - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko) - Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres) - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
...@@ -40,6 +41,8 @@ v 8.11.0 (unreleased) ...@@ -40,6 +41,8 @@ v 8.11.0 (unreleased)
- Various redundant database indexes have been removed - Various redundant database indexes have been removed
- Update `timeago` plugin to use multiple string/locale settings - Update `timeago` plugin to use multiple string/locale settings
- Remove unused images (ClemMakesApps) - 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 - Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs - Show deployment status on merge requests with external URLs
- Clean up unused routes (Josef Strzibny) - Clean up unused routes (Josef Strzibny)
...@@ -73,6 +76,7 @@ v 8.11.0 (unreleased) ...@@ -73,6 +76,7 @@ v 8.11.0 (unreleased)
- The overhead of instrumented method calls has been reduced - 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) - 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` - Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook
- Bump gitlab_git to speedup DiffCollection iterations - Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich) - Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell) - Make branches sortable without push permission !5462 (winniehell)
...@@ -82,6 +86,7 @@ v 8.11.0 (unreleased) ...@@ -82,6 +86,7 @@ v 8.11.0 (unreleased)
- Make "New issue" button in Issue page less obtrusive !5457 (winniehell) - Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
- Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
- Fix search for notes which belongs to deleted objects - 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) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- Allow branch names ending with .json for graph and network page !5579 (winniehell) - Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem - Add the `sprockets-es6` gem
...@@ -114,6 +119,13 @@ v 8.11.0 (unreleased) ...@@ -114,6 +119,13 @@ v 8.11.0 (unreleased)
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska) - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter - Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with - 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 v 8.10.5
- Add a data migration to fix some missing timestamps in the members table. !5670 - Add a data migration to fix some missing timestamps in the members table. !5670
...@@ -135,6 +147,9 @@ v 8.10.3 ...@@ -135,6 +147,9 @@ v 8.10.3
- Fix importer for GitHub Pull Requests when a branch was removed. !5573 - 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 - 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 - 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 v 8.10.2
- User can now search branches by name. !5144 - User can now search branches by name. !5144
...@@ -282,6 +297,7 @@ v 8.10.0 ...@@ -282,6 +297,7 @@ v 8.10.0
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab - Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info. - 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) - 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 - Set import_url validation to be more strict
- Memoize MR merged/closed events retrieval - Memoize MR merged/closed events retrieval
- Don't render discussion notes when requesting diff tab through AJAX - Don't render discussion notes when requesting diff tab through AJAX
...@@ -328,6 +344,10 @@ v 8.10.0 ...@@ -328,6 +344,10 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades - Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page - 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 v 8.9.6
- Fix importing of events under notes for GitLab projects. !5154 - Fix importing of events under notes for GitLab projects. !5154
- Fix log statements in import/export. !5129 - Fix log statements in import/export. !5129
...@@ -593,6 +613,9 @@ v 8.9.0 ...@@ -593,6 +613,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar - Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation - 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 v 8.8.7
- Fix privilege escalation issue with OAuth external users. - Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users. - Ensure references to private repos aren't shown to logged-out users.
......
...@@ -338,7 +338,7 @@ GEM ...@@ -338,7 +338,7 @@ GEM
httparty (0.13.7) httparty (0.13.7)
json (~> 1.8) json (~> 1.8)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.7.0.1) httpclient (2.8.2)
i18n (0.7.0) i18n (0.7.0)
ice_nine (0.11.1) ice_nine (0.11.1)
influxdb (0.2.3) influxdb (0.2.3)
......
...@@ -9,10 +9,11 @@ ...@@ -9,10 +9,11 @@
licensePath: "/api/:version/licenses/:key", licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key", gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) { group: function(group_id, callback) {
var url; var url = Api.buildUrl(Api.groupPath)
url = Api.buildUrl(Api.groupPath); .replace(':id', group_id);
url = url.replace(':id', group_id);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -24,8 +25,7 @@ ...@@ -24,8 +25,7 @@
}); });
}, },
groups: function(query, skip_ldap, callback) { groups: function(query, skip_ldap, callback) {
var url; var url = Api.buildUrl(Api.groupsPath);
url = Api.buildUrl(Api.groupsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -39,8 +39,7 @@ ...@@ -39,8 +39,7 @@
}); });
}, },
namespaces: function(query, callback) { namespaces: function(query, callback) {
var url; var url = Api.buildUrl(Api.namespacesPath);
url = Api.buildUrl(Api.namespacesPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -54,8 +53,7 @@ ...@@ -54,8 +53,7 @@
}); });
}, },
projects: function(query, order, callback) { projects: function(query, order, callback) {
var url; var url = Api.buildUrl(Api.projectsPath);
url = Api.buildUrl(Api.projectsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -70,9 +68,8 @@ ...@@ -70,9 +68,8 @@
}); });
}, },
newLabel: function(project_id, data, callback) { newLabel: function(project_id, data, callback) {
var url; var url = Api.buildUrl(Api.labelsPath)
url = Api.buildUrl(Api.labelsPath); .replace(':id', project_id);
url = url.replace(':id', project_id);
data.private_token = gon.api_token; data.private_token = gon.api_token;
return $.ajax({ return $.ajax({
url: url, url: url,
...@@ -86,9 +83,8 @@ ...@@ -86,9 +83,8 @@
}); });
}, },
groupProjects: function(group_id, query, callback) { groupProjects: function(group_id, query, callback) {
var url; var url = Api.buildUrl(Api.groupProjectsPath)
url = Api.buildUrl(Api.groupProjectsPath); .replace(':id', group_id);
url = url.replace(':id', group_id);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -102,8 +98,8 @@ ...@@ -102,8 +98,8 @@
}); });
}, },
licenseText: function(key, data, callback) { licenseText: function(key, data, callback) {
var url; var url = Api.buildUrl(Api.licensePath)
url = Api.buildUrl(Api.licensePath).replace(':key', key); .replace(':key', key);
return $.ajax({ return $.ajax({
url: url, url: url,
data: data data: data
...@@ -112,19 +108,32 @@ ...@@ -112,19 +108,32 @@
}); });
}, },
gitignoreText: function(key, callback) { gitignoreText: function(key, callback) {
var url; var url = Api.buildUrl(Api.gitignorePath)
url = Api.buildUrl(Api.gitignorePath).replace(':key', key); .replace(':key', key);
return $.get(url, function(gitignore) { return $.get(url, function(gitignore) {
return callback(gitignore); return callback(gitignore);
}); });
}, },
gitlabCiYml: function(key, callback) { gitlabCiYml: function(key, callback) {
var url; var url = Api.buildUrl(Api.gitlabCiYmlPath)
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); .replace(':key', key);
return $.get(url, function(file) { return $.get(url, function(file) {
return callback(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) { buildUrl: function(url) {
if (gon.relative_url_root != null) { if (gon.relative_url_root != null) {
url = gon.relative_url_root + url; url = gon.relative_url_root + url;
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
/*= require date.format */ /*= require date.format */
/*= require_directory ./behaviors */ /*= require_directory ./behaviors */
/*= require_directory ./blob */ /*= require_directory ./blob */
/*= require_directory ./templates */
/*= require_directory ./commit */ /*= require_directory ./commit */
/*= require_directory ./extensions */ /*= require_directory ./extensions */
/*= require_directory ./lib/utils */ /*= require_directory ./lib/utils */
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
} }
this.onClick = bind(this.onClick, this); 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.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.buildDropdown();
this.bindEvents(); this.bindEvents();
this.onFilenameUpdate(); this.onFilenameUpdate();
...@@ -60,11 +61,26 @@ ...@@ -60,11 +61,26 @@
return this.requestFile(item); 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); 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; return TemplateSelector;
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form')); new GLForm($('.issue-form'));
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
...@@ -62,6 +63,7 @@ ...@@ -62,6 +63,7 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form')); new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
this.render = bind(this.render, this); this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val(); this.VIEW_TYPE = $('input#view[type=hidden]').val();
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); 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) { FilesCommentButton.prototype.render = function(e) {
......
...@@ -70,13 +70,15 @@ ...@@ -70,13 +70,15 @@
name: newLabelField.val(), name: newLabelField.val(),
color: newColorField.val() color: newColorField.val()
}, function(label) { }, function(label) {
var errors;
$newLabelCreateButton.enable(); $newLabelCreateButton.enable();
if (label.message != null) { if (label.message != null) {
errors = _.map(label.message, function(value, key) { var errorText = label.message;
if (_.isObject(label.message)) {
errorText = _.map(label.message, function(value, key) {
return key + " " + value[0]; return key + " " + value[0];
}); }).join('<br/>');
return $newLabelError.html(errors.join("<br/>")).show(); }
return $newLabelError.html(errorText).show();
} else { } else {
return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
} }
......
...@@ -44,8 +44,8 @@ ...@@ -44,8 +44,8 @@
// Enable submit button // Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); 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 $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_level_attributes][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()){ if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
this.$form.find('input[type="submit"]').removeAttr('disabled'); this.$form.find('input[type="submit"]').removeAttr('disabled');
......
...@@ -39,12 +39,14 @@ ...@@ -39,12 +39,14 @@
_method: 'PATCH', _method: 'PATCH',
id: this.$wrap.data('banchId'), id: this.$wrap.data('banchId'),
protected_branch: { protected_branch: {
merge_access_level_attributes: { merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val() access_level: $allowedToMergeInput.val()
}, }],
push_access_level_attributes: { push_access_levels_attributes: [{
id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val() access_level: $allowedToPushInput.val()
} }]
} }
}, },
success: () => { success: () => {
......
/*= 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);
((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 @@ ...@@ -164,6 +164,10 @@
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); @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-danger,
&.btn-remove, &.btn-remove,
&.btn-red { &.btn-red {
......
...@@ -56,9 +56,13 @@ ...@@ -56,9 +56,13 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 6px; right: 6px;
margin-top: -4px; margin-top: -6px;
color: $dropdown-toggle-icon-color; color: $dropdown-toggle-icon-color;
font-size: 10px; font-size: 10px;
&.fa-spinner {
font-size: 16px;
margin-top: -8px;
}
} }
&:hover, { &:hover, {
...@@ -412,6 +416,7 @@ ...@@ -412,6 +416,7 @@
font-size: 14px; font-size: 14px;
a { a {
cursor: pointer;
padding-left: 10px; padding-left: 10px;
} }
} }
......
...@@ -6,11 +6,11 @@ ...@@ -6,11 +6,11 @@
table-layout: fixed; table-layout: fixed;
pre { pre {
padding: 10px; padding: 10px 0;
border: none; border: none;
border-radius: 0; border-radius: 0;
font-family: $monospace_font; font-family: $monospace_font;
font-size: $code_font_size !important; font-size: $code_font_size;
line-height: $code_line_height !important; line-height: $code_line_height !important;
margin: 0; margin: 0;
overflow: auto; overflow: auto;
...@@ -20,13 +20,20 @@ ...@@ -20,13 +20,20 @@
border-left: 1px solid; border-left: 1px solid;
code { code {
display: inline-block;
min-width: 100%;
font-family: $monospace_font; font-family: $monospace_font;
white-space: pre; white-space: normal;
word-wrap: normal; word-wrap: normal;
padding: 0; padding: 0;
.line { .line {
display: inline-block; display: block;
width: 100%;
min-height: 19px;
padding-left: 10px;
padding-right: 10px;
white-space: pre;
} }
} }
} }
......
...@@ -395,3 +395,12 @@ ...@@ -395,3 +395,12 @@
display: inline-block; display: inline-block;
line-height: 18px; 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 @@ ...@@ -99,7 +99,7 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 15px; margin-bottom: 15px;
max-width: 480px; max-width: 700px;
> p { > p {
margin-bottom: 0; margin-bottom: 0;
......
...@@ -20,10 +20,43 @@ ...@@ -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 { &:hover {
background-color: $row-hover;
border-color: $row-hover-border;
cursor: pointer; 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 { .todo-item {
...@@ -43,8 +76,6 @@ ...@@ -43,8 +76,6 @@
} }
.todo-body { .todo-body {
margin-right: 174px;
.todo-note { .todo-note {
word-wrap: break-word; word-wrap: break-word;
...@@ -90,6 +121,12 @@ ...@@ -90,6 +121,12 @@
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.todo {
.avatar {
display: none;
}
}
.todo-item { .todo-item {
.todo-title { .todo-title {
white-space: normal; white-space: normal;
...@@ -98,10 +135,6 @@ ...@@ -98,10 +135,6 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.avatar {
display: none;
}
.todo-body { .todo-body {
margin: 0; margin: 0;
border-left: 2px solid #ddd; border-left: 2px solid #ddd;
......
...@@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController ...@@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
head :ok head :ok
end end
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 end
class AutocompleteController < ApplicationController class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users] skip_before_action :authenticate_user!, only: [:users]
before_action :load_project, only: [:users]
before_action :find_users, only: [:users] before_action :find_users, only: [:users]
def users def users
...@@ -55,11 +56,8 @@ class AutocompleteController < ApplicationController ...@@ -55,11 +56,8 @@ class AutocompleteController < ApplicationController
def find_users def find_users
@users = @users =
if params[:project_id].present? if @project
project = Project.find(params[:project_id]) @project.team.users
return render_404 unless can?(current_user, :read_project, project)
project.team.users
elsif params[:group_id].present? elsif params[:group_id].present?
group = Group.find(params[:group_id]) group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group) return render_404 unless can?(current_user, :read_group, group)
...@@ -71,4 +69,14 @@ class AutocompleteController < ApplicationController ...@@ -71,4 +69,14 @@ class AutocompleteController < ApplicationController
User.none User.none
end end
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
end end
...@@ -7,11 +7,16 @@ module ServiceParams ...@@ -7,11 +7,16 @@ module ServiceParams
:build_key, :server, :teamcity_url, :drone_url, :build_type, :build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels, :colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events, # We're using `issues_events` and `merge_requests_events`
:note_events, :build_events, :wiki_page_events, # in the view so we still need to explicitly state them
:notify_only_broken_builds, :add_pusher, # here. `Service#event_names` would only give
:send_from_committer_email, :disable_diffs, :external_wiki_url, # `issue_events` and `merge_request_events` (singular!)
:notify, :color, # 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, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id] :jira_issue_transition_id]
...@@ -19,9 +24,7 @@ module ServiceParams ...@@ -19,9 +24,7 @@ module ServiceParams
FILTER_BLANK_PARAMS = [:password] FILTER_BLANK_PARAMS = [:password]
def service_params def service_params
dynamic_params = [] dynamic_params = @service.event_channel_names + @service.event_names
dynamic_params.concat(@service.event_channel_names)
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
if service_params[:service].is_a?(Hash) if service_params[:service].is_a?(Hash)
......
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
class Import::GitlabProjectsController < Import::BaseController class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled before_action :verify_gitlab_project_import_enabled
before_action :authenticate_admin!
def new def new
@namespace_id = project_params[:namespace_id] @namespace_id = project_params[:namespace_id]
...@@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController ...@@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file :path, :namespace_id, :file
) )
end end
def authenticate_admin!
render_404 unless current_user.is_admin?
end
end end
...@@ -4,11 +4,24 @@ class Projects::BadgesController < Projects::ApplicationController ...@@ -4,11 +4,24 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers, except: [:index] before_action :no_cache_headers, except: [:index]
def build def build
badge = Gitlab::Badge::Build.new(project, params[:ref]) build_status = Gitlab::Badge::Build::Status
.new(project, params[:ref])
render_badge build_status
end
def coverage
coverage_report = Gitlab::Badge::Coverage::Report
.new(project, params[:ref], params[:job])
render_badge coverage_report
end
private
def render_badge(badge)
respond_to do |format| respond_to do |format|
format.html { render_404 } format.html { render_404 }
format.svg do format.svg do
render 'badge', locals: { badge: badge.template } render 'badge', locals: { badge: badge.template }
end end
......
...@@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_branch_head, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff] before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff before_action :validate_diff_params, only: :diff
before_action :set_last_commit_sha, only: [:edit, :update]
def new def new
commit unless @repository.empty? commit unless @repository.empty?
...@@ -33,7 +34,6 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -33,7 +34,6 @@ class Projects::BlobController < Projects::ApplicationController
end end
def edit def edit
@last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
blob.load_all_data!(@repository) blob.load_all_data!(@repository)
end end
...@@ -55,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -55,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: after_edit_path, create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit, failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
rescue Files::UpdateService::FileChangedError
@conflict = true
render :edit
end end
def preview def preview
...@@ -152,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -152,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController
file_path: @file_path, file_path: @file_path,
commit_message: params[:commit_message], commit_message: params[:commit_message],
file_content: params[:content], file_content: params[:content],
file_content_encoding: params[:encoding] file_content_encoding: params[:encoding],
last_commit_sha: params[:last_commit_sha]
} }
end end
...@@ -161,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -161,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController
render nothing: true render nothing: true
end end
end end
def set_last_commit_sha
@last_commit_sha = Gitlab::Git::Commit.
last_for_path(@repository, @ref, @path).sha
end
end end
...@@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:build_events, :build_events,
:pipeline_events,
:enable_ssl_verification, :enable_ssl_verification,
:issues_events, :issues_events,
:merge_requests_events, :merge_requests_events,
......
...@@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions include IssuableActions
include ToggleAwardEmoji include ToggleAwardEmoji
include IssuableCollections include IssuableCollections
include SpammableActions
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
...@@ -185,6 +186,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -185,6 +186,7 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
alias_method :awardable, :issue alias_method :awardable, :issue
alias_method :spammable, :issue
def authorize_read_issue! def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
......
...@@ -3,7 +3,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -3,7 +3,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def show def show
@ref = params[:ref] || @project.default_branch || 'master' @ref = params[:ref] || @project.default_branch || 'master'
@build_badge = Gitlab::Badge::Build.new(@project, @ref).metadata
@badges = [Gitlab::Badge::Build::Status,
Gitlab::Badge::Coverage::Report]
@badges.map! do |badge|
badge.new(@project, @ref).metadata
end
end end
def update def update
......
...@@ -9,16 +9,16 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ...@@ -9,16 +9,16 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def index def index
@protected_branch = @project.protected_branches.new @protected_branch = @project.protected_branches.new
load_protected_branches_gon_variables load_gon_index
end end
def create 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? if @protected_branch.persisted?
redirect_to namespace_project_protected_branches_path(@project.namespace, @project) redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
else else
load_protected_branches load_protected_branches
load_protected_branches_gon_variables load_gon_index
render :index render :index
end end
end end
...@@ -28,7 +28,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ...@@ -28,7 +28,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end end
def update 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? if @protected_branch.valid?
respond_to do |format| respond_to do |format|
...@@ -58,17 +58,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ...@@ -58,17 +58,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def protected_branch_params def protected_branch_params
params.require(:protected_branch).permit(:name, params.require(:protected_branch).permit(:name,
merge_access_level_attributes: [:access_level], merge_access_levels_attributes: [:access_level, :id],
push_access_level_attributes: [:access_level]) push_access_levels_attributes: [:access_level, :id])
end end
def load_protected_branches def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_branches = @project.protected_branches.order(:name).page(params[:page])
end end
def load_protected_branches_gon_variables def access_levels_options
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 } }, 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 } } }) 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
end end
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
...@@ -182,17 +182,42 @@ module BlobHelper ...@@ -182,17 +182,42 @@ module BlobHelper
} }
end 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 def gitignore_names
@gitignore_names ||= @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
Gitlab::Template::Gitignore.categories.keys.map do |k|
[k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
end.to_h
end end
def gitlab_ci_ymls def gitlab_ci_ymls
@gitlab_ci_ymls ||= @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
[k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
end.to_h
end end
end end
...@@ -20,13 +20,19 @@ module SortingHelper ...@@ -20,13 +20,19 @@ module SortingHelper
end end
def projects_sort_options_hash def projects_sort_options_hash
{ options = {
sort_value_name => sort_title_name, sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated, sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated, sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created, sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created, sort_value_oldest_created => sort_title_oldest_created,
} }
if current_controller?('admin/projects')
options.merge!(sort_value_largest_repo => sort_title_largest_repo)
end
options
end end
def sort_title_priority def sort_title_priority
......
...@@ -344,7 +344,7 @@ module Ci ...@@ -344,7 +344,7 @@ module Ci
def execute_hooks def execute_hooks
return unless project 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_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks)
project.running_or_pending_build_count(force: true) project.running_or_pending_build_count(force: true)
......
...@@ -19,6 +19,8 @@ module Ci ...@@ -19,6 +19,8 @@ module Ci
after_save :keep_around_commits after_save :keep_around_commits
delegate :stages, to: :statuses
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
...@@ -56,6 +58,10 @@ module Ci ...@@ -56,6 +58,10 @@ module Ci
before_transition do |pipeline| before_transition do |pipeline|
pipeline.update_duration pipeline.update_duration
end end
after_transition do |pipeline, transition|
pipeline.execute_hooks unless transition.loopback?
end
end end
# ref can't be HEAD or SHA, can only be branch/tag name # ref can't be HEAD or SHA, can only be branch/tag name
...@@ -243,8 +249,18 @@ module Ci ...@@ -243,8 +249,18 @@ module Ci
self.duration = statuses.latest.duration self.duration = statuses.latest.duration
end end
def execute_hooks
data = pipeline_data
project.execute_hooks(data, :pipeline_hooks)
project.execute_services(data, :pipeline_hooks)
end
private private
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
def latest_builds_status def latest_builds_status
return 'failed' unless yaml_errors.blank? return 'failed' unless yaml_errors.blank?
......
module ProtectedBranchAccess
extend ActiveSupport::Concern
def humanize
self.class.human_access_levels[self.access_level]
end
end
module Spammable module Spammable
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods
def attr_spammable(attr, options = {})
spammable_attrs << [attr.to_s, options]
end
end
included do included do
has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam attr_accessor :spam
after_validation :check_for_spam, on: :create 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 end
def spam? def spam?
...@@ -13,4 +36,33 @@ module Spammable ...@@ -13,4 +36,33 @@ module Spammable
def check_for_spam def check_for_spam
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
end 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 end
...@@ -5,5 +5,6 @@ class ProjectHook < WebHook ...@@ -5,5 +5,6 @@ class ProjectHook < WebHook
scope :note_hooks, -> { where(note_events: true) } scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_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) } scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end end
...@@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base ...@@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base
default_value_for :merge_requests_events, false default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false default_value_for :tag_push_events, false
default_value_for :build_events, false default_value_for :build_events, false
default_value_for :pipeline_events, false
default_value_for :enable_ssl_verification, true default_value_for :enable_ssl_verification, true
scope :push_hooks, -> { where(push_events: true) } scope :push_hooks, -> { where(push_events: true) }
......
...@@ -36,6 +36,9 @@ class Issue < ActiveRecord::Base ...@@ -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_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') } 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 state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:reopened, :opened] => :closed
...@@ -262,4 +265,9 @@ class Issue < ActiveRecord::Base ...@@ -262,4 +265,9 @@ class Issue < ActiveRecord::Base
def overdue? def overdue?
due_date.try(:past?) || false due_date.try(:past?) || false
end end
# Only issues on public projects should be checked for spam
def check_for_spam?
project.public?
end
end end
...@@ -51,8 +51,7 @@ class BuildsEmailService < Service ...@@ -51,8 +51,7 @@ class BuildsEmailService < Service
end end
def test_data(project = nil, user = nil) def test_data(project = nil, user = nil)
build = project.builds.last Gitlab::DataBuilder::Build.build(project.builds.last)
Gitlab::BuildDataBuilder.build(build)
end end
def fields def fields
......
...@@ -56,6 +56,10 @@ class ProjectWiki ...@@ -56,6 +56,10 @@ class ProjectWiki
end end
end end
def repository_exists?
!!repository.exists?
end
def empty? def empty?
pages.empty? pages.empty?
end end
......
...@@ -5,11 +5,14 @@ class ProtectedBranch < ActiveRecord::Base ...@@ -5,11 +5,14 @@ class ProtectedBranch < ActiveRecord::Base
validates :name, presence: true validates :name, presence: true
validates :project, presence: true validates :project, presence: true
has_one :merge_access_level, dependent: :destroy has_many :merge_access_levels, dependent: :destroy
has_one :push_access_level, dependent: :destroy has_many :push_access_levels, dependent: :destroy
accepts_nested_attributes_for :push_access_level validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
accepts_nested_attributes_for :merge_access_level 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 def commit
project.commit(self.name) project.commit(self.name)
......
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
belongs_to :protected_branch belongs_to :protected_branch
delegate :project, to: :protected_branch delegate :project, to: :protected_branch
...@@ -17,8 +19,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base ...@@ -17,8 +19,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
project.team.max_member_access(user.id) >= access_level project.team.max_member_access(user.id) >= access_level
end end
def humanize
self.class.human_access_levels[self.access_level]
end
end end
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
include ProtectedBranchAccess
belongs_to :protected_branch belongs_to :protected_branch
delegate :project, to: :protected_branch delegate :project, to: :protected_branch
...@@ -20,8 +22,4 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base ...@@ -20,8 +22,4 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
project.team.max_member_access(user.id) >= access_level project.team.max_member_access(user.id) >= access_level
end end
def humanize
self.class.human_access_levels[self.access_level]
end
end end
...@@ -36,6 +36,7 @@ class Service < ActiveRecord::Base ...@@ -36,6 +36,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_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 :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
...@@ -79,13 +80,17 @@ class Service < ActiveRecord::Base ...@@ -79,13 +80,17 @@ class Service < ActiveRecord::Base
end end
def test_data(project, user) def test_data(project, user)
Gitlab::PushDataBuilder.build_sample(project, user) Gitlab::DataBuilder::Push.build_sample(project, user)
end end
def event_channel_names def event_channel_names
[] []
end end
def event_names
supported_events.map { |event| "#{event}_events" }
end
def event_field(event) def event_field(event)
nil nil
end end
......
...@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base ...@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
user.block user.block
user.destroy user.destroy
end end
def text
[title, description].join("\n")
end
end end
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
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
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 ...@@ -39,7 +39,12 @@ class DeleteBranchService < BaseService
end end
def build_push_data(branch) def build_push_data(branch)
Gitlab::PushDataBuilder Gitlab::DataBuilder::Push.build(
.build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) project,
current_user,
branch.target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
[])
end end
end end
...@@ -33,7 +33,12 @@ class DeleteTagService < BaseService ...@@ -33,7 +33,12 @@ class DeleteTagService < BaseService
end end
def build_push_data(tag) def build_push_data(tag)
Gitlab::PushDataBuilder Gitlab::DataBuilder::Push.build(
.build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) project,
current_user,
tag.target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
[])
end end
end end
...@@ -15,6 +15,7 @@ module Files ...@@ -15,6 +15,7 @@ module Files
else else
params[:file_content] params[:file_content]
end end
@last_commit_sha = params[:last_commit_sha]
# Validate parameters # Validate parameters
validate validate
......
...@@ -2,11 +2,34 @@ require_relative "base_service" ...@@ -2,11 +2,34 @@ require_relative "base_service"
module Files module Files
class UpdateService < Files::BaseService class UpdateService < Files::BaseService
class FileChangedError < StandardError; end
def commit def commit
repository.update_file(current_user, @file_path, @file_content, repository.update_file(current_user, @file_path, @file_content,
branch: @target_branch, branch: @target_branch,
previous_path: @previous_path, previous_path: @previous_path,
message: @commit_message) message: @commit_message)
end end
private
def validate
super
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
end
end
def file_has_changed?
return false unless @last_commit_sha && last_commit
@last_commit_sha != last_commit.sha
end
def last_commit
@last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path)
end
end end
end end
...@@ -91,12 +91,12 @@ class GitPushService < BaseService ...@@ -91,12 +91,12 @@ class GitPushService < BaseService
params = { params = {
name: @project.default_branch, 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 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 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 ProtectedBranches::CreateService.new(@project, current_user, params).execute
...@@ -138,13 +138,23 @@ class GitPushService < BaseService ...@@ -138,13 +138,23 @@ class GitPushService < BaseService
end end
def build_push_data def build_push_data
@push_data ||= Gitlab::PushDataBuilder. @push_data ||= Gitlab::DataBuilder::Push.build(
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) @project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
push_commits)
end end
def build_push_data_system_hook def build_push_data_system_hook
@push_data_system ||= Gitlab::PushDataBuilder. @push_data_system ||= Gitlab::DataBuilder::Push.build(
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], []) @project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
[])
end end
def push_to_existing_branch? def push_to_existing_branch?
......
...@@ -34,12 +34,24 @@ class GitTagPushService < BaseService ...@@ -34,12 +34,24 @@ class GitTagPushService < BaseService
end end
end end
Gitlab::PushDataBuilder. Gitlab::DataBuilder::Push.build(
build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message) project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
commits,
message)
end end
def build_system_push_data def build_system_push_data
Gitlab::PushDataBuilder. Gitlab::DataBuilder::Push.build(
build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '') project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
[],
'')
end end
end end
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 ...@@ -3,29 +3,34 @@ module Issues
def execute def execute
filter_params filter_params
label_params = params.delete(:label_ids) label_params = params.delete(:label_ids)
request = params.delete(:request) @request = params.delete(:request)
api = params.delete(:api) @api = params.delete(:api)
issue = project.issues.new(params) @issue = project.issues.new(params)
issue.author = params[:author] || current_user @issue.author = params[:author] || current_user
issue.spam = spam_check_service.execute(request, api) @issue.spam = spam_service.check(@api)
if issue.save if @issue.save
issue.update_attributes(label_ids: label_params) @issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user) notification_service.new_issue(@issue, current_user)
todo_service.new_issue(issue, current_user) todo_service.new_issue(@issue, current_user)
event_service.open_issue(issue, current_user) event_service.open_issue(@issue, current_user)
issue.create_cross_references!(current_user) user_agent_detail_service.create
execute_hooks(issue, 'open') @issue.create_cross_references!(current_user)
execute_hooks(@issue, 'open')
end end
issue @issue
end end
private private
def spam_check_service def spam_service
SpamCheckService.new(project, current_user, params) SpamService.new(@issue, @request)
end
def user_agent_detail_service
UserAgentDetailService.new(@issue, @request)
end end
end end
end end
...@@ -16,7 +16,7 @@ module Notes ...@@ -16,7 +16,7 @@ module Notes
end end
def hook_data def hook_data
Gitlab::NoteDataBuilder.build(@note, @note.author) Gitlab::DataBuilder::Note.build(@note, @note.author)
end end
def execute_note_hooks def execute_note_hooks
......
...@@ -5,23 +5,7 @@ module ProtectedBranches ...@@ -5,23 +5,7 @@ module ProtectedBranches
def execute def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
protected_branch = project.protected_branches.new(params) project.protected_branches.create(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
end end
end end
end end
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
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
class TestHookService class TestHookService
def execute(hook, current_user) 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') hook.execute(data, 'push_hooks')
end end
end end
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 @@ ...@@ -24,6 +24,11 @@
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true), = 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" data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
%td %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? - 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" = 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 - else
......
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } %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 .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title.title .todo-title.title
- unless todo.build_failed? - unless todo.build_failed?
= todo_target_state_pill(todo) = todo_target_state_pill(todo)
...@@ -19,13 +20,13 @@ ...@@ -19,13 +20,13 @@
&middot; #{time_ago_with_tooltip(todo.created_at)} &middot; #{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-body
.todo-note .todo-note
.md .md
= event_note(todo.body, project: todo.project) = 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 @@ ...@@ -24,7 +24,7 @@
.encoding-selector .encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' = 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]} %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
- if local_assigns[:path] - if local_assigns[:path]
.js-edit-mode-pane#preview.hide .js-edit-mode-pane#preview.hide
......
- page_title "Edit", @blob.path, @ref - page_title "Edit", @blob.path, @ref
- if @conflict
.alert.alert-danger
Someone edited the file the same time you did. Please check out
= link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
and make sure your changes will not unintentionally remove theirs.
.file-editor .file-editor
%ul.nav-links.no-bottom.js-edit-mode %ul.nav-links.no-bottom.js-edit-mode
%li.active %li.active
...@@ -13,8 +19,7 @@ ...@@ -13,8 +19,7 @@
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.col-md-8.col-lg-7 .col-md-8.col-lg-7
%strong.light-header= hook.url %strong.light-header= hook.url
%div %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) - if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize %span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5 .col-md-4.col-lg-5.text-right-lg.prepend-top-5
......
...@@ -37,14 +37,19 @@ ...@@ -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' = 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 %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = 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) - 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 = 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 New issue
- if can?(current_user, :update_issue, @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 '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 '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 - if @issue.submittable_as_spam? && current_user.admin?
Edit = 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 .issue-details.issuable-details
......
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
= link_to "#", class: 'btn js-toggle-button import_git' do = link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
%div{ class: 'import_gitlab_project' } %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 = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export') = icon('gitlab', text: 'GitLab export')
......
.row{ class: badge.title.gsub(' ', '-') }
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= badge.title.capitalize
.col-lg-9
.prepend-top-10
.panel.panel-default
.panel-heading
%b
= badge.title.capitalize
&middot;
= badge.to_html
.pull-right
= render 'shared/ref_switcher', destination: 'badges', align_right: true
.panel-body
.row
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight('.md', badge.to_markdown)
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', badge.to_html)
...@@ -77,27 +77,4 @@ ...@@ -77,27 +77,4 @@
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar = render partial: 'badge', collection: @badges
%h4.prepend-top-0
Builds Badge
.col-lg-9
.prepend-top-10
.panel.panel-default
.panel-heading
%b Builds badge &middot;
= @build_badge.to_html
.pull-right
= render 'shared/ref_switcher', destination: 'badges', align_right: true
.panel-body
.row
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight('.md', @build_badge.to_markdown)
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', @build_badge.to_html)
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
Protect a branch Protect a branch
.panel-body .panel-body
.form-horizontal .form-horizontal
= form_errors(@protected_branch)
.form-group .form-group
= f.label :name, class: 'col-md-2 text-right' do = f.label :name, class: 'col-md-2 text-right' do
Branch: Branch:
...@@ -18,19 +19,19 @@ ...@@ -18,19 +19,19 @@
%code production/* %code production/*
are supported are supported
.form-group .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: Allowed to merge:
.col-md-10 .col-md-10
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge wide', 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 .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: Allowed to push:
.col-md-10 .col-md-10
= dropdown_tag('Select', = dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push wide', 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 .panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true = f.submit 'Protect', class: 'btn-create btn', disabled: true
...@@ -13,16 +13,9 @@ ...@@ -13,16 +13,9 @@
= time_ago_with_tooltip(commit.committed_date) = time_ago_with_tooltip(commit.committed_date)
- else - else
(branch was removed from repository) (branch was removed from repository)
%td
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
= 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}" }})
- if can_admin_project - if can_admin_project
%td %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' = 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'
%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 @@ ...@@ -45,7 +45,7 @@
.filter-item.inline .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 } }) = 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 .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 .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 = 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 %ul
......
...@@ -2,7 +2,22 @@ ...@@ -2,7 +2,22 @@
.form-group .form-group
= f.label :title, class: 'control-label' = 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', = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad', required: true class: 'form-control pad', required: true
...@@ -23,6 +38,13 @@ ...@@ -23,6 +38,13 @@
to prevent a to prevent a
%strong Work In Progress %strong Work In Progress
merge request from being merged before it's ready. 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 .form-group.detail-page-description
= f.label :description, 'Description', class: 'control-label' = f.label :description, 'Description', class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -29,49 +29,56 @@ ...@@ -29,49 +29,56 @@
= f.label :push_events, class: 'list-label' do = f.label :push_events, class: 'list-label' do
%strong Push events %strong Push events
%p.light %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 %li
= f.check_box :tag_push_events, class: 'pull-left' = f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :tag_push_events, class: 'list-label' do = f.label :tag_push_events, class: 'list-label' do
%strong Tag push events %strong Tag push events
%p.light %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 %li
= f.check_box :note_events, class: 'pull-left' = f.check_box :note_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :note_events, class: 'list-label' do = f.label :note_events, class: 'list-label' do
%strong Comments %strong Comments
%p.light %p.light
This url will be triggered when someone adds a comment This URL will be triggered when someone adds a comment
%li %li
= f.check_box :issues_events, class: 'pull-left' = f.check_box :issues_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :issues_events, class: 'list-label' do = f.label :issues_events, class: 'list-label' do
%strong Issues events %strong Issues events
%p.light %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 %li
= f.check_box :merge_requests_events, class: 'pull-left' = f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do = f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events %strong Merge Request events
%p.light %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 %li
= f.check_box :build_events, class: 'pull-left' = f.check_box :build_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :build_events, class: 'list-label' do = f.label :build_events, class: 'list-label' do
%strong Build events %strong Build events
%p.light %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 %li
= f.check_box :wiki_page_events, class: 'pull-left' = f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do = f.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events %strong Wiki Page events
%p.light %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 .form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
.checkbox .checkbox
......
...@@ -252,7 +252,11 @@ Rails.application.routes.draw do ...@@ -252,7 +252,11 @@ Rails.application.routes.draw do
resource :impersonation, only: :destroy resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :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 resources :applications
...@@ -524,6 +528,11 @@ Rails.application.routes.draw do ...@@ -524,6 +528,11 @@ Rails.application.routes.draw do
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
#
# Templates
#
get '/templates/:template_type/:key' => 'templates#show', as: :template
scope do scope do
get( get(
'/blob/*id/diff', '/blob/*id/diff',
...@@ -822,6 +831,7 @@ Rails.application.routes.draw do ...@@ -822,6 +831,7 @@ Rails.application.routes.draw do
member do member do
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji post :toggle_award_emoji
post :mark_as_spam
get :referenced_merge_requests get :referenced_merge_requests
get :related_branches get :related_branches
get :can_create_branch get :can_create_branch
...@@ -878,7 +888,10 @@ Rails.application.routes.draw do ...@@ -878,7 +888,10 @@ Rails.application.routes.draw do
resources :badges, only: [:index] do resources :badges, only: [:index] do
collection do collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
get :build, constraints: { format: /svg/ } constraints format: /svg/ do
get :build
get :coverage
end
end end
end end
end end
......
class Gitlab::Seeder::Builds class Gitlab::Seeder::Builds
STAGES = %w[build notify_build test notify_test deploy notify_deploy] STAGES = %w[build notify_build test notify_test deploy notify_deploy]
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success },
{ name: 'slack post build', stage: 'notify_build', status: :success },
{ name: 'rspec:linux', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
{ name: 'spinach:linux', stage: 'test', status: :pending },
{ name: 'spinach:osx', stage: 'test', status: :canceled },
{ name: 'cucumber:linux', stage: 'test', status: :running },
{ name: 'cucumber:osx', stage: 'test', status: :failed },
{ name: 'slack post test', stage: 'notify_test', status: :success },
{ name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success },
]
def initialize(project) def initialize(project)
@project = project @project = project
...@@ -8,25 +24,7 @@ class Gitlab::Seeder::Builds ...@@ -8,25 +24,7 @@ class Gitlab::Seeder::Builds
def seed! def seed!
pipelines.each do |pipeline| pipelines.each do |pipeline|
begin begin
build_create!(pipeline, name: 'build:linux', stage: 'build', status_event: :success) BUILDS.each { |opts| build_create!(pipeline, opts) }
build_create!(pipeline, name: 'build:osx', stage: 'build', status_event: :success)
build_create!(pipeline, name: 'slack post build', stage: 'notify_build', status_event: :success)
build_create!(pipeline, name: 'rspec:linux', stage: 'test', status_event: :success)
build_create!(pipeline, name: 'rspec:windows', stage: 'test', status_event: :success)
build_create!(pipeline, name: 'rspec:windows', stage: 'test', status_event: :success)
build_create!(pipeline, name: 'rspec:osx', stage: 'test', status_event: :success)
build_create!(pipeline, name: 'spinach:linux', stage: 'test', status: :pending)
build_create!(pipeline, name: 'spinach:osx', stage: 'test', status_event: :cancel)
build_create!(pipeline, name: 'cucumber:linux', stage: 'test', status_event: :run)
build_create!(pipeline, name: 'cucumber:osx', stage: 'test', status_event: :drop)
build_create!(pipeline, name: 'slack post test', stage: 'notify_test', status_event: :success)
build_create!(pipeline, name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success)
build_create!(pipeline, name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success)
commit_status_create!(pipeline, name: 'jenkins', status: :success) commit_status_create!(pipeline, name: 'jenkins', status: :success)
print '.' print '.'
...@@ -48,8 +46,8 @@ class Gitlab::Seeder::Builds ...@@ -48,8 +46,8 @@ class Gitlab::Seeder::Builds
def build_create!(pipeline, opts = {}) def build_create!(pipeline, opts = {})
attributes = build_attributes_for(pipeline, opts) attributes = build_attributes_for(pipeline, opts)
build = Ci::Build.create!(attributes)
Ci::Build.create!(attributes) do |build|
if opts[:name].start_with?('build') if opts[:name].start_with?('build')
artifacts_cache_file(artifacts_archive_path) do |file| artifacts_cache_file(artifacts_archive_path) do |file|
build.artifacts_file = file build.artifacts_file = file
...@@ -65,6 +63,7 @@ class Gitlab::Seeder::Builds ...@@ -65,6 +63,7 @@ class Gitlab::Seeder::Builds
build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
end end
end end
end
def commit_status_create!(pipeline, opts = {}) def commit_status_create!(pipeline, opts = {})
attributes = commit_status_attributes_for(pipeline, opts) attributes = commit_status_attributes_for(pipeline, opts)
......
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
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
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
# 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
# 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
...@@ -897,6 +897,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -897,6 +897,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.string "category", default: "common", null: false t.string "category", default: "common", null: false
t.boolean "default", default: false t.boolean "default", default: false
t.boolean "wiki_page_events", default: true t.boolean "wiki_page_events", default: true
t.boolean "pipeline_events", default: false, null: false
end end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
...@@ -926,12 +927,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -926,12 +927,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.string "source_ip" t.string "source_ip"
t.string "user_agent" t.string "user_agent"
t.boolean "via_api" t.boolean "via_api"
t.integer "project_id"
t.string "noteable_type" t.string "noteable_type"
t.string "title" t.string "title"
t.text "description" t.text "description"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
end end
create_table "subscriptions", force: :cascade do |t| create_table "subscriptions", force: :cascade do |t|
...@@ -999,6 +1000,16 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -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", ["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 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| create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false t.string "encrypted_password", default: "", null: false
...@@ -1100,6 +1111,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -1100,6 +1111,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.boolean "build_events", default: false, null: false t.boolean "build_events", default: false, null: false
t.boolean "wiki_page_events", default: false, null: false t.boolean "wiki_page_events", default: false, null: false
t.string "token" t.string "token"
t.boolean "pipeline_events", default: false, null: false
end end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
......
...@@ -32,6 +32,41 @@ project. ...@@ -32,6 +32,41 @@ project.
Clicking on a pipeline will show the builds that were run for that pipeline. Clicking on a pipeline will show the builds that were run for that pipeline.
## Badges
There are build status and test coverage report badges available.
Go to pipeline settings to see available badges and code you can use to embed
badges in the `README.md` or your website.
### Build status badge
You can access a build status badge image using following link:
```
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
### Test coverage report badge
GitLab makes it possible to define the regular expression for coverage report,
that each build log will be matched against. This means that each build in the
pipeline can have the test coverage percentage value defined.
You can access test coverage badge using following link:
```
http://example.gitlab.com/namespace/project/badges/branch/coverage.svg
```
If you would like to get the coverage report from the specific job, you can add
a `job=coverage_job_name` parameter to the URL. For example, it is possible to
use following Markdown code to embed the est coverage report into `README.md`:
```markdown
![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
```
[builds]: #builds [builds]: #builds
[jobs]: yaml/README.md#jobs [jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages [stages]: yaml/README.md#stages
......
...@@ -218,21 +218,13 @@ project's settings. ...@@ -218,21 +218,13 @@ project's settings.
For more information read the For more information read the
[Builds emails service documentation](../../project_services/builds_emails.md). [Builds emails service documentation](../../project_services/builds_emails.md).
## Builds badge
You can access a builds badge image using following link:
```
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
Awesome! You started using CI in GitLab!
## Examples ## Examples
Visit the [examples README][examples] to see a list of examples using GitLab Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages. CI with various languages.
Awesome! You started using CI in GitLab!
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner [runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md [examples]: ../examples/README.md
......
...@@ -30,7 +30,11 @@ ...@@ -30,7 +30,11 @@
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md)
## Databases
- [What requires downtime?](what_requires_downtime.md) - [What requires downtime?](what_requires_downtime.md)
- [Adding database indexes](adding_database_indexes.md)
## Compliance ## Compliance
......
# 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. ...@@ -15,11 +15,14 @@ repository and maintained by GitLab UX designers.
## Navigation ## Navigation
GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu. 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 This menu will be visible regardless of what page you visit.
and the current user's profile picture. The content section contains a header and the content itself. The content section contains a header and the content itself. The header describes the current GitLab page and what navigation is
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
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 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.
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.
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 ### Adding new tab to header navigation
...@@ -99,3 +102,6 @@ Do not use both green and blue button in one form. ...@@ -99,3 +102,6 @@ Do not use both green and blue button in one form.
display counts in the UI. display counts in the UI.
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter [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
\ No newline at end of file
...@@ -22,14 +22,37 @@ To use Akismet: ...@@ -22,14 +22,37 @@ To use Akismet:
2. Sign-in or create a new account. 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`) 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. 6. Fill in the API key from step 3.
7. Save the configuration. 7. Save the configuration.
![Screenshot of Akismet settings](img/akismet_settings.png) ![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.
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
> than that of the exporter. > than that of the exporter.
> - For existing installations, the project import option has to be enabled in > - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'. > application settings (`/admin/application_settings`) under 'Import sources'.
> Ask your administrator if you don't see the **GitLab export** button when > You will have to be an administrator to enable and use the import functionality.
> creating a new project.
> - You can find some useful raketasks if you are an administrator in the > - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md) > [import_export](../../../administration/raketasks/project_import_export.md)
> raketask. > raketask.
......
...@@ -754,6 +754,174 @@ X-Gitlab-Event: Wiki Page Hook ...@@ -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 #### Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use If you want to see GitLab's webhooks in action for testing purposes you can use
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
- [Share projects with other groups](share_projects_with_other_groups.md) - [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md) - [Web Editor](web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
- [Issuable Templates](issuable_templates.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Merge Requests](merge_requests.md) - [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md) - [Revert changes](revert_changes.md)
......
# 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.
...@@ -9,7 +9,7 @@ Background: ...@@ -9,7 +9,7 @@ Background:
@javascript @javascript
Scenario: I should see New Projects page Scenario: I should see New Projects page
Then I see "New Project" page Then I see "New Project" page
Then I see all possible import optios Then I see all possible import options
@javascript @javascript
Scenario: I should see instructions on how to import from Git URL Scenario: I should see instructions on how to import from Git URL
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment