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

Merge remote-tracking branch 'upstream/master' into wall-clock-time-for-showing-pipeline

* upstream/master: (99 commits)
  Update CHANGELOG for 8.10.6, 8.9.7, and 8.8.8
  Updated Akismet documentation
  Add hover state to todos (!5361)
  Load issues and merge requests templates from repository
  Backport EE assertions in protected branch related specs.
  Revert "Merge branch '19957-write-tests-for-adding-comments-for-different-line-types-in-diff' into 'master'"
  Fix a missed `before_action` for `AutocompleteController`.
  Backport `AutocompleteController#load_project` from EE!581.
  Fix API::BranchesSpec.
  Fix failing tests relating to backporting ee!581.
  Revert unrelevant changes
  Fix the protected branches factory.
  Improve EE compatibility with protected branch access levels.
  Move the "update" portion of the protected branch view into a partial.
  Don't select an access level if already selected.
  Backport changes from gitlab-org/gitlab-ee!581 to CE.
  Further refactor and syntax fixes.
  Upgrade httpclient gem from 2.7.0.1 to 2.8.2.
  Make rubocop happy
  Make rubocop happy
  ...
parents 74c5f82f 1b338d59
...@@ -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)
...@@ -33,12 +35,16 @@ v 8.11.0 (unreleased) ...@@ -33,12 +35,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)
...@@ -55,6 +61,7 @@ v 8.11.0 (unreleased) ...@@ -55,6 +61,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
...@@ -79,6 +86,7 @@ v 8.11.0 (unreleased) ...@@ -79,6 +86,7 @@ v 8.11.0 (unreleased)
- Make "New issue" button in Issue page less obtrusive !5457 (winniehell) - Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
- Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
- Fix search for notes which belongs to deleted objects - Fix search for notes which belongs to deleted objects
- Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- Allow branch names ending with .json for graph and network page !5579 (winniehell) - Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem - Add the `sprockets-es6` gem
...@@ -109,6 +117,15 @@ v 8.11.0 (unreleased) ...@@ -109,6 +117,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
...@@ -130,6 +147,9 @@ v 8.10.3 ...@@ -130,6 +147,9 @@ v 8.10.3
- Fix importer for GitHub Pull Requests when a branch was removed. !5573 - Fix importer for GitHub Pull Requests when a branch was removed. !5573
- Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584 - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584
- Trim extra displayed carriage returns in diffs and files with CRLFs. !5588 - Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
- Fix label already exist error message in the right sidebar.
v 8.10.3 (unreleased)
v 8.10.2 v 8.10.2
- User can now search branches by name. !5144 - User can now search branches by name. !5144
...@@ -273,9 +293,11 @@ v 8.10.0 ...@@ -273,9 +293,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
...@@ -322,6 +344,10 @@ v 8.10.0 ...@@ -322,6 +344,10 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades - Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page - Show tooltip on GitLab export link in new project page
v 8.9.7
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Require administrator privileges to perform a project import.
v 8.9.6 v 8.9.6
- Fix importing of events under notes for GitLab projects. !5154 - Fix importing of events under notes for GitLab projects. !5154
- Fix log statements in import/export. !5129 - Fix log statements in import/export. !5129
...@@ -587,6 +613,9 @@ v 8.9.0 ...@@ -587,6 +613,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar - Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation - Add new sub nav style to Wiki and Graphs sub navigation
v 8.8.8
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
v 8.8.7 v 8.8.7
- Fix privilege escalation issue with OAuth external users. - Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users. - Ensure references to private repos aren't shown to logged-out users.
......
...@@ -338,7 +338,7 @@ GEM ...@@ -338,7 +338,7 @@ GEM
httparty (0.13.7) httparty (0.13.7)
json (~> 1.8) json (~> 1.8)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.7.0.1) httpclient (2.8.2)
i18n (0.7.0) i18n (0.7.0)
ice_nine (0.11.1) ice_nine (0.11.1)
influxdb (0.2.3) influxdb (0.2.3)
......
...@@ -9,10 +9,11 @@ ...@@ -9,10 +9,11 @@
licensePath: "/api/:version/licenses/:key", licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key", gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) { group: function(group_id, callback) {
var url; var url = Api.buildUrl(Api.groupPath)
url = Api.buildUrl(Api.groupPath); .replace(':id', group_id);
url = url.replace(':id', group_id);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -24,8 +25,7 @@ ...@@ -24,8 +25,7 @@
}); });
}, },
groups: function(query, skip_ldap, callback) { groups: function(query, skip_ldap, callback) {
var url; var url = Api.buildUrl(Api.groupsPath);
url = Api.buildUrl(Api.groupsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -39,8 +39,7 @@ ...@@ -39,8 +39,7 @@
}); });
}, },
namespaces: function(query, callback) { namespaces: function(query, callback) {
var url; var url = Api.buildUrl(Api.namespacesPath);
url = Api.buildUrl(Api.namespacesPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -54,8 +53,7 @@ ...@@ -54,8 +53,7 @@
}); });
}, },
projects: function(query, order, callback) { projects: function(query, order, callback) {
var url; var url = Api.buildUrl(Api.projectsPath);
url = Api.buildUrl(Api.projectsPath);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -70,9 +68,8 @@ ...@@ -70,9 +68,8 @@
}); });
}, },
newLabel: function(project_id, data, callback) { newLabel: function(project_id, data, callback) {
var url; var url = Api.buildUrl(Api.labelsPath)
url = Api.buildUrl(Api.labelsPath); .replace(':id', project_id);
url = url.replace(':id', project_id);
data.private_token = gon.api_token; data.private_token = gon.api_token;
return $.ajax({ return $.ajax({
url: url, url: url,
...@@ -86,9 +83,8 @@ ...@@ -86,9 +83,8 @@
}); });
}, },
groupProjects: function(group_id, query, callback) { groupProjects: function(group_id, query, callback) {
var url; var url = Api.buildUrl(Api.groupProjectsPath)
url = Api.buildUrl(Api.groupProjectsPath); .replace(':id', group_id);
url = url.replace(':id', group_id);
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
...@@ -102,8 +98,8 @@ ...@@ -102,8 +98,8 @@
}); });
}, },
licenseText: function(key, data, callback) { licenseText: function(key, data, callback) {
var url; var url = Api.buildUrl(Api.licensePath)
url = Api.buildUrl(Api.licensePath).replace(':key', key); .replace(':key', key);
return $.ajax({ return $.ajax({
url: url, url: url,
data: data data: data
...@@ -112,19 +108,32 @@ ...@@ -112,19 +108,32 @@
}); });
}, },
gitignoreText: function(key, callback) { gitignoreText: function(key, callback) {
var url; var url = Api.buildUrl(Api.gitignorePath)
url = Api.buildUrl(Api.gitignorePath).replace(':key', key); .replace(':key', key);
return $.get(url, function(gitignore) { return $.get(url, function(gitignore) {
return callback(gitignore); return callback(gitignore);
}); });
}, },
gitlabCiYml: function(key, callback) { gitlabCiYml: function(key, callback) {
var url; var url = Api.buildUrl(Api.gitlabCiYmlPath)
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); .replace(':key', key);
return $.get(url, function(file) { return $.get(url, function(file) {
return callback(file); return callback(file);
}); });
}, },
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
var url = Api.buildUrl(Api.issuableTemplatePath)
.replace(':key', key)
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
$.ajax({
url: url,
dataType: 'json'
}).done(function(file) {
callback(null, file);
}).error(callback);
},
buildUrl: function(url) { buildUrl: function(url) {
if (gon.relative_url_root != null) { if (gon.relative_url_root != null) {
url = gon.relative_url_root + url; url = gon.relative_url_root + url;
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
/*= require date.format */ /*= require date.format */
/*= require_directory ./behaviors */ /*= require_directory ./behaviors */
/*= require_directory ./blob */ /*= require_directory ./blob */
/*= require_directory ./templates */
/*= require_directory ./commit */ /*= require_directory ./commit */
/*= require_directory ./extensions */ /*= require_directory ./extensions */
/*= require_directory ./lib/utils */ /*= require_directory ./lib/utils */
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
} }
this.onClick = bind(this.onClick, this); this.onClick = bind(this.onClick, this);
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name'); this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown(); this.buildDropdown();
this.bindEvents(); this.bindEvents();
this.onFilenameUpdate(); this.onFilenameUpdate();
...@@ -60,11 +61,26 @@ ...@@ -60,11 +61,26 @@
return this.requestFile(item); return this.requestFile(item);
}; };
TemplateSelector.prototype.requestFile = function(item) {}; TemplateSelector.prototype.requestFile = function(item) {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
};
TemplateSelector.prototype.requestFileSuccess = function(file) { TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1); this.editor.setValue(file.content, 1);
return this.editor.focus(); if (!skipFocus) this.editor.focus();
};
TemplateSelector.prototype.startLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
};
TemplateSelector.prototype.stopLoadingSpinner = function() {
this.dropdownIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}; };
return TemplateSelector; return TemplateSelector;
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form')); new GLForm($('.issue-form'));
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
...@@ -62,6 +63,7 @@ ...@@ -62,6 +63,7 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form')); new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
this.render = bind(this.render, this); this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val(); this.VIEW_TYPE = $('input#view[type=hidden]').val();
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
$(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
} }
FilesCommentButton.prototype.render = function(e) { FilesCommentButton.prototype.render = function(e) {
......
...@@ -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> <% }); %>');
......
...@@ -70,13 +70,15 @@ ...@@ -70,13 +70,15 @@
name: newLabelField.val(), name: newLabelField.val(),
color: newColorField.val() color: newColorField.val()
}, function(label) { }, function(label) {
var errors;
$newLabelCreateButton.enable(); $newLabelCreateButton.enable();
if (label.message != null) { if (label.message != null) {
errors = _.map(label.message, function(value, key) { var errorText = label.message;
if (_.isObject(label.message)) {
errorText = _.map(label.message, function(value, key) {
return key + " " + value[0]; return key + " " + value[0];
}); }).join('<br/>');
return $newLabelError.html(errors.join("<br/>")).show(); }
return $newLabelError.html(errorText).show();
} else { } else {
return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
} }
......
...@@ -44,8 +44,8 @@ ...@@ -44,8 +44,8 @@
// Enable submit button // Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
this.$form.find('input[type="submit"]').removeAttr('disabled'); this.$form.find('input[type="submit"]').removeAttr('disabled');
......
...@@ -39,12 +39,14 @@ ...@@ -39,12 +39,14 @@
_method: 'PATCH', _method: 'PATCH',
id: this.$wrap.data('banchId'), id: this.$wrap.data('banchId'),
protected_branch: { protected_branch: {
merge_access_level_attributes: { merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val() access_level: $allowedToMergeInput.val()
}, }],
push_access_level_attributes: { push_access_levels_attributes: [{
id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val() access_level: $allowedToPushInput.val()
} }]
} }
}, },
success: () => { success: () => {
......
/*= require ../blob/template_selector */
((global) => {
class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
this.namespacePath = this.dropdown.data('namespace-path');
this.issuableType = this.wrapper.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
let initialQuery = {
name: this.dropdown.data('selected')
};
if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => {
if (this.currentTemplate) this.setInputValueToTemplateContent();
});
}
requestFile(query) {
this.startLoadingSpinner();
Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
this.setInputValueToTemplateContent();
});
return;
}
setInputValueToTemplateContent() {
// `this.requestFileSuccess` sets the value of the description input field
// to the content of the template selected.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the 2nd
// argument to `requestFileSuccess`.
this.requestFileSuccess(this.currentTemplate, true);
this.titleInput.focus();
} else {
this.requestFileSuccess(this.currentTemplate);
}
return;
}
}
global.IssuableTemplateSelector = IssuableTemplateSelector;
})(window);
((global) => {
class IssuableTemplateSelectors {
constructor(opts = {}) {
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
this.editor = opts.editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => {
let $dropdown = $(dropdown);
new IssuableTemplateSelector({
pattern: /(\.md)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
dropdown: $dropdown,
editor: this.editor
});
});
}
initEditor() {
let editor = $('.markdown-area');
// Proxy ace-editor's .setValue to jQuery's .val
editor.setValue = editor.val;
editor.getValue = editor.val;
return editor;
}
}
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
})(window);
...@@ -164,6 +164,10 @@ ...@@ -164,6 +164,10 @@
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
} }
&.btn-spam {
@include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
}
&.btn-danger, &.btn-danger,
&.btn-remove, &.btn-remove,
&.btn-red { &.btn-red {
......
...@@ -56,9 +56,13 @@ ...@@ -56,9 +56,13 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 6px; right: 6px;
margin-top: -4px; margin-top: -6px;
color: $dropdown-toggle-icon-color; color: $dropdown-toggle-icon-color;
font-size: 10px; font-size: 10px;
&.fa-spinner {
font-size: 16px;
margin-top: -8px;
}
} }
&:hover, { &:hover, {
...@@ -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
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
...@@ -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
......
...@@ -254,8 +254,9 @@ module Ci ...@@ -254,8 +254,9 @@ module Ci
end end
def execute_hooks def execute_hooks
project.execute_hooks(pipeline_data, :pipeline_hooks) data = pipeline_data
project.execute_services(pipeline_data, :pipeline_hooks) project.execute_hooks(data, :pipeline_hooks)
project.execute_services(data, :pipeline_hooks)
end end
private private
......
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
...@@ -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
...@@ -591,6 +591,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -591,6 +591,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"
......
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
...@@ -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
...@@ -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
......
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
......
...@@ -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
...@@ -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)
......
...@@ -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)
......
...@@ -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',
...@@ -813,6 +822,7 @@ Rails.application.routes.draw do ...@@ -813,6 +822,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
...@@ -869,7 +879,10 @@ Rails.application.routes.draw do ...@@ -869,7 +879,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
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectIdFromSpamLogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = true
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
DOWNTIME_REASON = 'Removing a column that contains data that is not used anywhere.'
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
remove_column :spam_logs, :project_id, :integer
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
disable_ddl_transaction!
def change
add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
end
end
...@@ -589,12 +589,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -589,12 +589,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.datetime "locked_at" t.datetime "locked_at"
t.integer "updated_by_id" t.integer "updated_by_id"
t.string "merge_error" t.string "merge_error"
t.text "merge_params"
t.boolean "merge_when_build_succeeds", default: false, null: false t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id" t.integer "merge_user_id"
t.string "merge_commit_sha" t.string "merge_commit_sha"
t.datetime "deleted_at" t.datetime "deleted_at"
t.string "in_progress_merge_commit_sha" t.string "in_progress_merge_commit_sha"
t.text "merge_params"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
...@@ -927,12 +927,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -927,12 +927,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.string "source_ip" t.string "source_ip"
t.string "user_agent" t.string "user_agent"
t.boolean "via_api" t.boolean "via_api"
t.integer "project_id"
t.string "noteable_type" t.string "noteable_type"
t.string "title" t.string "title"
t.text "description" t.text "description"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
end end
create_table "subscriptions", force: :cascade do |t| create_table "subscriptions", force: :cascade do |t|
...@@ -1000,6 +1000,16 @@ ActiveRecord::Schema.define(version: 20160810142633) do ...@@ -1000,6 +1000,16 @@ ActiveRecord::Schema.define(version: 20160810142633) do
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false
t.string "ip_address", null: false
t.integer "subject_id", null: false
t.string "subject_type", null: false
t.boolean "submitted", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false t.string "encrypted_password", default: "", null: false
......
...@@ -503,6 +503,7 @@ PUT /projects/:id/services/pivotaltracker ...@@ -503,6 +503,7 @@ PUT /projects/:id/services/pivotaltracker
Parameters: Parameters:
- `token` (**required**) - `token` (**required**)
- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.
### Delete PivotalTracker service ### Delete PivotalTracker service
...@@ -661,4 +662,3 @@ Get JetBrains TeamCity CI service settings for a project. ...@@ -661,4 +662,3 @@ Get JetBrains TeamCity CI service settings for a project.
``` ```
GET /projects/:id/services/teamcity GET /projects/:id/services/teamcity
``` ```
...@@ -32,6 +32,41 @@ project. ...@@ -32,6 +32,41 @@ project.
Clicking on a pipeline will show the builds that were run for that pipeline. Clicking on a pipeline will show the builds that were run for that pipeline.
## Badges
There are build status and test coverage report badges available.
Go to pipeline settings to see available badges and code you can use to embed
badges in the `README.md` or your website.
### Build status badge
You can access a build status badge image using following link:
```
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
### Test coverage report badge
GitLab makes it possible to define the regular expression for coverage report,
that each build log will be matched against. This means that each build in the
pipeline can have the test coverage percentage value defined.
You can access test coverage badge using following link:
```
http://example.gitlab.com/namespace/project/badges/branch/coverage.svg
```
If you would like to get the coverage report from the specific job, you can add
a `job=coverage_job_name` parameter to the URL. For example, it is possible to
use following Markdown code to embed the est coverage report into `README.md`:
```markdown
![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
```
[builds]: #builds [builds]: #builds
[jobs]: yaml/README.md#jobs [jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages [stages]: yaml/README.md#stages
......
...@@ -218,21 +218,13 @@ project's settings. ...@@ -218,21 +218,13 @@ project's settings.
For more information read the For more information read the
[Builds emails service documentation](../../project_services/builds_emails.md). [Builds emails service documentation](../../project_services/builds_emails.md).
## Builds badge
You can access a builds badge image using following link:
```
http://example.gitlab.com/namespace/project/badges/branch/build.svg
```
Awesome! You started using CI in GitLab!
## Examples ## Examples
Visit the [examples README][examples] to see a list of examples using GitLab Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages. CI with various languages.
Awesome! You started using CI in GitLab!
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner [runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md [examples]: ../examples/README.md
......
...@@ -30,7 +30,11 @@ ...@@ -30,7 +30,11 @@
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md)
## Databases
- [What requires downtime?](what_requires_downtime.md) - [What requires downtime?](what_requires_downtime.md)
- [Adding database indexes](adding_database_indexes.md)
## Compliance ## Compliance
......
# Adding Database Indexes
Indexes can be used to speed up database queries, but when should you add a new
index? Traditionally the answer to this question has been to add an index for
every column used for filtering or joining data. For example, consider the
following query:
```sql
SELECT *
FROM projects
WHERE user_id = 2;
```
Here we are filtering by the `user_id` column and as such a developer may decide
to index this column.
While in certain cases indexing columns using the above approach may make sense
it can actually have a negative impact. Whenever you write data to a table any
existing indexes need to be updated. The more indexes there are the slower this
can potentially become. Indexes can also take up quite some disk space depending
on the amount of data indexed and the index type. For example, PostgreSQL offers
"GIN" indexes which can be used to index certain data types that can not be
indexed by regular btree indexes. These indexes however generally take up more
data and are slower to update compared to btree indexes.
Because of all this one should not blindly add a new index for every column used
to filter data by. Instead one should ask themselves the following questions:
1. Can I write my query in such a way that it re-uses as many existing indexes
as possible?
2. Is the data going to be large enough that using an index will actually be
faster than just iterating over the rows in the table?
3. Is the overhead of maintaining the index worth the reduction in query
timings?
We'll explore every question in detail below.
## Re-using Queries
The first step is to make sure your query re-uses as many existing indexes as
possible. For example, consider the following query:
```sql
SELECT *
FROM todos
WHERE user_id = 123
AND state = 'open';
```
Now imagine we already have an index on the `user_id` column but not on the
`state` column. One may think this query will perform badly due to `state` being
unindexed. In reality the query may perform just fine given the index on
`user_id` can filter out enough rows.
The best way to determine if indexes are re-used is to run your query using
`EXPLAIN ANALYZE`. Depending on any extra tables that may be joined and
other columns being used for filtering you may find an extra index is not going
to make much (if any) difference. On the other hand you may determine that the
index _may_ make a difference.
In short:
1. Try to write your query in such a way that it re-uses as many existing
indexes as possible.
2. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
ideal query.
## Data Size
A database may decide not to use an index despite it existing in case a regular
sequence scan (= simply iterating over all existing rows) is faster. This is
especially the case for small tables.
If a table is expected to grow in size and you expect your query has to filter
out a lot of rows you may want to consider adding an index. If the table size is
very small (e.g. only a handful of rows) or any existing indexes filter out
enough rows you may _not_ want to add a new index.
## Maintenance Overhead
Indexes have to be updated on every table write. In case of PostgreSQL _all_
existing indexes will be updated whenever data is written to a table. As a
result of this having many indexes on the same table will slow down writes.
Because of this one should ask themselves: is the reduction in query performance
worth the overhead of maintaining an extra index?
If adding an index reduces SELECT timings by 5 milliseconds but increases
INSERT/UPDATE/DELETE timings by 10 milliseconds then the index may not be worth
it. On the other hand, if SELECT timings are reduced but INSERT/UPDATE/DELETE
timings are not affected you may want to add the index after all.
## Finding Unused Indexes
To see which indexes are unused you can run the following query:
```sql
SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
FROM pg_stat_all_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
AND idx_tup_read = 0
AND idx_tup_fetch = 0
ORDER BY pg_relation_size(indexrelname::regclass) desc;
```
This query outputs a list containing all indexes that are never used and sorts
them by indexes sizes in descending order. This query can be useful to
determine if any previously indexes are useful after all. More information on
the meaning of the various columns can be found at
<https://www.postgresql.org/docs/current/static/monitoring-stats.html>.
Because the output of this query relies on the actual usage of your database it
may be affected by factors such as (but not limited to):
* Certain queries never being executed, thus not being able to use certain
indexes.
* Certain tables having little data, resulting in PostgreSQL using sequence
scans instead of index scans.
In other words, this data is only reliable for a frequently used database with
plenty of data and with as many GitLab features enabled (and being used) as
possible.
...@@ -15,11 +15,14 @@ repository and maintained by GitLab UX designers. ...@@ -15,11 +15,14 @@ repository and maintained by GitLab UX designers.
## Navigation ## Navigation
GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu. GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo This menu will be visible regardless of what page you visit.
and the current user's profile picture. The content section contains a header and the content itself. The content section contains a header and the content itself. The header describes the current GitLab page and what navigation is
The header describes the current GitLab page and what navigation is available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group.
You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
### Adding new tab to header navigation ### Adding new tab to header navigation
...@@ -99,3 +102,6 @@ Do not use both green and blue button in one form. ...@@ -99,3 +102,6 @@ Do not use both green and blue button in one form.
display counts in the UI. display counts in the UI.
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter [number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
\ No newline at end of file
...@@ -22,14 +22,37 @@ To use Akismet: ...@@ -22,14 +22,37 @@ To use Akismet:
2. Sign-in or create a new account. 2. Sign-in or create a new account.
3. Click on "Show" to reveal the API key. 3. Click on **Show** to reveal the API key.
4. Go to Applications Settings on Admin Area (`admin/application_settings`) 4. Go to Applications Settings on Admin Area (`admin/application_settings`)
5. Check the `Enable Akismet` checkbox 5. Check the **Enable Akismet** checkbox
6. Fill in the API key from step 3. 6. Fill in the API key from step 3.
7. Save the configuration. 7. Save the configuration.
![Screenshot of Akismet settings](img/akismet_settings.png) ![Screenshot of Akismet settings](img/akismet_settings.png)
## Training
> *Note:* Training the Akismet filter is only available in 8.11 and above.
As a way to better recognize between spam and ham, you can train the Akismet
filter whenever there is a false positive or false negative.
When an entry is recognized as spam, it is rejected and added to the Spam Logs.
From here you can review if they are really spam. If one of them is not really
spam, you can use the **Submit as ham** button to tell Akismet that it falsely
recognized an entry as spam.
![Screenshot of Spam Logs](img/spam_log.png)
If an entry that is actually spam was not recognized as such, you will be able
to also submit this to Akismet. The **Submit as spam** button will only appear
to admin users.
![Screenshot of Issue](img/submit_issue.png)
Training Akismet will help it to recognize spam more accurately in the future.
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
> than that of the exporter. > than that of the exporter.
> - For existing installations, the project import option has to be enabled in > - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'. > application settings (`/admin/application_settings`) under 'Import sources'.
> Ask your administrator if you don't see the **GitLab export** button when > You will have to be an administrator to enable and use the import functionality.
> creating a new project.
> - You can find some useful raketasks if you are an administrator in the > - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md) > [import_export](../../../administration/raketasks/project_import_export.md)
> raketask. > raketask.
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
- [Share projects with other groups](share_projects_with_other_groups.md) - [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md) - [Web Editor](web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
- [Issuable Templates](issuable_templates.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Merge Requests](merge_requests.md) - [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md) - [Revert changes](revert_changes.md)
......
# Description templates
Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
![Description templates](img/description_templates.png)
_Example:_
`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
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