Commit 179ed967 authored by Regis's avatar Regis

Merge branch 'master' into auto-pipelines-vue

parents d8c73e3e a3cc2f1e
......@@ -2,4 +2,4 @@
/public/
/tmp/
/vendor/
/builds/
......@@ -4,7 +4,7 @@
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z_]+$"]
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
},
"globals": {
"$": false,
......
Please view this file on the master branch, on stable branches it's out of date.
**Note:** This file is automatically generated. Please see the [developer
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.14.0 (2016-11-22)
- Show correct environment log in admin/logs (@duk3luk3 !7191)
- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
- Backups do not fail anymore when using tar on annex and custom_hooks only. !5814
- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
......@@ -14,6 +18,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Cleaned up global namespace JS !19661 (Jose Ivan Vargas)
- Refactor Jira service to use jira-ruby gem
- Improved todos empty state
- Add hover to trash icon in notes !7008 (blackst0ne)
......@@ -23,6 +28,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix sidekiq stats in admin area (blackst0ne)
- Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file
- Hides container registry when repository is disabled
- API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954
- Stop unauthorized users dragging on milestone page (blackst0ne)
......@@ -42,6 +48,8 @@ Please view this file on the master branch, on stable branches it's out of date.
- New issue board list dropdown stays open after adding a new list
- Fix: Backup restore doesn't clear cache
- Optimize Event queries by removing default order
- Add new icon for skipped builds
- Show created icon in pipeline mini-graph
- Remove duplicate links from sidebar
- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh)
- Add Rake task to create/repair GitLab Shell hooks symlinks !5634
......@@ -54,16 +62,21 @@ Please view this file on the master branch, on stable branches it's out of date.
- Initialize Sidekiq with the list of queues used by GitLab
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
- Shortened merge request modal to let clipboard button not overlap
- Adds JavaScript validation for group path editing field
- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
- Improve search query parameter naming in /admin/users !7115 (YarNayar)
- Fix table pagination to be responsive
- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
- Updated commit SHA styling on the branches page.
## 8.13.3
## 8.13.3 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
- Fixed Import/Export foreign key issue to do with project members.
- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project
- Fix project features default values
- Changed build dropdown list length to be 6,5 builds long in the pipeline graph
## 8.13.2 (2016-10-31)
......@@ -253,6 +266,11 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
## 8.12.8 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
- Fixed Import/Export foreign key issue to do with project members.
## 8.12.7
- Prevent running `GfmAutocomplete` setup for each diff note. !6569
......@@ -512,6 +530,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
## 8.11.10 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
## 8.11.9
- Don't send Private-Token (API authentication) headers to Sentry
......@@ -752,6 +774,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
## 8.10.13 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
## 8.10.12
- Don't send Private-Token (API authentication) headers to Sentry
......
......@@ -104,7 +104,7 @@ gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.0'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
......@@ -260,9 +260,6 @@ group :development do
gem 'better_errors', '~> 1.0.1'
gem 'binding_of_caller', '~> 0.7.2'
# Docs generator
gem 'sdoc', '~> 0.3.20'
# thin instead webrick
gem 'thin', '~> 1.7.0'
end
......
......@@ -567,7 +567,7 @@ GEM
ffi (>= 0.5.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rdoc (3.12.2)
rdoc (4.2.2)
json (~> 1.4)
recaptcha (3.0.0)
json
......@@ -663,9 +663,6 @@ GEM
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
sdoc (0.3.20)
json (>= 1.1.3)
rdoc (~> 3.10)
seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
......@@ -936,7 +933,7 @@ DEPENDENCIES
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
rblineprof (~> 0.3.6)
rdoc (~> 3.6)
rdoc (~> 4.2)
recaptcha (~> 3.0)
redcarpet (~> 3.3.3)
redis (~> 3.2)
......@@ -956,7 +953,6 @@ DEPENDENCIES
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
sentry-raven (~> 2.0.0)
......
......@@ -80,7 +80,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Redis 2.8+
- MySQL or PostgreSQL
For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html).
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
## Third-party applications
......@@ -96,7 +96,7 @@ For upgrading information please see our [update page](https://about.gitlab.com/
## Documentation
All documentation can be found on [doc.gitlab.com/ce/](http://doc.gitlab.com/ce/).
All documentation can be found on [docs.gitlab.com/ce/](https://docs.gitlab.com/ce/).
## Getting help
......
......@@ -22,16 +22,14 @@
});
},
// Return groups list. Filtered by query
// Only active groups retrieved
groups: function(query, skip_ldap, skip_groups, callback) {
groups: function(query, options, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
search: query,
skip_groups: skip_groups,
per_page: 20
},
data: $.extend({
search: query,
per_page: 20
}, options),
dataType: "json"
}).done(function(groups) {
return callback(groups);
......
......@@ -13,6 +13,7 @@
/*= require jquery-ui/sortable */
/*= require jquery_ujs */
/*= require jquery.endless-scroll */
/*= require jquery.timeago */
/*= require jquery.highlight */
/*= require jquery.waitforimages */
/*= require jquery.atwho */
......@@ -54,125 +55,53 @@
/*= require_directory . */
/*= require fuzzaldrin-plus */
(function() {
window.slugify = function(text) {
return text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase();
};
window.ajaxGet = function(url) {
return $.ajax({
type: "GET",
url: url,
dataType: "script"
});
};
window.split = function(val) {
return val.split(/,\s*/);
};
window.extractLast = function(term) {
return split(term).pop();
};
window.rstrip = function(val) {
if (val) {
return val.replace(/\s+$/, '');
} else {
return val;
}
};
// Disable button if text field is empty
window.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
event_name = event_name || 'input';
var closest_submit, field;
field = $(field_selector);
closest_submit = field.closest('form').find(button_selector);
if (rstrip(field.val()) === "") {
closest_submit.disable();
}
return field.on(event_name, function() {
if (rstrip($(this).val()) === "") {
return closest_submit.disable();
} else {
return closest_submit.enable();
}
});
};
(function () {
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
window.addEventListener('hashchange', gl.utils.shiftWindow);
// Disable button if any input field with given selector is empty
window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
var closest_submit, updateButtons;
closest_submit = form.find(button_selector);
updateButtons = function() {
var filled;
filled = true;
form.find('input').filter(form_selector).each(function() {
return filled = rstrip($(this).val()) !== "" || !$(this).attr('required');
});
if (filled) {
return closest_submit.enable();
} else {
return closest_submit.disable();
}
};
updateButtons();
return form.keyup(updateButtons);
};
window.sanitize = function(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
};
window.shiftWindow = function() {
return scrollBy(0, -100);
};
document.addEventListener("page:fetch", gl.utils.cleanupBeforeFetch);
window.addEventListener("hashchange", shiftWindow);
window.onload = function() {
window.onload = function () {
// Scroll the window to avoid the topnav bar
// https://github.com/twitter/bootstrap/issues/1768
if (location.hash) {
return setTimeout(shiftWindow, 100);
return setTimeout(gl.utils.shiftWindow, 100);
}
};
$(function() {
var $body, $document, $sidebarGutterToggle, $window, bootstrapBreakpoint, checkInitialSidebarSize, fitSidebarForSize, flash;
$document = $(document);
$window = $(window);
$body = $('body');
$(function () {
var $body = $('body');
var $document = $(document);
var $window = $(window);
var $sidebarGutterToggle = $('.js-sidebar-toggle');
var $flash = $('.flash-container');
var bootstrapBreakpoint = bp.getBreakpointSize();
var checkInitialSidebarSize;
var fitSidebarForSize;
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
gl.utils.preventDisabledButtons();
bootstrapBreakpoint = bp.getBreakpointSize();
$(".nav-sidebar").niceScroll({
$('.nav-sidebar').niceScroll({
cursoropacitymax: '0.4',
cursorcolor: '#FFF',
cursorborder: "1px solid #FFF"
cursorborder: '1px solid #FFF'
});
$(".js-select-on-focus").on("focusin", function() {
return $(this).select().one('mouseup', function(e) {
$('.js-select-on-focus').on('focusin', function () {
return $(this).select().one('mouseup', function (e) {
return e.preventDefault();
});
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
});
$('.remove-row').bind('ajax:success', function() {
$('.remove-row').bind('ajax:success', function () {
$(this).tooltip('destroy')
.closest('li')
.fadeOut();
});
$('.js-remove-tr').bind('ajax:before', function() {
$('.js-remove-tr').bind('ajax:before', function () {
return $(this).hide();
});
$('.js-remove-tr').bind('ajax:success', function() {
$('.js-remove-tr').bind('ajax:success', function () {
return $(this).closest('tr').fadeOut();
});
$('select.select2').select2({
......@@ -180,8 +109,8 @@
// Initialize select2 selects
dropdownAutoWidth: true
});
$('.js-select2').bind('select2-close', function() {
return setTimeout((function() {
$('.js-select2').bind('select2-close', function () {
return setTimeout((function () {
$('.select2-container-active').removeClass('select2-container-active');
return $(':focus').blur();
}), 1);
......@@ -191,24 +120,24 @@
$.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
placement: function (_, el) {
return $(el).data('placement') || 'bottom';
}
});
$('.trigger-submit').on('change', function() {
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
// Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
// Flash
if ((flash = $(".flash-container")).length > 0) {
flash.click(function() {
if ($flash.length > 0) {
$flash.click(function () {
return $(this).fadeOut();
});
flash.show();
$flash.show();
}
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) {
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
buttons = $('[type="submit"]', this);
switch (e.type) {
......@@ -219,36 +148,36 @@
return buttons.enable();
}
});
$(document).ajaxError(function(e, xhrObj, xhrSetting, xhrErrorText) {
var ref;
$(document).ajaxError(function (e, xhrObj) {
var ref = xhrObj.status;
if (xhrObj.status === 401) {
return new Flash('You need to be logged in.', 'alert');
} else if ((ref = xhrObj.status) === 404 || ref === 500) {
} else if (ref === 404 || ref === 500) {
return new Flash('Something went wrong on our end.', 'alert');
}
});
$('.account-box').hover(function() {
$('.account-box').hover(function () {
// Show/Hide the profile menu when hovering the account box
return $(this).toggleClass('hover');
});
$document.on('click', '.diff-content .js-show-suppressed-diff', function() {
$document.on('click', '.diff-content .js-show-suppressed-diff', function () {
var $container;
$container = $(this).parent();
$container.next('table').show();
return $container.remove();
// Commit show suppressed diff
});
$('.navbar-toggle').on('click', function() {
$('.navbar-toggle').on('click', function () {
$('.header-content .title').toggle();
$('.header-content .header-logo').toggle();
$('.header-content .navbar-collapse').toggle();
return $('.navbar-toggle').toggleClass('active');
});
// Show/hide comments on diff
$body.on("click", ".js-toggle-diff-comments", function(e) {
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
$this.toggleClass('active');
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
$this.toggleClass('active');
if ($this.hasClass('active')) {
notesHolders.show().find('.hide').show();
} else {
......@@ -257,30 +186,27 @@
$this.trigger('blur');
return e.preventDefault();
});
$document.off("click", '.js-confirm-danger');
$document.on("click", '.js-confirm-danger', function(e) {
var btn, form, text;
$document.off('click', '.js-confirm-danger');
$document.on('click', '.js-confirm-danger', function (e) {
var btn = $(e.target);
var form = btn.closest('form');
var text = btn.data('confirm-danger-message');
e.preventDefault();
btn = $(e.target);
text = btn.data("confirm-danger-message");
form = btn.closest("form");
return new ConfirmDangerModal(form, text);
});
$document.on('click', 'button', function() {
$document.on('click', 'button', function () {
return $(this).blur();
});
$('input[type="search"]').each(function() {
var $this;
$this = $(this);
$('input[type="search"]').each(function () {
var $this = $(this);
$this.attr('value', $this.val());
});
$document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function(e) {
$document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () {
var $this;
$this = $(this);
return $this.attr('value', $this.val());
});
$sidebarGutterToggle = $('.js-sidebar-toggle');
$document.off('breakpoint:change').on('breakpoint:change', function(e, breakpoint) {
$document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) {
var $gutterIcon;
if (breakpoint === 'sm' || breakpoint === 'xs') {
$gutterIcon = $sidebarGutterToggle.find('i');
......@@ -289,7 +215,7 @@
}
}
});
fitSidebarForSize = function() {
fitSidebarForSize = function () {
var oldBootstrapBreakpoint;
oldBootstrapBreakpoint = bootstrapBreakpoint;
bootstrapBreakpoint = bp.getBreakpointSize();
......@@ -297,13 +223,13 @@
return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
}
};
checkInitialSidebarSize = function() {
checkInitialSidebarSize = function () {
bootstrapBreakpoint = bp.getBreakpointSize();
if (bootstrapBreakpoint === "xs" || "sm") {
if (bootstrapBreakpoint === 'xs' || 'sm') {
return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
}
};
$window.off("resize.app").on("resize.app", function(e) {
$window.off('resize.app').on('resize.app', function () {
return fitSidebarForSize();
});
gl.awardsHandler = new AwardsHandler();
......
/* eslint-disable */
(function() {
this.AwardsHandler = (function() {
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
......
......@@ -12,7 +12,7 @@
submit.disable();
$('.js-confirm-danger-input').off('input');
$('.js-confirm-danger-input').on('input', function() {
if (rstrip($(this).val()) === project_path) {
if (gl.utils.rstrip($(this).val()) === project_path) {
return submit.enable();
} else {
return submit.disable();
......
......@@ -299,7 +299,7 @@
};
Dispatcher.prototype.initFieldErrors = function() {
$('.show-gl-field-errors').each((i, form) => {
$('.gl-show-field-errors').each((i, form) => {
new gl.GlFieldErrors(form);
});
};
......
......@@ -126,8 +126,8 @@
}
return {
username: m.username,
title: sanitize(title),
search: sanitize(m.username + " " + m.name)
title: gl.utils.sanitize(title),
search: gl.utils.sanitize(m.username + " " + m.name)
};
});
}
......@@ -159,7 +159,7 @@
}
return {
id: i.iid,
title: sanitize(i.title),
title: gl.utils.sanitize(i.title),
search: i.iid + " " + i.title
};
});
......@@ -189,7 +189,7 @@
}
return {
id: m.iid,
title: sanitize(m.title),
title: gl.utils.sanitize(m.title),
search: "" + m.title
};
});
......@@ -222,7 +222,7 @@
}
return {
id: m.iid,
title: sanitize(m.title),
title: gl.utils.sanitize(m.title),
search: m.iid + " " + m.title
};
});
......@@ -240,9 +240,9 @@
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
return "\"" + (sanitize(title)) + "\"";
return "\"" + (gl.utils.sanitize(title)) + "\"";
} else {
return sanitize(title);
return gl.utils.sanitize(title);
}
};
return $.map(merges, function(m) {
......
/* eslint-disable no-param-reassign */
((global) => {
/*
* This class overrides the browser's validation error bubbles, displaying custom
* error messages for invalid fields instead. To begin validating any form, add the
* class `gl-show-field-errors` to the form element, and ensure error messages are
* declared in each inputs' `title` attribute. If no title is declared for an invalid
* field the user attempts to submit, "This field is required." will be shown by default.
*
* Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input.
*
* Set a custom error anchor for error message to be injected after with the
* class `gl-field-error-anchor`
*
* Examples:
*
* Basic:
*
* <form class='gl-show-field-errors'>
* <input type='text' name='username' title='Username is required.'/>
* </form>
*
* Ignore specific inputs (e.g. UsernameValidator):
*
* <form class='gl-show-field-errors'>
* <div class="form-group>
* <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/>
* </div>
* <div class="form-group">
* <input type='text' name='username' title='Username is required.'/>
* </div>
* </form>
*
* Custom Error Anchor (allows error message to be injected after specified element):
*
* <form class='gl-show-field-errors'>
* <div class="form-group gl-field-error-anchor">
* <input type='text' name='username' title='Username is required.'/>
* // Error message typically injected here
* </div>
* // Error message now injected here
* </form>
*
* */
/*
* Regex Patterns in use:
*
* Only alphanumeric: : "[a-zA-Z0-9]+"
* No special characters : "[a-zA-Z0-9-_]+",
*
* */
const errorMessageClass = 'gl-field-error';
const inputErrorClass = 'gl-field-error-outline';
const errorAnchorSelector = '.gl-field-error-anchor';
const ignoreInputSelector = '.gl-field-error-ignore';
class GlFieldError {
constructor({ input, formErrors }) {
this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0);
this.form = formErrors;
this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
this.state = {
valid: false,
empty: true,
};
this.initFieldValidation();
}
initFieldValidation() {
const customErrorAnchor = this.inputElement.parents(errorAnchorSelector);
const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement;
// hidden when injected into DOM
errorAnchor.after(this.fieldErrorElement);
this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
this.scopedSiblings = this.safelySelectSiblings();
}
safelySelectSiblings() {
// Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled
const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`);
const parentContainer = this.inputElement.parent('.form-group');
// Only select siblings when they're scoped within a form-group with one input
const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
}
renderValidity() {
this.renderClear();
if (this.state.valid) {
this.renderValid();
} else if (this.state.empty) {
this.renderEmpty();
} else if (!this.state.valid) {
this.renderInvalid();
}
}
handleInvalidSubmit(event) {
event.preventDefault();
const currentValue = this.accessCurrentValue();
this.state.valid = false;
this.state.empty = currentValue === '';
this.renderValidity();
this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup
this.inputElement.off('keyup.fieldValidator')
.on('keyup.fieldValidator', this.updateValidity.bind(this));
}
/* Get or set current input value */
accessCurrentValue(newVal) {
return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
}
getInputValidity() {
return this.inputDomElement.validity.valid;
}
updateValidity() {
const inputVal = this.accessCurrentValue();
this.state.empty = !inputVal.length;
this.state.valid = this.getInputValidity();
this.renderValidity();
}
renderValid() {
return this.renderClear();
}
renderEmpty() {
return this.renderInvalid();
}
renderInvalid() {
this.inputElement.addClass(inputErrorClass);
this.scopedSiblings.hide();
return this.fieldErrorElement.show();
}
renderClear() {
const inputVal = this.accessCurrentValue();
if (!inputVal.split(' ').length) {
const trimmedInput = inputVal.trim();
this.accessCurrentValue(trimmedInput);
}
this.inputElement.removeClass(inputErrorClass);
this.scopedSiblings.hide();
this.fieldErrorElement.hide();
}
}
global.GlFieldError = GlFieldError;
})(window.gl || (window.gl = {}));
/* eslint-disable */
((global) => {
/*
* This class overrides the browser's validation error bubbles, displaying custom
* error messages for invalid fields instead. To begin validating any form, add the
* class `show-gl-field-errors` to the form element, and ensure error messages are
* declared in each inputs' title attribute.
*
* Example:
*
* <form class='show-gl-field-errors'>
* <input type='text' name='username' title='Username is required.'/>
*</form>
*
* */
const errorMessageClass = 'gl-field-error';
const inputErrorClass = 'gl-field-error-outline';
class GlFieldError {
constructor({ input, formErrors }) {
this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0);
this.form = formErrors;
this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`);
this.state = {
valid: false,
empty: true
};
this.initFieldValidation();
}
initFieldValidation() {
// hidden when injected into DOM
this.inputElement.after(this.fieldErrorElement);
this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this));
this.scopedSiblings = this.safelySelectSiblings();
}
safelySelectSiblings() {
// Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity
const ignoreSelector = '.validation-ignore';
const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`);
const parentContainer = this.inputElement.parent('.form-group');
// Only select siblings when they're scoped within a form-group with one input
const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1;
return safelyScoped ? unignoredSiblings : this.fieldErrorElement;
}
renderValidity() {
this.renderClear();
if (this.state.valid) {
return this.renderValid();
}
if (this.state.empty) {
return this.renderEmpty();
}
if (!this.state.valid) {
return this.renderInvalid();
}
}
//= require gl_field_error
handleInvalidSubmit(event) {
event.preventDefault();
const currentValue = this.accessCurrentValue();
this.state.valid = false;
this.state.empty = currentValue === '';
this.renderValidity();
this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup
this.inputElement.off('keyup.field_validator')
.on('keyup.field_validator', this.updateValidity.bind(this));
}
/* Get or set current input value */
accessCurrentValue(newVal) {
return newVal ? this.inputElement.val(newVal) : this.inputElement.val();
}
getInputValidity() {
return this.inputDomElement.validity.valid;
}
updateValidity() {
const inputVal = this.accessCurrentValue();
this.state.empty = !inputVal.length;
this.state.valid = this.getInputValidity();
this.renderValidity();
}
renderValid() {
return this.renderClear();
}
renderEmpty() {
return this.renderInvalid();
}
renderInvalid() {
this.inputElement.addClass(inputErrorClass);
this.scopedSiblings.hide();
return this.fieldErrorElement.show();
}
renderClear() {
const inputVal = this.accessCurrentValue();
if (!inputVal.split(' ').length) {
const trimmedInput = inputVal.trim();
this.accessCurrentValue(trimmedInput);
}
this.inputElement.removeClass(inputErrorClass);
this.scopedSiblings.hide();
this.fieldErrorElement.hide();
}
}
const customValidationFlag = 'no-gl-field-errors';
((global) => {
const customValidationFlag = 'gl-field-error-ignore';
class GlFieldErrors {
constructor(form) {
......@@ -144,7 +22,7 @@
this.state.inputs = this.form.find(validateSelectors).toArray()
.filter((input) => !input.classList.contains(customValidationFlag))
.map((input) => new GlFieldError({ input, formErrors: this }));
.map((input) => new global.GlFieldError({ input, formErrors: this }));
this.form.on('submit', this.catchInvalidFormSubmit);
}
......
......@@ -24,8 +24,8 @@
if (isNewForm) {
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
......
......@@ -6,15 +6,16 @@
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
return function(i, select) {
var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap');
var all_available, skip_groups;
all_available = $(select).data('all-available');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({
placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
options = { all_available: all_available, skip_groups: skip_groups };
return Api.groups(query.term, options, function(groups) {
var data;
data = {
results: groups
......
......@@ -24,6 +24,81 @@
return null;
}
};
w.gl.utils.ajaxGet = function(url) {
return $.ajax({
type: "GET",
url: url,
dataType: "script"
});
};
w.gl.utils.split = function(val) {
return val.split(/,\s*/);
};
w.gl.utils.extractLast = function(term) {
return this.split(term).pop();
};
w.gl.utils.rstrip = function rstrip(val) {
if (val) {
return val.replace(/\s+$/, '');
} else {
return val;
}
};
w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
event_name = event_name || 'input';
var closest_submit, field, that;
that = this;
field = $(field_selector);
closest_submit = field.closest('form').find(button_selector);
if (this.rstrip(field.val()) === "") {
closest_submit.disable();
}
return field.on(event_name, function() {
if (that.rstrip($(this).val()) === "") {
return closest_submit.disable();
} else {
return closest_submit.enable();
}
});
};
w.gl.utils.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
var closest_submit, updateButtons;
closest_submit = form.find(button_selector);
updateButtons = function() {
var filled;
filled = true;
form.find('input').filter(form_selector).each(function() {
return filled = this.rstrip($(this).val()) !== "" || !$(this).attr('required');
});
if (filled) {
return closest_submit.enable();
} else {
return closest_submit.disable();
}
};
updateButtons();
return form.keyup(updateButtons);
};
w.gl.utils.sanitize = function(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
};
w.gl.utils.unbindEvents = function() {
return $(document).off('scroll');
};
w.gl.utils.shiftWindow = function() {
return w.scrollBy(0, -100);
};
gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
return $tooltipEl.tooltip('destroy').attr('title', newTitle).tooltip('fixTitle');
};
......
......@@ -11,7 +11,7 @@
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit);
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess);
disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
removeRow(e) {
......
......@@ -45,7 +45,9 @@
};
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
var $repoAccessLevel = $('.js-repo-access-level select'),
containerRegistry = document.querySelectorAll('.js-container-registry')[0],
containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
......@@ -70,8 +72,17 @@
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
if (containerRegistry) {
containerRegistry.style.display = '';
}
} else {
this.$repoSelects.addClass('disabled');
if (containerRegistry) {
containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false;
}
}
}.bind(this));
};
......
......@@ -24,7 +24,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(term, false, false, groupsCallback);
return Api.groups(term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
......@@ -73,7 +73,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, false, false, groupsCallback);
return Api.groups(query.term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
......
......@@ -11,7 +11,7 @@
filterable: true,
fieldName: 'group_id',
data: function(term, callback) {
return Api.groups(term, false, false, function(data) {
return Api.groups(term, {}, function(data) {
data.unshift({
name: 'Any'
});
......
......@@ -36,7 +36,7 @@
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border: 1px solid $border-color;
border-radius: $border-radius-base;
outline: 0;
text-overflow: ellipsis;
......@@ -45,11 +45,9 @@
.fa {
position: absolute;
top: 50%;
right: 6px;
margin-top: -6px;
top: 10px;
right: 8px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
&.fa-spinner {
font-size: 16px;
......
......@@ -136,3 +136,35 @@ label {
color: $red-normal;
}
.gl-show-field-errors {
.gl-field-success-outline {
border: 1px solid $green-normal;
&:focus {
box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal;
border: 0 none;
}
}
.gl-field-error-outline {
border: 1px solid $red-normal;
&:focus {
box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6);
border: 0 none;
}
}
.gl-field-success-message {
color: $green-normal;
}
.gl-field-error-message {
color: $red-normal;
}
.gl-field-hint {
color: $gl-text-color;
}
}
......@@ -27,9 +27,9 @@
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: $caret-width-base dashed;
border-right: $caret-width-base solid transparent;
border-left: $caret-width-base solid transparent;
border-top: 5px dashed;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
color: $gray-darkest;
}
}
......
......@@ -164,7 +164,22 @@
.branch-commit {
color: $gl-gray;
.commit-id,
.commit-icon {
text-align: center;
display: inline-block;
svg {
height: 14px;
width: 14px;
vertical-align: middle;
fill: $table-text-gray;
}
}
.commit-id {
color: $gl-link-color;
}
.commit-row-message {
color: $gl-gray;
}
......
......@@ -55,6 +55,10 @@
float: left;
}
.file-buttons {
font-size: 0;
}
.select2 {
float: right;
}
......
// CI icon colors
.ci-status-icon {
&-created {
fill: $gray-darkest;
}
&-skipped,
&-canceled {
fill: $gl-text-color;
}
}
......@@ -75,43 +75,17 @@
.login-body {
font-size: 13px;
input + p {
margin-top: 5px;
}
.gl-field-success-outline {
border: 1px solid $green-normal;
&:focus {
box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal;
border: 0 none;
}
}
.gl-field-error-outline {
border: 1px solid $red-normal;
&:focus {
box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6);
border: 0 none;
}
}
.username .validation-success,
.gl-field-success-message {
.username .validation-success {
color: $green-normal;
}
.username .validation-error,
.gl-field-error-message {
.username .validation-error {
color: $red-normal;
}
.gl-field-hint {
color: $gl-text-color;
}
}
}
......
......@@ -85,6 +85,11 @@
}
.commit-link {
a {
&:focus {
text-decoration: none;
}
}
.ci-status {
......@@ -439,7 +444,7 @@
}
.grouped-pipeline-dropdown {
padding: 8px 0;
padding: 0;
width: 186px;
left: auto;
right: -197px;
......@@ -448,6 +453,14 @@
ul {
max-height: 245px;
overflow: auto;
li:first-child {
padding-top: 8px;
}
li:last-child {
padding-bottom: 8px;
}
}
a {
......
......@@ -6,7 +6,8 @@
white-space: nowrap;
border-radius: 4px;
&:hover {
&:hover,
&:focus {
text-decoration: none;
}
......
......@@ -116,8 +116,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_packet_size,
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:repository_storage,
:enabled_git_access_protocol,
repository_storages: [],
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: []
......
......@@ -7,7 +7,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
@group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.group.try(:id)
@skip_groups << project.namespace_id unless project.personal?
end
def create
......
......@@ -93,11 +93,11 @@ module ApplicationSettingsHelper
end
end
def repository_storage_options_for_select
def repository_storages_options_for_select
options = Gitlab.config.repositories.storages.map do |name, path|
["#{name} - #{path}", name]
end
options_for_select(options, @application_setting.repository_storage)
options_for_select(options, @application_setting.repository_storages)
end
end
......@@ -47,8 +47,10 @@ module CiStatusHelper
'icon_play'
when 'created'
'icon_status_created'
when 'skipped'
'icon_status_skipped'
else
'icon_status_cancel'
'icon_status_canceled'
end
custom_icon(icon_name)
......
......@@ -43,7 +43,7 @@ module DropdownsHelper
default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down')
output << icon('caret-down')
output.html_safe
end
end
......
......@@ -18,6 +18,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :disabled_oauth_sign_in_sources, Array
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
serialize :repository_storages
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
......@@ -74,9 +75,8 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :repository_storages, presence: true
validate :check_repository_storages
validates :enabled_git_access_protocol,
inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
......@@ -166,7 +166,7 @@ class ApplicationSetting < ActiveRecord::Base
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
container_registry_token_expire_delay: 5,
repository_storage: 'default',
repository_storages: ['default'],
user_default_external: false,
)
end
......@@ -201,6 +201,29 @@ class ApplicationSetting < ActiveRecord::Base
self.domain_blacklist_raw = file.read
end
def repository_storages
value = read_attribute(:repository_storages)
value = [value] if value.is_a?(String)
value = [] if value.nil?
value
end
# repository_storage is still required in the API. Remove in 9.0
def repository_storage
repository_storages.first
end
def repository_storage=(value)
self.repository_storages = [value]
end
# Choose one of the available repository storage options. Currently all have
# equal weighting.
def pick_repository_storage
repository_storages.sample
end
def runners_registration_token
ensure_runners_registration_token!
end
......@@ -208,4 +231,12 @@ class ApplicationSetting < ActiveRecord::Base
def health_check_access_token
ensure_health_check_access_token!
end
private
def check_repository_storages
invalid = repository_storages - Gitlab.config.repositories.storages.keys
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
invalid.empty?
end
end
......@@ -441,11 +441,11 @@ class MergeRequest < ActiveRecord::Base
end
def should_remove_source_branch?
merge_params['should_remove_source_branch'].present?
Gitlab::Utils.to_boolean(merge_params['should_remove_source_branch'])
end
def force_remove_source_branch?
merge_params['force_remove_source_branch'].present?
Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
end
def remove_source_branch?
......
......@@ -28,7 +28,7 @@ class Project < ActiveRecord::Base
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
......
= render 'devise/shared/tab_single', tab_title: 'Sign in preview'
.login-box
%form.show-gl-field-errors
%form.gl-show-field-errors
.form-group
= label_tag :login
= text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
......
......@@ -353,9 +353,9 @@
%fieldset
%legend Repository Storage
.form-group
= f.label :repository_storage, 'Storage path for new projects', class: 'control-label col-sm-2'
= f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
.col-sm-10
= f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control'
= f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control'
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
......
- @no_container = true
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger]
= render 'admin/background_jobs/head'
......
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{current_user.name} issues"
xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
......
......@@ -2,13 +2,13 @@
- header_title "Issues", issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{current_user.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- if current_user
= link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
......
= render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions'
.login-box
.login-body
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
.form-group
......
= render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box
.login-body
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
= f.hidden_field :reset_password_token
......
= render 'devise/shared/tab_single', tab_title: 'Reset Password'
.login-box
.login-body
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
.form-group
......
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f|
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
%div.form-group
= f.label "Username or email", for: :login
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
......
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do
.form-group
= label_tag :username, 'Username or email'
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
......
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
= label_tag :username, "#{server['label']} Username"
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
......
......@@ -7,7 +7,7 @@
.login-box
.login-body
- if @user.two_factor_otp_enabled?
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
......
#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
.login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user show-gl-field-errors", "aria-live" => "assertive" }) do |f|
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= devise_error_messages!
%div.form-group
......@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
%div.username.form-group
= f.label :username
= f.text_field :username, class: "form-control middle no-gl-field-error", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
= f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
......
= render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions'
.login-box
.login-body
= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= devise_error_messages!
.form-group.append-bottom-20
......
......@@ -2,7 +2,7 @@
.panel-heading
Group settings
.panel-body
= form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f|
= form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
......
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@group.name} issues"
xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_group_url
xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
......
- page_title "Issues"
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- if current_user
= link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
......
......@@ -5,7 +5,7 @@
New Group
%hr
= form_for @group, html: { class: 'group-form form-horizontal' } do |f|
= form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f|
= form_errors(@group)
= render 'shared/group_form', f: f, autofocus: true
......
......@@ -26,8 +26,8 @@
Perform code reviews and enhance collaboration with merge requests.
Each project can also have an issue tracker and a wiki.
- if current_application_settings.sign_in_text.present?
= markdown_field(current_application_settings, :sign_in_text)
- if current_application_settings.sign_in_text.present?
= markdown_field(current_application_settings, :sign_in_text)
%hr.footer-fixed
.container.footer-container
......
......@@ -14,13 +14,13 @@
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name'
.pull-right
.pull-right.file-buttons
.license-selector.js-license-selector-wrap.hidden
= dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
= dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.hidden
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
%span.no-wrap
= custom_icon('icon_no_wrap')
......
......@@ -26,6 +26,6 @@
:javascript
disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
gl.utils.disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
new NewCommitForm($('.js-upload-blob-form'))
.branch-commit
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace"
&middot;
%span.str-truncated
......
......@@ -41,7 +41,7 @@
- else
Cant find HEAD commit for this branch
- stages_status = pipeline.statuses.relevant.latest.stages_status
- stages_status = pipeline.statuses.latest.stages_status
%td.stage-cell
- stages.each do |stage|
- status = stages_status[stage]
......
......@@ -102,7 +102,7 @@
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- if Gitlab.config.registry.enabled
.form-group
.form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
.checkbox
= f.label :container_registry_enabled do
= f.check_box :container_registry_enabled
......@@ -290,4 +290,4 @@
Saving project.
%p Please wait a moment, this page will automatically refresh when ready.
= render 'shared/confirm_modal', phrase: @project.path
\ No newline at end of file
= render 'shared/confirm_modal', phrase: @project.path
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@project.name} issues"
xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: url_for(params), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any?
......
......@@ -8,7 +8,7 @@
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
%div{ class: (container_class) }
- if @project.issues.any?
......@@ -16,7 +16,7 @@
= render 'shared/issuable/nav', type: :issues
.nav-controls
- if current_user
= link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10' do
= icon('rss')
%span.icon-label
Subscribe
......
......@@ -12,7 +12,7 @@
= render 'projects/last_push'
= render "home_panel"
- if @project.feature_available?(:repository, current_user)
- if current_user && can?(current_user, :download_code, @project)
%nav.project-stats{ class: container_class }
%ul.nav
%li
......
......@@ -37,5 +37,5 @@
:javascript
// Load last commit log for each file in tree
$('#tree-slider').waitForImages(function() {
ajaxGet("#{escape_javascript(@logs_path)}");
gl.utils.ajaxGet("#{escape_javascript(@logs_path)}");
});
......@@ -9,11 +9,13 @@
= f.label :path, class: 'control-label' do
Group path
.col-sm-10
.input-group
.input-group.gl-field-error-anchor
.input-group-addon
= root_url
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false
autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+",
required: true, title: 'Please choose a group name with no special characters.'
- if @group.persisted?
.alert.alert-warning.prepend-top-10
%ul
......
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-canceled" viewBox="0 0 14 14">
<g fill="#5C5C5C" fill-rule="evenodd">
<path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/>
<rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/>
......
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" class="ci-status-icon-created" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
......@@ -142,6 +142,7 @@
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[force_remove_source_branch]' do
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0'
= check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
Remove source branch when merge request is accepted.
......
......@@ -22,7 +22,7 @@
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
= multi_label_name(selected, "Labels")
= icon('chevron-down')
= icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)
......
......@@ -12,6 +12,7 @@ Options = Struct.new(
:amend,
:author,
:dry_run,
:force,
:merge_request,
:title
)
......@@ -21,7 +22,7 @@ class ChangelogOptionParser
options = Options.new
parser = OptionParser.new do |opts|
opts.banner = "Usage: #{__FILE__} [options]"
opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
# Note: We do not provide a shorthand for this in order to match the `git
# commit` interface
......@@ -29,6 +30,10 @@ class ChangelogOptionParser
options.amend = value
end
opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
options.force = value
end
opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
options.merge_request = value
end
......@@ -111,8 +116,9 @@ class ChangelogEntry
def assert_new_file!
return unless File.exist?(file_path)
return if options.force
fail_with "#{file_path} already exists!"
fail_with "#{file_path} already exists! Use `--force` to overwrite."
end
def assert_title!
......
---
title: Issues atom feed url reflect filters on dashboard
merge_request: 7114
author: Lucas Deschamps
---
title: Only skip group when it's actually a group in the "Share with group" select
merge_request: 7262
author:
---
title: 'Fix: Guest sees some repository details and gets 404'
merge_request:
author:
---
title: Introduce round-robin project creation to spread load over multiple shards
merge_request: 7266
author:
---
title: Ensure merge request's "remove branch" accessors return booleans
merge_request: 7267
author:
---
title: Fix invalid filename validation on eslint
merge_request: 7281
author:
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RenameRepositoryStorageColumn < 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 = ''
# 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
rename_column :application_settings, :repository_storage, :repository_storages
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161025231710) do
ActiveRecord::Schema.define(version: 20161103171205) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -88,7 +88,7 @@ ActiveRecord::Schema.define(version: 20161025231710) do
t.integer "container_registry_token_expire_delay", default: 5
t.text "after_sign_up_text"
t.boolean "user_default_external", default: false, null: false
t.string "repository_storage", default: "default"
t.string "repository_storages", default: "default"
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
......
......@@ -13,7 +13,8 @@ This guide talks about how to read and use these system log files.
This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
omnibus package or in `/home/git/gitlab/log/production.log` for
installations from source.
installations from source. (When Gitlab is running in an environment
other than production, the corresponding logfile is shown here.)
It contains information about all performed requests. You can see the
URL and type of request, IP address and what exactly parts of code were
......
......@@ -91,6 +91,9 @@ be stored via the **Application Settings** in the Admin area.
![Choose repository storage path in Admin area](img/repository_storages_admin_ui.png)
Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be
randomly placed on one of the selected paths.
[ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578
[restart gitlab]: restart_gitlab.md#installations-from-source
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
......
......@@ -2,7 +2,12 @@
## List groups
Get a list of groups. (As user: my groups, as admin: all groups)
Get a list of groups. (As user: my groups or all available, as admin: all groups).
Parameters:
- `all_available` (optional) - if passed, show all groups you have access to
- `skip_groups` (optional)(array of group IDs) - if passed, skip groups
```
GET /groups
......@@ -21,7 +26,6 @@ GET /groups
You can search for groups by name or path, see below.
## List a group's projects
Get a list of projects in this group.
......
......@@ -42,6 +42,7 @@ Example response:
"sign_in_text" : null,
"container_registry_token_expire_delay": 5,
"repository_storage": "default",
"repository_storages": ["default"],
"koding_enabled": false,
"koding_url": null
}
......@@ -73,7 +74,8 @@ PUT /application/settings
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
| `repository_storage` | string | no | The first entry in `repository_storages`. Deprecated, but retained for compatibility reasons |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
......
......@@ -27,11 +27,14 @@ Example response:
```json
[
{
"id" : 1,
"url" : "https://gitlab.example.com/hook",
"created_at" : "2015-11-04T20:07:35.874Z"
}
{
"id":1,
"url":"https://gitlab.example.com/hook",
"created_at":"2016-10-31T12:32:15.192Z",
"push_events":true,
"tag_push_events":false,
"enable_ssl_verification":true
}
]
```
......@@ -48,6 +51,10 @@ POST /hooks
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `url` | string | yes | The hook URL |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
| `push_events` | boolean | no | When true, the hook will fire on push events |
| `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
Example request:
......@@ -59,11 +66,14 @@ Example response:
```json
[
{
"id" : 2,
"url" : "https://gitlab.example.com/hook",
"created_at" : "2015-11-04T20:07:35.874Z"
}
{
"id":1,
"url":"https://gitlab.example.com/hook",
"created_at":"2016-10-31T12:32:15.192Z",
"push_events":true,
"tag_push_events":false,
"enable_ssl_verification":true
}
]
```
......
......@@ -226,7 +226,7 @@ e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_ap
> **Note:**
This feature requires GitLab 8.8 and GitLab Runner 1.2.
Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../container_registry/README.md). For example, if you're using
Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, if you're using
docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
......
......@@ -21,6 +21,9 @@ The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
contributors. Both are optional.
Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members should not.
If you're working on the GitLab EE repository, the entry will be added to
`changelogs/unreleased-ee/` instead.
......@@ -46,13 +49,14 @@ author:
The entry filename is based on the name of the current Git branch. If you run
the command above on a branch called `feature/hey-dz`, it will generate a
`changelogs/unreleased/feature-hey-dz` file.
`changelogs/unreleased/feature-hey-dz.yml` file.
### Arguments
| Argument | Shorthand | Purpose |
| ----------------- | --------- | --------------------------------------------- |
| `--amend` | | Amend the previous commit |
| `--force` | `-f` | Overwrite an existing entry |
| `--merge-request` | `-m` | Merge Request ID |
| `--dry-run` | `-n` | Don't actually write anything, just print |
| `--git-username` | `-u` | Use Git user.name configuration as the author |
......@@ -79,6 +83,23 @@ merge_request:
author:
```
#### `--force` or `-f`
Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it
already exists.
```text
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
error changelogs/unreleased/feature-hey-dz.yml already exists! Use `--force` to overwrite.
$ bin/changelog 'Hey DZ, I added a feature to GitLab!' --force
create changelogs/unreleased/feature-hey-dz.yml
---
title: Hey DZ, I added a feature to GitLab!
merge_request: 1983
author:
```
#### `--merge-request` or `-m`
Use the **`--merge-request`** or **`-m`** argument to provide the
......
......@@ -465,6 +465,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[cURL]: http://curl.haxx.se/ "cURL website"
[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
......
......@@ -196,6 +196,12 @@ It consists of two subtasks:
As long as the fixtures don't change, `rake teaspoon:tests` is sufficient
(and saves you some time).
If you need to debug your tests and/or application code while they're
running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
in your browser, open DevTools, and run tests for individual files by clicking
on them. This is also much faster than setting up and running tests from the
command line.
Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake teaspoon:fixtures`.
......
......@@ -42,14 +42,6 @@ To run several tests inside one directory:
If you want to use [Spring](https://github.com/rails/spring) set
`ENABLE_SPRING=1` in your environment.
## Generate searchable docs for source code
You can find results under the `doc/code` directory.
```
bundle exec rake gitlab:generate_docs
```
## Generate API documentation for project services (e.g. Slack)
```
......
......@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-13-stable gitlab
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-14-stable gitlab
**Note:** You can change `8-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
**Note:** You can change `8-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
......@@ -479,10 +479,14 @@ Copy the example site config:
sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab
sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab
Make sure to edit the config file to match your setup:
Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the 'git' user:
# Change YOUR_SERVER_FQDN to the fully-qualified
# domain name of your host serving GitLab.
#
# Remember to match your paths to GitLab, especially
# if installing for a user other than 'git'.
#
# If using Ubuntu default nginx install:
# either remove the default_server from the listen line
# or else sudo rm -f /etc/nginx/sites-enabled/default
......
......@@ -2,7 +2,7 @@
GitLab University is the best place to learn about **Version Control with Git and GitLab**.
It doesn't replace, but accompanies our great [Documentation](http://docs.gitlab.com)
It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
and [Blog Articles](https://about.gitlab.com/blog/).
Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
......@@ -31,7 +31,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
1. [GitLab Basics - Article](http://doc.gitlab.com/ce/gitlab-basics/README.html)
1. [GitLab Basics - Article](../gitlab-basics/README.md)
1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
......@@ -51,10 +51,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 1.5. Migrating from other Source Control
1. [Migrating from BitBucket/Stash](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
1. [Migrating from GitHub](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
1. [Migrating from SVN](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
1. [Migrating from Fogbugz](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
1. [Migrating from GitHub](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
1. [Migrating from SVN](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
#### 1.6. GitLab Inc.
......@@ -91,11 +91,11 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
1. [GitLab Pages Documentation](http://doc.gitlab.com/ee/pages/README.html)
1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html)
#### 2.2. GitLab Issues
1. [Markdown in GitLab](http://doc.gitlab.com/ce/markdown/markdown.html)
1. [Markdown in GitLab](../user/markdown.md)
1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
......@@ -129,7 +129,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
1. [GitLab Flow Overview](https://about.gitlab.com/2014/09/29/gitlab-flow/)
1. [Always Start with an Issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)
1. [GitLab Flow Documentation](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
1. [GitLab Flow Documentation](https://docs.gitlab.com/ee/workflow/gitlab_flow.html)
#### 2.5. GitLab Comparisons
......@@ -189,8 +189,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.9. <a name="integrations"></a> Integrations
1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
1. [How to Integrate Jira with GitLab](http://doc.gitlab.com/ee/integration/jira.html)
1. [How to Integrate Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html)
1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html)
1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html)
1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
......
......@@ -10,7 +10,7 @@ User authentication by combination of 2 different steps during login. This allow
### Access Levels
Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](http://doc.gitlab.com/ce/permissions/permissions.html)
Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](../../permissions/permissions.md
### Active Directory (AD)
......
......@@ -58,28 +58,28 @@ Sometimes we need to upgrade customers from old versions of GitLab to latest, so
- Users
- Groups
- Projects
- [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
- [Backup using our Backup rake task](https://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
- [Upgrade to 5.0 source using our Upgrade documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/4.2-to-5.0.md)
- [Upgrade to 5.1 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.0-to-5.1.md)
- [Upgrade to 6.0 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.1-to-6.0.md)
- [Upgrade to 7.14 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/6.x-or-7.x-to-7.14.md)
- [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
- [Perform the MySQL to PostgreSQL migration to convert your backup](http://docs.gitlab.com/ce/update/mysql_to_postgresql.html#converting-a-gitlab-backup-file-from-mysql-to-postgres)
- [Upgrade to Omnibus 7.14](http://doc.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
- [Restore backup using our Restore rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-a-previously-created-backup)
- [Backup using our Backup rake task](https://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
- [Perform the MySQL to PostgreSQL migration to convert your backup](https://docs.gitlab.com/ce/update/mysql_to_postgresql.html#converting-a-gitlab-backup-file-from-mysql-to-postgres)
- [Upgrade to Omnibus 7.14](https://docs.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
- [Restore backup using our Restore rake task](https://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-a-previously-created-backup)
- [Upgrade to latest EE](https://about.gitlab.com/downloads-ee)
- (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support
- Perform a downgrade from [EE to CE](http://doc.gitlab.com/ee/downgrade_ee_to_ce/README.html)
- Perform a downgrade from [EE to CE](https://docs.gitlab.com/ee/downgrade_ee_to_ce/README.html)
#### Start to learn about some of the integrations that we support
Our integrations add great value to GitLab. User questions often relate to integrating GitLab with existing external services and the configuration involved
- Learn about our Integrations (specially, not only):
- [LDAP](http://doc.gitlab.com/ee/integration/ldap.html)
- [JIRA](http://doc.gitlab.com/ee/project_services/jira.html)
- [Jenkins](http://doc.gitlab.com/ee/integration/jenkins.html)
- [SAML](http://doc.gitlab.com/ce/integration/saml.html)
- [LDAP](https://docs.gitlab.com/ee/integration/ldap.html)
- [JIRA](https://docs.gitlab.com/ee/project_services/jira.html)
- [Jenkins](https://docs.gitlab.com/ee/integration/jenkins.html)
- [SAML](https://docs.gitlab.com/ce/integration/saml.html)
#### Goals
......@@ -91,8 +91,8 @@ Our integrations add great value to GitLab. User questions often relate to integ
#### Understand the gathering of diagnostics for GitLab instances
- Learn about the GitLab checks that are available
- [Environment Information and maintenance checks](http://docs.gitlab.com/ce/raketasks/maintenance.html)
- [GitLab check](http://docs.gitlab.com/ce/raketasks/check.html)
- [Environment Information and maintenance checks](https://docs.gitlab.com/ce/raketasks/maintenance.html)
- [GitLab check](https://docs.gitlab.com/ce/raketasks/check.html)
- Omnibus commands
- [Status](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#get-service-status)
- [Starting and stopping services](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#starting-and-stopping)
......@@ -167,12 +167,12 @@ Some tickets need specific knowledge or a deep understanding of a particular com
Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
- Set up and try [Git Annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
- Set up and try [Git LFS](http://doc.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
- Get to know the [GitLab API](http://doc.gitlab.com/ee/api/README.html), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
- Set up [GitLab CI](http://doc.gitlab.com/ee/ci/quick_start/README.html)
- Create your first [GitLab Page](http://doc.gitlab.com/ee/pages/administration.html)
- Set up and try [Git Annex](https://docs.gitlab.com/ee/workflow/git_annex.html)
- Set up and try [Git LFS](https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
- Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
- Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html)
- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html)
- Get to know the GitLab Codebase by reading through the source code:
- Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
......
# From 8.13 to 8.14
Make sure you view this update guide from the tag (version) of GitLab you would
like to install. In most cases this should be the highest numbered production
tag (without rc in it). You can select the tag in the version dropdown at the
top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the
[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
guide links by version.
### 1. Stop server
sudo service gitlab stop
### 2. Backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
### 3. Update Ruby
We will continue supporting Ruby < 2.3 for the time being but we recommend you
upgrade to Ruby 2.3 if you're running a source installation, as this is the same
version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
cd ruby-2.3.1
./configure --disable-install-rdoc
make
sudo make install
```
Install Bundler:
```bash
sudo gem install bundler --no-ri --no-rdoc
```
### 4. Get latest code
```bash
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
For GitLab Community Edition:
```bash
sudo -u git -H git checkout 8-14-stable
```
OR
For GitLab Enterprise Edition:
```bash
sudo -u git -H git checkout 8-14-stable-ee
```
### 5. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.0.0
```
### 6. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
sudo -u git -H git checkout v0.8.5
sudo -u git -H make
```
### 7. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 8. Update configuration files
#### New configuration options for `gitlab.yml`
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
git diff origin/8-13-stable:config/gitlab.yml.example origin/8-14-stable:config/gitlab.yml.example
```
#### Git configuration
Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`.
```sh
sudo -u git -H git config --global repack.writeBitmaps true
```
#### Nginx configuration
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
# For HTTPS configurations
git diff origin/8-13-stable:lib/support/nginx/gitlab-ssl origin/8-14-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-14-stable:lib/support/nginx/gitlab
```
If you are using Apache instead of NGINX please see the updated [Apache templates].
Also note that because Apache does not support upstreams behind Unix sockets you
will need to let gitlab-workhorse listen on a TCP port. You can do this
via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/lib/support/init.d/gitlab.default.example#L38
#### SMTP configuration
If you're installing from source and use SMTP to deliver mail, you will need to add the following line
to config/initializers/smtp_settings.rb:
```ruby
ActionMailer::Base.delivery_method = :smtp
```
See [smtp_settings.rb.sample] as an example.
[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
### 9. Start application
sudo service gitlab start
sudo service nginx restart
### 10. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
To make sure you didn't miss anything run a more thorough check:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.13)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 8.12 to 8.13](8.12-to-8.13.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
......@@ -4,7 +4,7 @@ class Spinach::Features::AdminLogs < Spinach::FeatureSteps
include SharedAdmin
step 'I should see tabs with available logs' do
expect(page).to have_content 'production.log'
expect(page).to have_content 'test.log'
expect(page).to have_content 'githost.log'
expect(page).to have_content 'application.log'
end
......
......@@ -43,14 +43,13 @@ module API
end
class Hook < Grape::Entity
expose :id, :url, :created_at
expose :id, :url, :created_at, :push_events, :tag_push_events
expose :enable_ssl_verification
end
class ProjectHook < Hook
expose :project_id, :push_events
expose :issues_events, :merge_requests_events, :tag_push_events
expose :project_id, :issues_events, :merge_requests_events
expose :note_events, :build_events, :pipeline_events, :wiki_page_events
expose :enable_ssl_verification
end
class BasicProjectDetails < Grape::Entity
......@@ -510,6 +509,7 @@ module API
expose :after_sign_out_path
expose :container_registry_token_expire_delay
expose :repository_storage
expose :repository_storages
expose :koding_enabled
expose :koding_url
end
......
......@@ -8,11 +8,14 @@ module API
#
# Parameters:
# skip_groups (optional) - Array of group ids to exclude from list
# all_available (optional, boolean) - Show all group that you have access to
# Example Request:
# GET /groups
get do
@groups = if current_user.admin
Group.all
elsif params[:all_available]
GroupsFinder.new.execute(current_user)
else
current_user.groups
end
......
......@@ -17,12 +17,12 @@ module API
present current_settings, with: Entities::ApplicationSetting
end
# Modify applicaiton settings
# Modify application settings
#
# Example Request:
# PUT /application/settings
put "application/settings" do
attributes = current_settings.attributes.keys - ["id"]
attributes = ["repository_storage"] + current_settings.attributes.keys - ["id"]
attrs = attributes_for_keys(attributes)
if current_settings.update_attributes(attrs)
......
......@@ -12,6 +12,7 @@ module API
end
get do
hooks = SystemHook.all
present hooks, with: Entities::Hook
end
......@@ -19,10 +20,14 @@ module API
success Entities::Hook
end
params do
requires :url, type: String, desc: 'The URL for the system hook'
requires :url, type: String, desc: "The URL to send the request to"
optional :token, type: String, desc: 'The token used to validate payloads'
optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
end
post do
hook = SystemHook.new declared(params).to_h
hook = SystemHook.new declared(params, include_missing: false).to_h
if hook.save
present hook, with: Entities::Hook
......
module Gitlab
class ProductionLogger < Gitlab::Logger
class EnvironmentLogger < Gitlab::Logger
def self.file_name_noext
'production'
Rails.env
end
end
end
......@@ -3,10 +3,25 @@ module Gitlab
class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
def self.clean!(relation_hash:)
relation_hash.reject! do |key, _value|
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end
def self.clean(*args)
new(*args).clean
end
def initialize(relation_hash:, relation_class:)
@relation_hash = relation_hash
@relation_class = relation_class
end
def clean
@relation_hash.reject do |key, _value|
prohibited_key?(key) || !@relation_class.attribute_method?(key)
end.except('id')
end
private
def prohibited_key?(key)
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end
end
end
......
......@@ -43,6 +43,14 @@ module Gitlab
raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result
remove_symlinks!
end
def remove_symlinks!
Dir["#{@shared.export_path}/**/*"].each do |path|
FileUtils.rm(path) if File.lstat(path).symlink?
end
true
end
end
......
......@@ -55,7 +55,12 @@ module Gitlab
end
def member_hash(member)
member.except('id').merge(source_id: @project.id, importing: true)
parsed_hash(member).merge('source_id' => @project.id, 'importing' => true)
end
def parsed_hash(member)
Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys,
relation_class: ProjectMember)
end
def find_project_user_query(member)
......
......@@ -9,8 +9,14 @@ module Gitlab
end
def restore
json = IO.read(@path)
@tree_hash = ActiveSupport::JSON.decode(json)
begin
json = IO.read(@path)
@tree_hash = ActiveSupport::JSON.decode(json)
rescue => e
Rails.logger.error("Import/Export error: #{e.message}")
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
end
@project_members = @tree_hash.delete('project_members')
ActiveRecord::Base.no_touching do
......
......@@ -14,7 +14,7 @@ module Gitlab
priorities: :label_priorities,
label: :project_label }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
......@@ -30,7 +30,7 @@ module Gitlab
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
@relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
@relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper
@user = user
@imported_object_retries = 0
......@@ -172,11 +172,8 @@ module Gitlab
end
def parsed_relation_hash
@parsed_relation_hash ||= begin
Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
@relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end
@parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash,
relation_class: relation_class)
end
def set_st_diffs
......
......@@ -24,12 +24,19 @@ module Gitlab
end
def verify_version!(version)
if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
if different_version?(version)
raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
else
true
end
end
def different_version?(version)
Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
rescue => e
Rails.logger.error("Import/Export error: #{e.message}")
raise Gitlab::ImportExport::Error.new('Incorrect VERSION format')
end
end
end
end
namespace :gitlab do
desc "GitLab | Generate sdocs for project"
task generate_docs: :environment do
system(*%W(bundle exec sdoc -o doc/code app lib))
end
end
......@@ -10,42 +10,38 @@ describe 'bin/changelog' do
expect(options.amend).to eq true
end
it 'parses --merge-request' do
options = described_class.parse(%w[foo --merge-request 1234 bar])
it 'parses --force and -f' do
%w[--force -f].each do |flag|
options = described_class.parse(%W[foo #{flag} bar])
expect(options.merge_request).to eq 1234
expect(options.force).to eq true
end
end
it 'parses -m' do
options = described_class.parse(%w[foo -m 4321 bar])
it 'parses --merge-request and -m' do
%w[--merge-request -m].each do |flag|
options = described_class.parse(%W[foo #{flag} 1234 bar])
expect(options.merge_request).to eq 4321
expect(options.merge_request).to eq 1234
end
end
it 'parses --dry-run' do
options = described_class.parse(%w[foo --dry-run bar])
it 'parses --dry-run and -n' do
%w[--dry-run -n].each do |flag|
options = described_class.parse(%W[foo #{flag} bar])
expect(options.dry_run).to eq true
expect(options.dry_run).to eq true
end
end
it 'parses -n' do
options = described_class.parse(%w[foo -n bar])
expect(options.dry_run).to eq true
end
it 'parses --git-username' do
it 'parses --git-username and -u' do
allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
options = described_class.parse(%w[foo --git-username bar])
expect(options.author).to eq 'Jane Doe'
end
it 'parses -u' do
allow(described_class).to receive(:git_user_name).and_return('John Smith')
options = described_class.parse(%w[foo -u bar])
%w[--git-username -u].each do |flag|
options = described_class.parse(%W[foo #{flag} bar])
expect(options.author).to eq 'John Smith'
expect(options.author).to eq 'Jane Doe'
end
end
it 'parses -h' do
......
......@@ -7,15 +7,16 @@ describe "Admin Runners" do
describe "Runners page" do
before do
runner = FactoryGirl.create(:ci_runner)
runner = FactoryGirl.create(:ci_runner, contacted_at: Time.now)
pipeline = FactoryGirl.create(:ci_pipeline)
FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
visit admin_runners_path
end
it { page.has_text? "Manage Runners" }
it { page.has_text? "To register a new runner" }
it { page.has_text? "Runners with last contact less than a minute ago: 1" }
it 'has all necessary texts' do
expect(page).to have_text "To register a new Runner"
expect(page).to have_text "Runners with last contact less than a minute ago: 1"
end
describe 'search' do
before do
......@@ -27,8 +28,10 @@ describe "Admin Runners" do
search_form.click_button 'Search'
end
it { expect(page).to have_content("runner-foo") }
it { expect(page).not_to have_content("runner-bar") }
it 'shows correct runner' do
expect(page).to have_content("runner-foo")
expect(page).not_to have_content("runner-bar")
end
end
end
......@@ -46,8 +49,10 @@ describe "Admin Runners" do
end
describe 'projects' do
it { expect(page).to have_content(@project1.name_with_namespace) }
it { expect(page).to have_content(@project2.name_with_namespace) }
it 'contains project names' do
expect(page).to have_content(@project1.name_with_namespace)
expect(page).to have_content(@project2.name_with_namespace)
end
end
describe 'search' do
......@@ -57,8 +62,10 @@ describe "Admin Runners" do
search_form.click_button 'Search'
end
it { expect(page).to have_content(@project1.name_with_namespace) }
it { expect(page).not_to have_content(@project2.name_with_namespace) }
it 'contains name of correct project' do
expect(page).to have_content(@project1.name_with_namespace)
expect(page).not_to have_content(@project2.name_with_namespace)
end
end
describe 'enable/create' do
......
......@@ -19,6 +19,17 @@ describe "Dashboard Issues Feed", feature: true do
expect(body).to have_selector('title', text: "#{user.name} issues")
end
it "renders atom feed with url parameters" do
visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI::parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
context "issue with basic fields" do
let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
......
......@@ -3,10 +3,14 @@ require 'spec_helper'
describe 'Issues Feed', feature: true do
describe 'GET /issues' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project) }
let!(:issue) { create(:issue, author: user, project: project) }
before { project.team << [user, :developer] }
before do
project.team << [user, :developer]
group.add_developer(user)
end
context 'when authenticated' do
it 'renders atom feed' do
......@@ -33,5 +37,28 @@ describe 'Issues Feed', feature: true do
expect(body).to have_selector('entry summary', text: issue.title)
end
end
it "renders atom feed with url parameters for project issues" do
visit namespace_project_issues_path(project.namespace, project,
:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI::parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
it "renders atom feed with url parameters for group issues" do
visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id)
link = find('link[type="application/atom+xml"]')
params = CGI::parse(URI.parse(link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('state' => ['opened'])
expect(params).to include('assignee_id' => [user.id.to_s])
end
end
end
......@@ -64,9 +64,11 @@ describe 'Commits' do
visit ci_status_path(pipeline)
end
it { expect(page).to have_content pipeline.sha[0..7] }
it { expect(page).to have_content pipeline.git_commit_message }
it { expect(page).to have_content pipeline.git_author_name }
it 'shows pipeline`s data' do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.git_author_name
end
end
context 'Download artifacts' do
......
......@@ -44,6 +44,22 @@ describe "Dashboard Issues filtering", feature: true, js: true do
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
it 'updates atom feed link' do
visit_issues(milestone_title: '', assignee_id: user.id)
link = find('.nav-controls a', text: 'Subscribe')
params = CGI::parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('milestone_title' => [''])
expect(params).to include('assignee_id' => [user.id.to_s])
expect(auto_discovery_params).to include('private_token' => [user.private_token])
expect(auto_discovery_params).to include('milestone_title' => [''])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
end
def show_milestone_dropdown
......@@ -51,7 +67,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
expect(page).to have_selector('.dropdown-content', visible: true)
end
def visit_issues
visit issues_dashboard_path
def visit_issues(*args)
visit issues_dashboard_path(*args)
end
end
......@@ -4,6 +4,7 @@ describe 'Filter issues', feature: true do
include WaitForAjax
let!(:project) { create(:project) }
let!(:group) { create(:group) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
......@@ -11,6 +12,7 @@ describe 'Filter issues', feature: true do
before do
project.team << [user, :master]
group.add_developer(user)
login_as(user)
create(:issue, project: project)
end
......@@ -347,4 +349,36 @@ describe 'Filter issues', feature: true do
end
end
end
it 'updates atom feed link for project issues' do
visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id)
link = find('.nav-controls a', text: 'Subscribe')
params = CGI::parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('milestone_title' => [''])
expect(params).to include('assignee_id' => [user.id.to_s])
expect(auto_discovery_params).to include('private_token' => [user.private_token])
expect(auto_discovery_params).to include('milestone_title' => [''])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
it 'updates atom feed link for group issues' do
visit issues_group_path(group, milestone_title: '', assignee_id: user.id)
link = find('.nav-controls a', text: 'Subscribe')
params = CGI::parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
expect(params).to include('private_token' => [user.private_token])
expect(params).to include('milestone_title' => [''])
expect(params).to include('assignee_id' => [user.id.to_s])
expect(auto_discovery_params).to include('private_token' => [user.private_token])
expect(auto_discovery_params).to include('milestone_title' => [''])
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
end
......@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Edit Merge Request', feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
before do
project.team << [user, :master]
......@@ -28,5 +28,17 @@ feature 'Edit Merge Request', feature: true do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
it 'allows to unselect "Remove source branch"' do
merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
uncheck 'Remove source branch when merge request is accepted'
click_button 'Save changes'
expect(page).to have_content 'Remove source branch'
end
end
end
......@@ -79,12 +79,14 @@ describe "Builds" do
click_link "Cancel running"
end
it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
it { expect(page).to have_content 'canceled' }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
it { expect(page).not_to have_link 'Cancel running' }
it 'shows all necessary content' do
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content 'canceled'
expect(page).to have_content @build.short_sha
expect(page).to have_content @build.ref
expect(page).to have_content @build.name
expect(page).not_to have_link 'Cancel running'
end
end
describe "GET /:project/builds/:id" do
......@@ -93,10 +95,12 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
it { expect(page.status_code).to eq(200) }
it { expect(page).to have_content @commit.sha[0..7] }
it { expect(page).to have_content @commit.git_commit_message }
it { expect(page).to have_content @commit.git_author_name }
it 'shows commit`s data' do
expect(page.status_code).to eq(200)
expect(page).to have_content @commit.sha[0..7]
expect(page).to have_content @commit.git_commit_message
expect(page).to have_content @commit.git_author_name
end
end
context "Build from other project" do
......@@ -167,7 +171,7 @@ describe "Builds" do
describe 'Variables' do
before do
@trigger_request = create :ci_trigger_request_with_variables
@trigger_request = create :ci_trigger_request_with_variables
@build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request
visit namespace_project_build_path(@project.namespace, @project, @build)
end
......@@ -176,14 +180,14 @@ describe "Builds" do
expect(page).to have_css('.reveal-variables')
expect(page).not_to have_css('.js-build-variable')
expect(page).not_to have_css('.js-build-value')
click_button 'Reveal Variables'
expect(page).not_to have_css('.reveal-variables')
expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end
end
end
end
describe "POST /:project/builds/:id/cancel" do
......@@ -194,9 +198,11 @@ describe "Builds" do
click_link "Cancel"
end
it { expect(page.status_code).to eq(200) }
it { expect(page).to have_content 'canceled' }
it { expect(page).to have_content 'Retry' }
it 'loads the page and shows all needed controls' do
expect(page.status_code).to eq(200)
expect(page).to have_content 'canceled'
expect(page).to have_content 'Retry'
end
end
context "Build from other project" do
......
......@@ -183,4 +183,19 @@ describe 'Edit Project Settings', feature: true do
end
end
end
# Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/24056
describe 'project statistic visibility' do
let!(:project) { create(:project, :private) }
before do
project.team << [member, :guest]
login_as(member)
visit namespace_project_path(project.namespace, project)
end
it "does not show project statistic for guest" do
expect(page).not_to have_selector('.project-stats')
end
end
end
%form.show-gl-field-errors{action: 'submit', method: 'post'}
%form.gl-show-field-errors{action: 'submit', method: 'post'}
.form-group
%input.required-text{required: true, type: 'text'} Text
.form-group
......@@ -10,6 +10,6 @@
.form-group
%input.hidden{ type:'hidden' }
.form-group
%input.custom.no-gl-field-errors{ type:'text' } Custom, do not validate
%input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate
.form-group
%input.submit{type: 'submit'} Submit
......@@ -8,7 +8,7 @@
describe('GL Style Field Errors', function() {
beforeEach(function() {
fixture.load('gl_field_errors.html');
const $form = this.$form = $('form.show-gl-field-errors');
const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new global.GlFieldErrors($form);
});
......@@ -21,7 +21,7 @@
});
it('should ignore elements with custom error handling', function() {
const customErrorFlag = 'no-gl-field-errors';
const customErrorFlag = 'gl-field-error-ignore';
const customErrorElem = $(`.${customErrorFlag}`);
expect(customErrorElem.length).toBe(1);
......
/* eslint-disable */
/*= require merge_request_widget */
/*= require lib/utils/jquery.timeago.js */
/*= require jquery.timeago.js */
(function() {
describe('MergeRequestWidget', function() {
......
require 'spec_helper'
describe Gitlab::ImportExport::AttributeCleaner, lib: true do
let(:relation_class){ double('relation_class').as_null_object }
let(:unsafe_hash) do
{
'id' => 101,
'service_id' => 99,
'moved_to_id' => 99,
'namespace_id' => 99,
......@@ -27,8 +29,9 @@ describe Gitlab::ImportExport::AttributeCleaner, lib: true do
end
it 'removes unwanted attributes from the hash' do
described_class.clean!(relation_hash: unsafe_hash)
# allow(relation_class).to receive(:attribute_method?).and_return(true)
parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class)
expect(unsafe_hash).to eq(post_safe_hash)
expect(parsed_hash).to eq(post_safe_hash)
end
end
require 'spec_helper'
describe Gitlab::ImportExport::FileImporter, lib: true do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" }
let(:valid_file) { "#{shared.export_path}/valid.json" }
let(:symlink_file) { "#{shared.export_path}/invalid.json" }
let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" }
before do
stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0)
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true)
setup_files
described_class.import(archive_file: '', shared: shared)
end
after do
FileUtils.rm_rf(export_path)
end
it 'removes symlinks in root folder' do
expect(File.exist?(symlink_file)).to be false
end
it 'removes symlinks in subfolders' do
expect(File.exist?(subfolder_symlink_file)).to be false
end
it 'does not remove a valid file' do
expect(File.exist?(valid_file)).to be true
end
def setup_files
FileUtils.mkdir_p("#{shared.export_path}/subfolder/")
FileUtils.touch(valid_file)
FileUtils.ln_s(valid_file, symlink_file)
FileUtils.ln_s(valid_file, subfolder_symlink_file)
end
end
require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
describe 'restore project tree' do
......@@ -175,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
end
end
context 'project.json file access check' do
it 'does not read a symlink' do
Dir.mktmpdir do |tmpdir|
setup_symlink(tmpdir, 'project.json')
allow(shared).to receive(:export_path).and_call_original
restored_project_json
expect(shared.errors.first).not_to include('test')
end
end
end
end
end
end
require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::VersionChecker, services: true do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
describe 'bundle a project Git repo' do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
let(:version) { Gitlab::ImportExport.version }
before do
......@@ -27,4 +29,16 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
end
end
end
describe 'version file access check' do
it 'does not read a symlink' do
Dir.mktmpdir do |tmpdir|
setup_symlink(tmpdir, 'VERSION')
described_class.check!(shared: shared)
expect(shared.errors.first).not_to include('test')
end
end
end
end
......@@ -41,14 +41,62 @@ describe ApplicationSetting, models: true do
subject { setting }
end
context 'repository storages inclussion' do
# Upgraded databases will have this sort of content
context 'repository_storages is a String, not an Array' do
before { setting.__send__(:raw_write_attribute, :repository_storages, 'default') }
it { expect(setting.repository_storages_before_type_cast).to eq('default') }
it { expect(setting.repository_storages).to eq(['default']) }
end
context 'repository storages' do
before do
storages = { 'custom' => 'tmp/tests/custom_repositories' }
storages = {
'custom1' => 'tmp/tests/custom_repositories_1',
'custom2' => 'tmp/tests/custom_repositories_2',
'custom3' => 'tmp/tests/custom_repositories_3',
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
it { is_expected.to allow_value('custom').for(:repository_storage) }
it { is_expected.not_to allow_value('alternative').for(:repository_storage) }
describe 'inclusion' do
it { is_expected.to allow_value('custom1').for(:repository_storages) }
it { is_expected.to allow_value(['custom2', 'custom3']).for(:repository_storages) }
it { is_expected.not_to allow_value('alternative').for(:repository_storages) }
it { is_expected.not_to allow_value(['alternative', 'custom1']).for(:repository_storages) }
end
describe 'presence' do
it { is_expected.not_to allow_value([]).for(:repository_storages) }
it { is_expected.not_to allow_value("").for(:repository_storages) }
it { is_expected.not_to allow_value(nil).for(:repository_storages) }
end
describe '.pick_repository_storage' do
it 'uses Array#sample to pick a random storage' do
array = double('array', sample: 'random')
expect(setting).to receive(:repository_storages).and_return(array)
expect(setting.pick_repository_storage).to eq('random')
end
describe '#repository_storage' do
it 'returns the first storage' do
setting.repository_storages = ['good', 'bad']
expect(setting.repository_storage).to eq('good')
end
end
describe '#repository_storage=' do
it 'overwrites repository_storages' do
setting.repository_storage = 'overwritten'
expect(setting.repository_storages).to eq(['overwritten'])
end
end
end
end
end
......
require 'spec_helper'
include Gitlab::Routing.url_helpers
describe JiraService, models: true do
include Gitlab::Routing.url_helpers
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
......@@ -79,7 +80,9 @@ describe JiraService, models: true do
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
Project.default_url_options[:script_name] = "/gitlab"
allow(JiraService).to receive(:default_url_options) do
{ script_name: '/gitlab' }
end
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
......
......@@ -837,16 +837,19 @@ describe Project, models: true do
context 'repository storage by default' do
let(:project) { create(:empty_project) }
subject { project.repository_storage }
before do
storages = { 'alternative_storage' => '/some/path' }
storages = {
'default' => 'tmp/tests/repositories',
'picked' => 'tmp/tests/repositories',
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
stub_application_setting(repository_storage: 'alternative_storage')
allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(true)
end
it { is_expected.to eq('alternative_storage') }
it 'picks storage from ApplicationSetting' do
expect_any_instance_of(ApplicationSetting).to receive(:pick_repository_storage).and_return('picked')
expect(project.repository_storage).to eq('picked')
end
end
context 'shared runners by default' do
......
......@@ -37,7 +37,7 @@ describe API::API, api: true do
end
end
context "when authenticated as admin" do
context "when authenticated as admin" do
it "admin: returns an array of all groups" do
get api("/groups", admin)
expect(response).to have_http_status(200)
......@@ -55,6 +55,17 @@ describe API::API, api: true do
expect(json_response.length).to eq(1)
end
end
context "when using all_available in request" do
it "returns all groups you have access to" do
public_group = create :group, :public
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(public_group.name)
end
end
end
describe "GET /groups/:id" do
......
......@@ -14,14 +14,14 @@ describe API::API, 'MergeRequestDiffs', api: true do
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
context 'valid merge request' do
before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) }
let(:merge_request_diff) { merge_request.merge_request_diffs.first }
it { expect(response.status).to eq 200 }
it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) }
it { expect(json_response.first['id']).to eq(merge_request_diff.id) }
it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
it 'returns 200 for a valid merge request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
merge_request_diff = merge_request.merge_request_diffs.first
expect(response.status).to eq 200
expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
expect(json_response.first['id']).to eq(merge_request_diff.id)
expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
end
it 'returns a 404 when merge_request_id not found' do
......@@ -31,14 +31,14 @@ describe API::API, 'MergeRequestDiffs', api: true do
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
context 'valid merge request' do
before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) }
let(:merge_request_diff) { merge_request.merge_request_diffs.first }
it { expect(response.status).to eq 200 }
it { expect(json_response['id']).to eq(merge_request_diff.id) }
it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) }
it 'returns a 200 for a valid merge request' do
merge_request_diff = merge_request.merge_request_diffs.first
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
expect(response.status).to eq 200
expect(json_response['id']).to eq(merge_request_diff.id)
expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha)
expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size)
end
it 'returns a 404 when merge_request_id not found' do
......
......@@ -186,14 +186,14 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
context 'valid merge request' do
before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) }
let(:commit) { merge_request.commits.first }
it { expect(response.status).to eq 200 }
it { expect(json_response.size).to eq(merge_request.commits.size) }
it { expect(json_response.first['id']).to eq(commit.id) }
it { expect(json_response.first['title']).to eq(commit.title) }
it 'returns a 200 when merge request is valid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
commit = merge_request.commits.first
expect(response.status).to eq 200
expect(json_response.size).to eq(merge_request.commits.size)
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['title']).to eq(commit.title)
end
it 'returns a 404 when merge_request_id not found' do
......
......@@ -33,6 +33,7 @@ describe API::API, 'Settings', api: true do
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
expect(json_response['repository_storage']).to eq('custom')
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['koding_enabled']).to be_truthy
expect(json_response['koding_url']).to eq('http://koding.example.com')
end
......
......@@ -13,6 +13,7 @@ describe API::API, api: true do
context "when no user" do
it "returns authentication error" do
get api("/hooks")
expect(response).to have_http_status(401)
end
end
......@@ -20,6 +21,7 @@ describe API::API, api: true do
context "when not an admin" do
it "returns forbidden error" do
get api("/hooks", user)
expect(response).to have_http_status(403)
end
end
......@@ -27,9 +29,12 @@ describe API::API, api: true do
context "when authenticated as admin" do
it "returns an array of hooks" do
get api("/hooks", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
expect(json_response.first['push_events']).to be true
expect(json_response.first['tag_push_events']).to be false
end
end
end
......@@ -43,6 +48,7 @@ describe API::API, api: true do
it "responds with 400 if url not given" do
post api("/hooks", admin)
expect(response).to have_http_status(400)
end
......@@ -51,6 +57,14 @@ describe API::API, api: true do
post api("/hooks", admin)
end.not_to change { SystemHook.count }
end
it 'sets default values for events' do
post api('/hooks', admin), url: 'http://mep.mep', enable_ssl_verification: true
expect(response).to have_http_status(201)
expect(json_response['enable_ssl_verification']).to be true
expect(json_response['tag_push_events']).to be false
end
end
describe "GET /hooks/:id" do
......
......@@ -220,26 +220,33 @@ describe Ci::API::API do
end
context 'when request is valid' do
it { expect(response.status).to eq 202 }
it 'gets correct response' do
expect(response.status).to eq 202
expect(response.header).to have_key 'Range'
expect(response.header).to have_key 'Build-Status'
end
it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
it { expect(response.header).to have_key 'Range' }
it { expect(response.header).to have_key 'Build-Status' }
end
context 'when content-range start is too big' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
it { expect(response.status).to eq 416 }
it { expect(response.header).to have_key 'Range' }
it { expect(response.header['Range']).to eq '0-11' }
it 'gets correct response' do
expect(response.status).to eq 416
expect(response.header).to have_key 'Range'
expect(response.header['Range']).to eq '0-11'
end
end
context 'when content-range start is too small' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
it { expect(response.status).to eq 416 }
it { expect(response.header).to have_key 'Range' }
it { expect(response.header['Range']).to eq '0-11' }
it 'gets correct response' do
expect(response.status).to eq 416
expect(response.header).to have_key 'Range'
expect(response.header['Range']).to eq '0-11'
end
end
context 'when Content-Range header is missing' do
......
......@@ -109,10 +109,12 @@ describe Ci::API::API do
end
describe "DELETE /runners/delete" do
let!(:runner) { FactoryGirl.create(:ci_runner) }
before { delete ci_api("/runners/delete"), token: runner.token }
it 'returns 200' do
runner = FactoryGirl.create(:ci_runner)
delete ci_api("/runners/delete"), token: runner.token
it { expect(response).to have_http_status 200 }
it { expect(Ci::Runner.count).to eq(0) }
expect(response).to have_http_status 200
expect(Ci::Runner.count).to eq(0)
end
end
end
module ImportExport
module CommonUtil
def setup_symlink(tmpdir, symlink_name)
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(tmpdir)
File.open("#{tmpdir}/test", 'w') { |file| file.write("test") }
FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}")
end
end
end
......@@ -39,3 +39,6 @@ captures/
# Keystore files
*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
......@@ -7,6 +7,11 @@
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
......@@ -34,3 +39,13 @@
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
modules.order
Module.symvers
Mkfile.old
dkms.conf
.architect
bootstrap.css
bootstrap.js
bootstrap.json
bootstrap.jsonp
build/
classic.json
classic.jsonp
ext/
modern.json
modern.jsonp
resources/sass/.sass-cache/
......@@ -4,9 +4,6 @@
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
......
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
exclusive Copyright and Related Rights (defined below) upon the creator and
subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
Certain owners wish to permanently relinquish those rights to a Work for the
purpose of contributing to a commons of creative, cultural and scientific
works ("Commons") that the public can reliably and without fear of later
claims of infringement build upon, modify, incorporate in other works, reuse
and redistribute as freely as possible in any form whatsoever and for any
purposes, including without limitation commercial purposes. These owners may
contribute to the Commons to promote the ideal of a free culture and the
further production of creative, cultural and scientific works, or to gain
reputation or greater distribution for their Work in part through the use and
efforts of others.
For these and/or other purposes and motivations, and without any expectation
of additional consideration or compensation, the person associating CC0 with a
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
and publicly distribute the Work under its terms, with knowledge of his or her
Copyright and Related Rights in the Work and the meaning and intended legal
effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
Related Rights"). Copyright and Related Rights include, but are not limited
to, the following:
i. the right to reproduce, adapt, distribute, perform, display, communicate,
and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or likeness
depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data in
a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
vii. other similar, equivalent or corresponding rights throughout the world
based on applicable law or treaty, and any national implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
and Related Rights and associated claims and causes of action, whether now
known or unknown (including existing as well as future claims and causes of
action), in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number of
copies, and (iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
the Waiver for the benefit of each member of the public at large and to the
detriment of Affirmer's heirs and successors, fully intending that such Waiver
shall not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of the Work
by the public as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be
judged legally invalid or ineffective under applicable law, then the Waiver
shall be preserved to the maximum extent permitted taking into account
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
is so judged Affirmer hereby grants to each affected person a royalty-free,
non transferable, non sublicensable, non exclusive, irrevocable and
unconditional license to exercise Affirmer's Copyright and Related Rights in
the Work (i) in all territories worldwide, (ii) for the maximum duration
provided by applicable law or treaty (including future time extensions), (iii)
in any current or future medium and for any number of copies, and (iv) for any
purpose whatsoever, including without limitation commercial, advertising or
promotional purposes (the "License"). The License shall be deemed effective as
of the date CC0 was applied by Affirmer to the Work. Should any part of the
License for any reason be judged legally invalid or ineffective under
applicable law, such partial invalidity or ineffectiveness shall not
invalidate the remainder of the License, and in such case Affirmer hereby
affirms that he or she will not (i) exercise any of his or her remaining
Copyright and Related Rights in the Work or (ii) assert any associated claims
and causes of action with respect to the Work, in either case contrary to
Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or warranties
of any kind concerning the Work, express, implied, statutory or otherwise,
including without limitation warranties of title, merchantability, fitness
for a particular purpose, non infringement, or the absence of latent or
other defects, accuracy, or the present or absence of errors, whether or not
discoverable, all to the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without limitation
any person's Copyright and Related Rights in the Work. Further, Affirmer
disclaims responsibility for obtaining any necessary consents, permissions
or other rights required for any use of the Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to this
CC0 or use of the Work.
For more information, please see
<http://creativecommons.org/publicdomain/zero/1.0/>
......@@ -7,6 +7,7 @@ app/storage/
# Laravel 5 & Lumen specific
bootstrap/cache/
public/storage
.env.*.php
.env.php
.env
......
# For projects using nanoc (http://nanoc.ws/)
# For projects using Nanoc (http://nanoc.ws/)
# Default location for output, needs to match output_dir's value found in config.yaml
# Default location for output (needs to match output_dir's value found in nanoc.yaml)
output/
# Temporary file directory
......
......@@ -11,3 +11,10 @@ system/cache/
system/logs/
system/storage/
# vQmod log files
vqmod/logs/*
# vQmod cache files
vqmod/vqcache/*
vqmod/checked.cache
vqmod/mods.cache
......@@ -66,7 +66,7 @@ docs/_build/
# PyBuilder
target/
# IPython Notebook
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
......
......@@ -5,3 +5,6 @@
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
......@@ -61,6 +61,15 @@ acs-*.bib
# fixme
*.lox
# feynmf/feynmp
*.mf
*.mp
*.t[1-9]
*.t[1-9][0-9]
*.tfm
*.[1-9]
*.[1-9][0-9]
#(r)(e)ledmac/(r)(e)ledpar
*.end
*.?end
......
# Visual Studio 2015 user specific files
.vs/
# Visual Studio 2015 database file
*.VC.db
# Compiled Object files
*.slo
*.lo
......
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
*.vcxproj.filters
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
......@@ -44,6 +47,7 @@ dlldata.c
project.lock.json
project.fragment.lock.json
artifacts/
Properties/launchSettings.json
*_i.c
*_p.c
......@@ -238,6 +242,9 @@ FakesAssemblies/
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
......
# Based on openjdk:8, already includes lein
image: clojure:lein-2.7.0
# If you need to configure a database, add a `services` section here
# See https://docs.gitlab.com/ce/ci/services/postgres.html
# Make sure you configure the connection as well
before_script:
# If you need to install any external applications, like a
# postgres client, you may want to uncomment the line below:
#
#- apt-get update -y
#
# Retrieve project dependencies
# Do this on before_script since it'll be shared between both test and
# any production sections a user adds
- lein deps
test:
script:
# If you need to run any migrations or configure the database, this
# would be the point to do it.
- lein test
# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/crystallang/crystal/
image: "crystallang/crystal:latest"
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
# services:
# - mysql:latest
# - redis:latest
# - postgres:latest
# variables:
# POSTGRES_DB: database_name
# Cache shards in between builds
cache:
paths:
- libs
# This is a basic example for a shard or script which doesn't use
# services such as redis or postgres
before_script:
- apt-get update -qq && apt-get install -y -qq libxml2-dev
- crystal -v # Print out Crystal version for debugging
- shards
# If you are using built-in Crystal Spec.
spec:
script:
- crystal spec
# If you are using minitest.cr
minitest:
script:
- crystal test/spec_test.cr # change to the file(s) you execute for tests
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