Commit 1d3aa59f authored by Connor Shea's avatar Connor Shea

Merge branch 'master' into diff-line-comment-vuejs

parents 1e7cbe0b 76aa85cc
...@@ -15,6 +15,7 @@ variables: ...@@ -15,6 +15,7 @@ variables:
USE_DB: "true" USE_DB: "true"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
......
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)
- 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)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres) - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
- Fix the title of the toggle dropdown button. !5515 (herminiotorres) - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
...@@ -32,12 +34,16 @@ v 8.11.0 (unreleased) ...@@ -32,12 +34,16 @@ v 8.11.0 (unreleased)
- Add "No one can push" as an option for protected branches. !5081 - Add "No one can push" as an option for protected branches. !5081
- Improve performance of AutolinkFilter#text_parse by using XPath - Improve performance of AutolinkFilter#text_parse by using XPath
- Add experimental Redis Sentinel support !1877 - Add experimental Redis Sentinel support !1877
- Rendering of SVGs as blobs is now limited to SVGs with a size smaller or equal to 2MB
- Fix branches page dropdown sort initial state (ClemMakesApps) - Fix branches page dropdown sort initial state (ClemMakesApps)
- Environments have an url to link to - Environments have an url to link to
- 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
- Clean up unused routes (Josef Strzibny) - Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission - Fix issue on empty project to allow developers to only push to protected branches if given permission
- Add green outline to New Branch button. !5447 (winniehell) - Add green outline to New Branch button. !5447 (winniehell)
...@@ -54,6 +60,7 @@ v 8.11.0 (unreleased) ...@@ -54,6 +60,7 @@ v 8.11.0 (unreleased)
- Optimize checking if a user has read access to a list of issues !5370 - Optimize checking if a user has read access to a list of issues !5370
- Store all DB secrets in secrets.yml, under descriptive names !5274 - Store all DB secrets in secrets.yml, under descriptive names !5274
- Nokogiri's various parsing methods are now instrumented - Nokogiri's various parsing methods are now instrumented
- Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor) - Add simple identifier to public SSH keys (muteor)
- Admin page now references docs instead of a specific file !5600 (AnAverageHuman) - Admin page now references docs instead of a specific file !5600 (AnAverageHuman)
- Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363 - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
...@@ -68,6 +75,7 @@ v 8.11.0 (unreleased) ...@@ -68,6 +75,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)
...@@ -77,6 +85,7 @@ v 8.11.0 (unreleased) ...@@ -77,6 +85,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
...@@ -107,6 +116,15 @@ v 8.11.0 (unreleased) ...@@ -107,6 +116,15 @@ v 8.11.0 (unreleased)
- Sort folders with submodules in Files view !5521 - Sort folders with submodules in Files view !5521
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0 - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
- 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
- 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
...@@ -271,9 +289,11 @@ v 8.10.0 ...@@ -271,9 +289,11 @@ v 8.10.0
- Fix new snippet style bug (elliotec) - Fix new snippet style bug (elliotec)
- Instrument Rinku usage - Instrument Rinku usage
- Be explicit to define merge request discussion variables - Be explicit to define merge request discussion variables
- Use cache for todos counter calling TodoService
- 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
...@@ -320,6 +340,10 @@ v 8.10.0 ...@@ -320,6 +340,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
...@@ -585,6 +609,9 @@ v 8.9.0 ...@@ -585,6 +609,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();
......
...@@ -5,13 +5,10 @@ ...@@ -5,13 +5,10 @@
this.Issuable = { this.Issuable = {
init: function() { init: function() {
if (!issuable_created) {
issuable_created = true;
Issuable.initTemplates(); Issuable.initTemplates();
Issuable.initSearch(); Issuable.initSearch();
Issuable.initChecks(); Issuable.initChecks();
return Issuable.initLabelFilterRemove(); return Issuable.initLabelFilterRemove();
}
}, },
initTemplates: function() { initTemplates: function() {
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
......
...@@ -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, {
...@@ -406,6 +410,7 @@ ...@@ -406,6 +410,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;
}
}
...@@ -69,6 +69,10 @@ ...@@ -69,6 +69,10 @@
&.ci-success { &.ci-success {
color: $gl-success; color: $gl-success;
a.environment {
color: inherit;
}
} }
&.ci-success_with_warnings { &.ci-success_with_warnings {
...@@ -126,7 +130,6 @@ ...@@ -126,7 +130,6 @@
&.has-conflicts .fa-exclamation-triangle { &.has-conflicts .fa-exclamation-triangle {
color: $gl-warning; color: $gl-warning;
} }
} }
p:last-child { p:last-child {
......
...@@ -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
...@@ -37,8 +37,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -37,8 +37,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def todos_counts def todos_counts
{ {
count: TodosFinder.new(current_user, state: :pending).execute.count, count: current_user.todos_pending_count,
done_count: TodosFinder.new(current_user, state: :done).execute.count done_count: current_user.todos_done_count
} }
end end
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
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
def execute(current_user = nil, options = {}) def execute(current_user = nil, project_ids_relation = nil)
segments = all_projects(current_user) segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
find_union(segments, Project) find_union(segments, Project)
end end
......
...@@ -27,9 +27,11 @@ class TodosFinder ...@@ -27,9 +27,11 @@ class TodosFinder
items = by_action_id(items) items = by_action_id(items)
items = by_action(items) items = by_action(items)
items = by_author(items) items = by_author(items)
items = by_project(items)
items = by_state(items) items = by_state(items)
items = by_type(items) items = by_type(items)
# Filtering by project HAS TO be the last because we use
# the project IDs yielded by the todos query thus far
items = by_project(items)
items.reorder(id: :desc) items.reorder(id: :desc)
end end
...@@ -91,14 +93,9 @@ class TodosFinder ...@@ -91,14 +93,9 @@ class TodosFinder
@project @project
end end
def projects def projects(items)
return @projects if defined?(@projects) item_project_ids = items.reorder(nil).select(:project_id)
ProjectsFinder.new.execute(current_user, item_project_ids)
if project?
@projects = project
else
@projects = ProjectsFinder.new.execute(current_user)
end
end end
def type? def type?
...@@ -136,8 +133,9 @@ class TodosFinder ...@@ -136,8 +133,9 @@ class TodosFinder
def by_project(items) def by_project(items)
if project? if project?
items = items.where(project: project) items = items.where(project: project)
elsif projects else
items = items.merge(projects).joins(:project) item_projects = projects(items)
items = items.merge(item_projects).joins(:project)
end end
items items
......
...@@ -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
......
module TodosHelper module TodosHelper
def todos_pending_count def todos_pending_count
@todos_pending_count ||= TodosFinder.new(current_user, state: :pending).execute.count @todos_pending_count ||= current_user.todos_pending_count
end end
def todos_done_count def todos_done_count
@todos_done_count ||= TodosFinder.new(current_user, state: :done).execute.count @todos_done_count ||= current_user.todos_done_count
end end
def todo_action_name(todo) def todo_action_name(todo)
......
...@@ -3,6 +3,9 @@ class Blob < SimpleDelegator ...@@ -3,6 +3,9 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
# The maximum size of an SVG that can be displayed.
MAXIMUM_SVG_SIZE = 2.megabytes
# Wrap a Gitlab::Git::Blob object, or return nil when given nil # Wrap a Gitlab::Git::Blob object, or return nil when given nil
# #
# This method prevents the decorated object from evaluating to "truthy" when # This method prevents the decorated object from evaluating to "truthy" when
...@@ -31,6 +34,10 @@ class Blob < SimpleDelegator ...@@ -31,6 +34,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG' text? && language && language.name == 'SVG'
end end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
def video? def video?
UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
end end
......
...@@ -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
...@@ -36,4 +36,10 @@ class Deployment < ActiveRecord::Base ...@@ -36,4 +36,10 @@ class Deployment < ActiveRecord::Base
def manual_actions def manual_actions
deployable.try(:other_actions) deployable.try(:other_actions)
end end
def includes_commit?(commit)
return false unless commit
project.repository.is_ancestor?(commit.id, sha)
end
end end
...@@ -25,4 +25,10 @@ class Environment < ActiveRecord::Base ...@@ -25,4 +25,10 @@ class Environment < ActiveRecord::Base
def nullify_external_url def nullify_external_url
self.external_url = nil if self.external_url.blank? self.external_url = nil if self.external_url.blank?
end end
def includes_commit?(commit)
return false unless last_deployment
last_deployment.includes_commit?(commit)
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
...@@ -610,6 +610,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -610,6 +610,14 @@ class MergeRequest < ActiveRecord::Base
!pipeline || pipeline.success? !pipeline || pipeline.success?
end end
def environments
return unless diff_head_commit
target_project.environments.select do |environment|
environment.includes_commit?(diff_head_commit)
end
end
def state_human_name def state_human_name
if merged? if merged?
"Merged" "Merged"
......
...@@ -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
......
class PivotaltrackerService < Service class PivotaltrackerService < Service
include HTTParty include HTTParty
prop_accessor :token API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
prop_accessor :token, :restrict_to_branch
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
def title def title
...@@ -18,7 +20,17 @@ class PivotaltrackerService < Service ...@@ -18,7 +20,17 @@ class PivotaltrackerService < Service
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: '' } {
type: 'text',
name: 'token',
placeholder: 'Pivotal Tracker API token.'
},
{
type: 'text',
name: 'restrict_to_branch',
placeholder: 'Comma-separated list of branches which will be ' \
'automatically inspected. Leave blank to include all branches.'
}
] ]
end end
...@@ -28,8 +40,8 @@ class PivotaltrackerService < Service ...@@ -28,8 +40,8 @@ class PivotaltrackerService < Service
def execute(data) def execute(data)
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
return unless allowed_branch?(data[:ref])
url = 'https://www.pivotaltracker.com/services/v5/source_commits'
data[:commits].each do |commit| data[:commits].each do |commit|
message = { message = {
'source_commit' => { 'source_commit' => {
...@@ -40,7 +52,7 @@ class PivotaltrackerService < Service ...@@ -40,7 +52,7 @@ class PivotaltrackerService < Service
} }
} }
PivotaltrackerService.post( PivotaltrackerService.post(
url, API_ENDPOINT,
body: message.to_json, body: message.to_json,
headers: { headers: {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
...@@ -49,4 +61,15 @@ class PivotaltrackerService < Service ...@@ -49,4 +61,15 @@ class PivotaltrackerService < Service
) )
end end
end end
private
def allowed_branch?(ref)
return true unless ref.present? && restrict_to_branch.present?
branch = Gitlab::Git.ref_name(ref)
allowed_branches = restrict_to_branch.split(',').map(&:strip)
branch.present? && allowed_branches.include?(branch)
end
end end
...@@ -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
...@@ -809,13 +809,13 @@ class User < ActiveRecord::Base ...@@ -809,13 +809,13 @@ class User < ActiveRecord::Base
def todos_done_count(force: false) def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
todos.done.count TodosFinder.new(self, state: :done).execute.count
end end
end end
def todos_pending_count(force: false) def todos_pending_count(force: false)
Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
todos.pending.count TodosFinder.new(self, state: :pending).execute.count
end 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
...@@ -30,10 +30,21 @@ module MergeRequests ...@@ -30,10 +30,21 @@ module MergeRequests
end end
def get_branches(changes) def get_branches(changes)
return [] if project.empty_repo?
return [] unless project.merge_requests_enabled
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
changes_list.map do |change| changes_list.map do |change|
next unless Gitlab::Git.branch_ref?(change[:ref]) next unless Gitlab::Git.branch_ref?(change[:ref])
Gitlab::Git.branch_name(change[:ref])
# Deleted branch
next if Gitlab::Git.blank_ref?(change[:newrev])
# Default branch
branch_name = Gitlab::Git.branch_name(change[:ref])
next if branch_name == project.default_branch
branch_name
end.compact end.compact
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
...@@ -144,8 +144,9 @@ class TodoService ...@@ -144,8 +144,9 @@ class TodoService
def mark_todos_as_done(todos, current_user) def mark_todos_as_done(todos, current_user)
todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all) todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
todos.update_all(state: :done) marked_todos = todos.update_all(state: :done)
current_user.update_todos_count_cache current_user.update_todos_count_cache
marked_todos
end end
# When user marks an issue as todo # When user marks an issue as todo
......
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
......
.file-content.image_file .file-content.image_file
- if blob.svg? - if blob.svg?
- if blob.size_within_svg_limits?
- # We need to scrub SVG but we cannot do so in the RawController: it would - # We need to scrub SVG but we cannot do so in the RawController: it would
- # be wrong/strange if RawController modified the data. - # be wrong/strange if RawController modified the data.
- blob.load_all_data!(@repository) - blob.load_all_data!(@repository)
- blob = sanitize_svg(blob) - blob = sanitize_svg(blob)
%img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
- else
.nothing-here-block
The SVG could not be displayed as it is too large, you can
#{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
instead.
- else - else
%img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))} %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
- 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
......
...@@ -42,3 +42,16 @@ ...@@ -42,3 +42,16 @@
.ci_widget.ci-error{style: "display:none"} .ci_widget.ci-error{style: "display:none"}
= icon("times-circle") = icon("times-circle")
Could not connect to the CI server. Please check your settings and try again. Could not connect to the CI server. Please check your settings and try again.
- @merge_request.environments.each do |environment|
.mr-widget-heading
.ci_widget.ci-success
= ci_icon_for_status("success")
%span.hidden-sm
Deployed to
= succeed '.' do
= link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
- external_url = environment.external_url
- if external_url
= link_to external_url, target: '_blank' do
= icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
...@@ -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
......
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
%li.project-row{ class: css_class } %li.project-row{ class: css_class }
= cache(cache_key) do = cache(cache_key) do
.controls .controls
- if project.archived
%span.label.label-warning archived
- if project.commit.try(:status) - if project.commit.try(:status)
%span %span
= render_commit_status(project.commit) = render_commit_status(project.commit)
......
...@@ -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',
...@@ -820,6 +829,7 @@ Rails.application.routes.draw do ...@@ -820,6 +829,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
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.
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