Commit 6a00cf1b authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into show-status-from-branch

* upstream/master: (74 commits)
  Clarify the author field for the changelog documentation
  Add and update .gitignore & .gitlab-ci.yml templates for 8.14
  Update "Installation from source" guide for 8.14.0
  Add CHANGELOG entries for latest patches
  Merge branch 'fix/import-export-symlink-vulnerability' into 'security'
  Merge branch 'fix/import-projectmember-security' into 'security'
  Use stubs instead of modifying global states
  Add changelog instructions to CHANGELOG.md
  Try not to include anything globally!
  Update help banner for bin/changelog
  Add a `--force` option to bin/changelog
  Update examples in changelog docs to use single quotes around title
  Use the server's base URL without relative URL part when creating links in JIRA
  Update docs and test description
  Update docs and unexpose token
  Make ESLint ignore instrumented files for coverage analysis (!7236)
  Initialize form validation on new group form.
  Check that JavaScript file names match convention (!7238)
  Unchange username_validator.
  Move snake_case to camelCase.
  ...
parents f3c3d8e6 3ac3106e
/coverage-javascript/
/public/ /public/
/tmp/ /tmp/
/vendor/ /vendor/
......
{ {
"extends": "airbnb", "extends": "airbnb",
"plugins": [
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z_]+$"]
},
"globals": { "globals": {
"$": false, "$": false,
"_": false, "_": 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) ## 8.14.0 (2016-11-22)
- 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 - 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) - Adds user project membership expired event to clarify why user was removed (Callum Dryden)
- Trim leading and trailing whitespace on project_path (Linus Thiel) - Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc) - Prevent award emoji via notes for issues/MRs authored by user (barthc)
- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
- Fix Markdown styling inside reference links (Jan Zdráhal) - Fix Markdown styling inside reference links (Jan Zdráhal)
- Fix extra space on Build sidebar on Firefox !7060 - Fix extra space on Build sidebar on Firefox !7060
- Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087 - Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd) - Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Refactor Jira service to use jira-ruby gem - Refactor Jira service to use jira-ruby gem
- Improved todos empty state - Improved todos empty state
- Add hover to trash icon in notes !7008 (blackst0ne) - Add hover to trash icon in notes !7008 (blackst0ne)
- Hides project activity tabs when features are disabled
- Only show one error message for an invalid email !5905 (lycoperdon) - Only show one error message for an invalid email !5905 (lycoperdon)
- Added guide describing how to upgrade PostgreSQL using Slony
- Fix sidekiq stats in admin area (blackst0ne) - Fix sidekiq stats in admin area (blackst0ne)
- Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file - Created cycle analytics bundle JavaScript file
- API: Fix booleans not recognized as such when using the `to_boolean` helper - API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954 - Removed delete branch tooltip !6954
- Stop unauthorized users dragging on milestone page (blackst0ne) - Stop unauthorized users dragging on milestone page (blackst0ne)
- Restore issue boards welcome message when a project is created !6899 - Restore issue boards welcome message when a project is created !6899
- Check that JavaScript file names match convention !7238 (winniehell)
- Do not show tooltip for active element !7105 (winniehell) - Do not show tooltip for active element !7105 (winniehell)
- Escape ref and path for relative links !6050 (winniehell) - Escape ref and path for relative links !6050 (winniehell)
- Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose)
- Fix broken issue/merge request links in JIRA comments. !6143 (Brian Kintz)
- Fix filtering of milestones with quotes in title (airatshigapov) - Fix filtering of milestones with quotes in title (airatshigapov)
- Fix issue boards dragging bug in Safari
- Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison)
- Fix showing pipeline status for a given commit from correct branch !7034 - Fix showing pipeline status for a given commit from correct branch !7034
- Update mail_room and enable sentinel support to Reply By Email (!7101) - Update mail_room and enable sentinel support to Reply By Email (!7101)
...@@ -33,24 +46,28 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -33,24 +46,28 @@ 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 - New issue board list dropdown stays open after adding a new list
- Fix: Backup restore doesn't clear cache - Fix: Backup restore doesn't clear cache
- Optimize Event queries by removing default order - Optimize Event queries by removing default order
- Remove duplicate links from sidebar
- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) - 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
- Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld) - Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld)
- Replace jquery.cookie plugin with js.cookie !7085 - Replace jquery.cookie plugin with js.cookie !7085
- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
- Show full status link on MR & commit pipelines - Show full status link on MR & commit pipelines
- Fix documents and comments on Build API `scope` - Fix documents and comments on Build API `scope`
- Initialize Sidekiq with the list of queues used by GitLab
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
- Shortened merge request modal to let clipboard button not overlap - 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) - 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) - 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) - Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
## 8.13.3 ## 8.13.3 (2016-11-02)
- Fix relative links in Markdown wiki when displayed in "Project" tab !7218 - Removes any symlinks before importing a project export file. CVE-2016-9086
- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project - Fixed Import/Export foreign key issue to do with project members.
- Fix project features default values
## 8.13.2 (2016-10-31) ## 8.13.2 (2016-10-31)
...@@ -240,6 +257,11 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -240,6 +257,11 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix broken Project API docs (Takuya Noguchi) - Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master) - 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 ## 8.12.7
- Prevent running `GfmAutocomplete` setup for each diff note. !6569 - Prevent running `GfmAutocomplete` setup for each diff note. !6569
...@@ -499,6 +521,10 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -499,6 +521,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 - Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs - 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 ## 8.11.9
- Don't send Private-Token (API authentication) headers to Sentry - Don't send Private-Token (API authentication) headers to Sentry
...@@ -739,6 +765,10 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -739,6 +765,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 - Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done - 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 ## 8.10.12
- Don't send Private-Token (API authentication) headers to Sentry - Don't send Private-Token (API authentication) headers to Sentry
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
- [Technical debt](#technical-debt) - [Technical debt](#technical-debt)
- [Merge requests](#merge-requests) - [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines) - [Merge request guidelines](#merge-request-guidelines)
- [Merge request description format](#merge-request-description-format)
- [Contribution acceptance criteria](#contribution-acceptance-criteria) - [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Changes for Stable Releases](#changes-for-stable-releases) - [Changes for Stable Releases](#changes-for-stable-releases)
- [Definition of done](#definition-of-done) - [Definition of done](#definition-of-done)
...@@ -247,13 +246,7 @@ request is as follows: ...@@ -247,13 +246,7 @@ request is as follows:
1. Fork the project into your personal space on GitLab.com 1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master` 1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG.md](CHANGELOG.md): 1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are fixing a ~regression issue, you can add your entry to the next
patch release (e.g. `8.12.5` if current version is `8.12.4`)
1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
current version is `8.12.4`
1. Please add your entry at a random place among the entries of the targeted
release
1. If you are writing documentation, make sure to follow the 1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide] [documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by 1. If you have multiple commits please combine them into one commit by
...@@ -262,8 +255,11 @@ request is as follows: ...@@ -262,8 +255,11 @@ request is as follows:
1. Submit a merge request (MR) to the `master` branch 1. Submit a merge request (MR) to the `master` branch
1. The MR title should describe the change you want to make 1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it, see the [merge request description format] used to achieve it.
(#merge-request-description-format) 1. If you are contributing code, fill in the template already provided in the
"Description" field.
1. If you are contributing documentation, choose `Documentation` from the
"Choose a template" menu and fill in the template.
1. If the MR changes the UI it should include *Before* and *After* screenshots 1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages, 1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R` `grep css-class ./app -R`
...@@ -469,6 +465,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -469,6 +465,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[contributor-covenant]: http://contributor-covenant.org [contributor-covenant]: http://contributor-covenant.org
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
......
...@@ -117,7 +117,7 @@ gem 'truncato', '~> 0.7.8' ...@@ -117,7 +117,7 @@ gem 'truncato', '~> 0.7.8'
gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2' gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2'
# Diffs # Diffs
gem 'diffy', '~> 3.0.3' gem 'diffy', '~> 3.1.0'
# Application server # Application server
group :unicorn do group :unicorn do
...@@ -196,7 +196,7 @@ gem 'loofah', '~> 2.0.3' ...@@ -196,7 +196,7 @@ gem 'loofah', '~> 2.0.3'
gem 'licensee', '~> 8.0.0' gem 'licensee', '~> 8.0.0'
# Protect against bruteforcing # Protect against bruteforcing
gem 'rack-attack', '~> 4.3.1' gem 'rack-attack', '~> 4.4.1'
# Ace editor # Ace editor
gem 'ace-rails-ap', '~> 4.1.0' gem 'ace-rails-ap', '~> 4.1.0'
......
...@@ -180,7 +180,7 @@ GEM ...@@ -180,7 +180,7 @@ GEM
railties railties
rotp (~> 2.0) rotp (~> 2.0)
diff-lcs (1.2.5) diff-lcs (1.2.5)
diffy (3.0.7) diffy (3.1.0)
docile (1.1.5) docile (1.1.5)
doorkeeper (4.2.0) doorkeeper (4.2.0)
railties (>= 4.2) railties (>= 4.2)
...@@ -520,7 +520,7 @@ GEM ...@@ -520,7 +520,7 @@ GEM
rack (1.6.4) rack (1.6.4)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.3.1) rack-attack (4.4.1)
rack rack
rack-cors (0.4.0) rack-cors (0.4.0)
rack-mount (0.8.3) rack-mount (0.8.3)
...@@ -847,7 +847,7 @@ DEPENDENCIES ...@@ -847,7 +847,7 @@ DEPENDENCIES
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.0.3) diffy (~> 3.1.0)
doorkeeper (~> 4.2.0) doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8) email_reply_parser (~> 0.5.8)
...@@ -929,7 +929,7 @@ DEPENDENCIES ...@@ -929,7 +929,7 @@ DEPENDENCIES
poltergeist (~> 1.9.0) poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0) premailer-rails (~> 1.9.0)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.3.1) rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0) rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rails (= 4.2.7.1) rails (= 4.2.7.1)
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
fallbackClass: 'is-dragging', fallbackClass: 'is-dragging',
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.has-tooltip, .btn', filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 50, delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
......
...@@ -299,7 +299,7 @@ ...@@ -299,7 +299,7 @@
}; };
Dispatcher.prototype.initFieldErrors = function() { Dispatcher.prototype.initFieldErrors = function() {
$('.show-gl-field-errors').each((i, form) => { $('.gl-show-field-errors').each((i, form) => {
new gl.GlFieldErrors(form); new gl.GlFieldErrors(form);
}); });
}; };
......
/* 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 */ /* 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();
}
}
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() { //= require gl_field_error
return this.renderInvalid();
}
renderInvalid() {
this.inputElement.addClass(inputErrorClass);
this.scopedSiblings.hide();
return this.fieldErrorElement.show();
}
renderClear() { ((global) => {
const inputVal = this.accessCurrentValue(); const customValidationFlag = 'gl-field-error-ignore';
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';
class GlFieldErrors { class GlFieldErrors {
constructor(form) { constructor(form) {
...@@ -144,7 +22,7 @@ ...@@ -144,7 +22,7 @@
this.state.inputs = this.form.find(validateSelectors).toArray() this.state.inputs = this.form.find(validateSelectors).toArray()
.filter((input) => !input.classList.contains(customValidationFlag)) .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); this.form.on('submit', this.catchInvalidFormSubmit);
} }
......
...@@ -238,8 +238,11 @@ ...@@ -238,8 +238,11 @@
_this.expandViewContainer(); _this.expandViewContainer();
} }
_this.diffsLoaded = true; _this.diffsLoaded = true;
var anchoredDiff = gl.utils.getLocationHash();
if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
_this.scrollToElement("#diffs"); _this.scrollToElement("#diffs");
_this.highlighSelectedLine(); _this.highlighSelectedLine();
});
_this.filesCommentButton = $('.files .diff-file').filesCommentButton(); _this.filesCommentButton = $('.files .diff-file').filesCommentButton();
return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) { return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
e.preventDefault(); e.preventDefault();
...@@ -252,6 +255,17 @@ ...@@ -252,6 +255,17 @@
}); });
}; };
MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
var diffTitle = $('#file-path-' + anchoredDiff);
var diffFile = diffTitle.closest('.diff-file');
var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
diffFile.singleFileDiff(true, cb);
} else {
cb();
}
};
MergeRequestTabs.prototype.highlighSelectedLine = function() { MergeRequestTabs.prototype.highlighSelectedLine = function() {
var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight; var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
$('.hll').removeClass('hll'); $('.hll').removeClass('hll');
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) { function SingleFileDiff(file, forceLoad, cb) {
this.file = file; this.file = file;
this.toggleDiff = bind(this.toggleDiff, this); this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
...@@ -32,9 +32,12 @@ ...@@ -32,9 +32,12 @@
this.$toggleIcon.addClass('fa-caret-down'); this.$toggleIcon.addClass('fa-caret-down');
} }
$('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
if (forceLoad) {
this.toggleDiff(null, cb);
}
} }
SingleFileDiff.prototype.toggleDiff = function(e) { SingleFileDiff.prototype.toggleDiff = function(e, cb) {
var $target = $(e.target); var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
...@@ -54,11 +57,11 @@ ...@@ -54,11 +57,11 @@
} }
} else { } else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(); return this.getContentHTML(cb);
} }
}; };
SingleFileDiff.prototype.getContentHTML = function() { SingleFileDiff.prototype.getContentHTML = function(cb) {
this.collapsedContent.hide(); this.collapsedContent.hide();
this.loadingContent.show(); this.loadingContent.show();
$.get(this.diffForPath, (function(_this) { $.get(this.diffForPath, (function(_this) {
...@@ -76,6 +79,8 @@ ...@@ -76,6 +79,8 @@
if (typeof DiffNotesApp !== 'undefined') { if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents(); DiffNotesApp.compileComponents();
} }
if (cb) cb();
}; };
})(this)); })(this));
}; };
...@@ -84,10 +89,10 @@ ...@@ -84,10 +89,10 @@
})(); })();
$.fn.singleFileDiff = function() { $.fn.singleFileDiff = function(forceLoad, cb) {
return this.each(function() { return this.each(function() {
if (!$.data(this, 'singleFileDiff')) { if (!$.data(this, 'singleFileDiff') || forceLoad) {
return $.data(this, 'singleFileDiff', new SingleFileDiff(this)); return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
} }
}); });
}; };
......
...@@ -11,11 +11,9 @@ ...@@ -11,11 +11,9 @@
$this.parent().find('.star-count').text(data.star_count); $this.parent().find('.star-count').text(data.star_count);
if (isStarred) { if (isStarred) {
$starSpan.removeClass('starred').text('Star'); $starSpan.removeClass('starred').text('Star');
gl.utils.updateTooltipTitle($this, 'Star project');
$starIcon.removeClass('fa-star').addClass('fa-star-o'); $starIcon.removeClass('fa-star').addClass('fa-star-o');
} else { } else {
$starSpan.addClass('starred').text('Unstar'); $starSpan.addClass('starred').text('Unstar');
gl.utils.updateTooltipTitle($this, 'Unstar project');
$starIcon.removeClass('fa-star-o').addClass('fa-star'); $starIcon.removeClass('fa-star-o').addClass('fa-star');
} }
}; };
......
.avatar { @mixin avatar-size($size, $margin-right) {
width: $size;
height: $size;
margin-right: $margin-right;
}
.avatar-container {
float: left; float: left;
margin-right: 12px; margin-right: 15px;
border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1);
&.s16 { @include avatar-size(16px, 6px); }
&.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); }
&.s26 { @include avatar-size(26px, 8px); }
&.s32 { @include avatar-size(32px, 10px); }
&.s36 { @include avatar-size(36px, 10px); }
&.s40 { @include avatar-size(40px, 10px); }
&.s46 { @include avatar-size(46px, 15px); }
&.s48 { @include avatar-size(48px, 10px); }
&.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
&.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); }
}
.avatar {
@extend .avatar-container;
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline { &.avatar-inline {
float: none; float: none;
...@@ -20,22 +45,6 @@ ...@@ -20,22 +45,6 @@
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
&.s16 { width: 16px; height: 16px; margin-right: 6px; }
&.s20 { width: 20px; height: 20px; margin-right: 7px; }
&.s24 { width: 24px; height: 24px; margin-right: 8px; }
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
&.s36 { width: 36px; height: 36px; margin-right: 10px; }
&.s40 { width: 40px; height: 40px; margin-right: 10px; }
&.s46 { width: 46px; height: 46px; margin-right: 15px; }
&.s48 { width: 48px; height: 48px; margin-right: 10px; }
&.s60 { width: 60px; height: 60px; margin-right: 12px; }
&.s70 { width: 70px; height: 70px; margin-right: 14px; }
&.s90 { width: 90px; height: 90px; margin-right: 15px; }
&.s110 { width: 110px; height: 110px; margin-right: 15px; }
&.s140 { width: 140px; height: 140px; margin-right: 20px; }
&.s160 { width: 160px; height: 160px; margin-right: 20px; }
} }
.identicon { .identicon {
...@@ -54,3 +63,17 @@ ...@@ -54,3 +63,17 @@
&.s140 { font-size: 72px; line-height: 138px; } &.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; } &.s160 { font-size: 96px; line-height: 158px; }
} }
.image-container {
@extend .avatar-container;
overflow: hidden;
display: flex;
.avatar {
border-radius: 0;
border: none;
height: auto;
margin: 0;
align-self: center;
}
}
\ No newline at end of file
...@@ -136,3 +136,35 @@ label { ...@@ -136,3 +136,35 @@ label {
color: $red-normal; 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;
}
}
...@@ -21,7 +21,14 @@ ...@@ -21,7 +21,14 @@
background: $color-darker; background: $color-darker;
} }
.nav-sidebar li { .sidebar-header,
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
}
.nav-sidebar {
li {
a { a {
color: $color-light; color: $color-light;
...@@ -73,6 +80,8 @@ ...@@ -73,6 +80,8 @@
} }
} }
} }
}
} }
} }
......
...@@ -49,12 +49,16 @@ header { ...@@ -49,12 +49,16 @@ header {
font-size: 18px; font-size: 18px;
padding: 0; padding: 0;
margin: ($header-height - 28) / 2 0; margin: ($header-height - 28) / 2 0;
margin-left: 10px; margin-left: 8px;
height: 28px; height: 28px;
min-width: 28px; min-width: 28px;
line-height: 28px; line-height: 28px;
text-align: center; text-align: center;
&.header-user-dropdown-toggle {
margin-left: 14px;
}
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
......
...@@ -142,10 +142,6 @@ ul.content-list { ...@@ -142,10 +142,6 @@ ul.content-list {
} }
} }
.avatar {
margin-right: 15px;
}
.controls { .controls {
float: right; float: right;
......
...@@ -7,8 +7,70 @@ ...@@ -7,8 +7,70 @@
.pagination { .pagination {
padding: 0; padding: 0;
} }
.gap,
.gap:hover {
background-color: $gray-light;
padding: $gl-vert-padding;
cursor: default;
}
} }
.panel > .gl-pagination { .panel > .gl-pagination {
margin: 0; margin: 0;
} }
/**
* Extra-small screen pagination.
*/
@media (max-width: 320px) {
.gl-pagination {
.first,
.last {
display: none;
}
.page {
display: none;
&.active {
display: inline;
}
}
}
}
/**
* Small screen pagination
*/
@media (max-width: $screen-xs) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
}
.page {
display: none;
&.active {
display: inline;
}
}
}
}
/**
* Medium screen pagination
*/
@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
.gl-pagination {
.page {
display: none;
&.active,
&.sibling {
display: inline;
}
}
}
}
...@@ -59,6 +59,11 @@ ...@@ -59,6 +59,11 @@
padding: 0 !important; padding: 0 !important;
} }
.sidebar-header {
padding: 11px 22px 12px;
font-size: 20px;
}
li { li {
&.separate-item { &.separate-item {
padding-top: 10px; padding-top: 10px;
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
opacity: 1!important; opacity: 1!important;
* { * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
// !important to make sure no style can override this when dragging // !important to make sure no style can override this when dragging
cursor: -webkit-grabbing!important; cursor: -webkit-grabbing!important;
cursor: grabbing!important; cursor: grabbing!important;
...@@ -45,11 +49,6 @@ ...@@ -45,11 +49,6 @@
.page-with-sidebar { .page-with-sidebar {
padding-bottom: 0; padding-bottom: 0;
} }
.issues-filters {
position: relative;
z-index: 999999;
}
} }
.boards-app { .boards-app {
......
...@@ -52,10 +52,25 @@ ...@@ -52,10 +52,25 @@
.build-header { .build-header {
position: relative; position: relative;
padding: 0;
display: flex;
min-height: 58px;
align-items: center;
.btn-inverted {
@include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
}
@media (max-width: $screen-sm-max) {
padding-right: 40px; padding-right: 40px;
@media (min-width: $screen-sm-min) { .btn-inverted {
padding-right: 0; display: none;
}
}
.header-content {
flex: 1;
} }
a { a {
...@@ -137,10 +152,15 @@ ...@@ -137,10 +152,15 @@
.retry-link { .retry-link {
color: $gl-link-color; color: $gl-link-color;
display: none;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
@media (max-width: $screen-sm-max) {
display: block;
}
} }
.stage-item { .stage-item {
......
...@@ -36,10 +36,6 @@ ...@@ -36,10 +36,6 @@
} }
} }
.dash-project-avatar {
float: left;
}
.dash-project-access-icon { .dash-project-access-icon {
float: left; float: left;
margin-right: 5px; margin-right: 5px;
......
...@@ -3,12 +3,14 @@ ...@@ -3,12 +3,14 @@
} }
.dashboard .side .panel .panel-heading .input-group { .dashboard .side .panel .panel-heading .input-group {
.form-control { .form-control {
height: 42px; height: 42px;
} }
} }
.group-row { .group-row {
.stats { .stats {
float: right; float: right;
line-height: $list-text-height; line-height: $list-text-height;
...@@ -21,12 +23,14 @@ ...@@ -21,12 +23,14 @@
} }
.ldap-group-links { .ldap-group-links {
.form-actions { .form-actions {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
} }
} }
.groups-cover-block { .groups-cover-block {
.container-fluid { .container-fluid {
position: relative; position: relative;
} }
...@@ -41,9 +45,14 @@ ...@@ -41,9 +45,14 @@
background-color: $background-color; background-color: $background-color;
} }
} }
.group-avatar {
border: 0;
}
} }
.groups-header { .groups-header {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.nav-links { .nav-links {
width: 35%; width: 35%;
......
...@@ -75,43 +75,17 @@ ...@@ -75,43 +75,17 @@
.login-body { .login-body {
font-size: 13px; font-size: 13px;
input + p { input + p {
margin-top: 5px; margin-top: 5px;
} }
.gl-field-success-outline { .username .validation-success {
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 {
color: $green-normal; color: $green-normal;
} }
.username .validation-error, .username .validation-error {
.gl-field-error-message {
color: $red-normal; color: $red-normal;
} }
.gl-field-hint {
color: $gl-text-color;
}
} }
} }
......
...@@ -96,8 +96,8 @@ ...@@ -96,8 +96,8 @@
.project-avatar { .project-avatar {
float: none; float: none;
margin-left: auto; margin: 0 auto;
margin-right: auto; border: none;
&.identicon { &.identicon {
border-radius: 50%; border-radius: 50%;
......
...@@ -39,6 +39,12 @@ module EventsHelper ...@@ -39,6 +39,12 @@ module EventsHelper
end end
end end
def event_filter_visible(feature_key)
return true unless @project
@project.feature_available?(feature_key, current_user)
end
def event_preposition(event) def event_preposition(event)
if event.push? || event.commented? || event.target if event.push? || event.commented? || event.target
"at" "at"
......
...@@ -237,7 +237,7 @@ class JiraService < IssueTrackerService ...@@ -237,7 +237,7 @@ class JiraService < IssueTrackerService
end end
def resource_url(resource) def resource_url(resource)
"#{Settings.gitlab['url'].chomp("/")}#{resource}" "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end end
def build_entity_url(entity_name, entity_id) def build_entity_url(entity_name, entity_id)
......
...@@ -2,11 +2,11 @@ class ProjectPolicy < BasePolicy ...@@ -2,11 +2,11 @@ class ProjectPolicy < BasePolicy
def rules def rules
team_access!(user) team_access!(user)
owner = user.admin? || owner = project.owner == user ||
project.owner == user ||
(project.group && project.group.has_owner?(user)) (project.group && project.group.has_owner?(user))
owner_access! if owner owner_access! if user.admin? || owner
team_member_owner_access! if owner
if project.public? || (project.internal? && !user.external?) if project.public? || (project.internal? && !user.external?)
guest_access! guest_access!
...@@ -16,7 +16,7 @@ class ProjectPolicy < BasePolicy ...@@ -16,7 +16,7 @@ class ProjectPolicy < BasePolicy
can! :read_build if project.public_builds? can! :read_build if project.public_builds?
if project.request_access_enabled && if project.request_access_enabled &&
!(owner || project.team.member?(user) || project_group_member?(user)) !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access can! :request_access
end end
end end
...@@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy ...@@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy
can! :destroy_issue can! :destroy_issue
end end
def team_member_owner_access!
team_member_reporter_access!
end
# Push abilities on the users team role # Push abilities on the users team role
def team_access!(user) def team_access!(user)
access = project.team.max_member_access(user.id) access = project.team.max_member_access(user.id)
......
...@@ -13,20 +13,8 @@ module MergeRequests ...@@ -13,20 +13,8 @@ module MergeRequests
merge_request.target_project ||= (project.forked_from_project || project) merge_request.target_project ||= (project.forked_from_project || project)
merge_request.target_branch ||= merge_request.target_project.default_branch merge_request.target_branch ||= merge_request.target_project.default_branch
if merge_request.target_branch.blank? || merge_request.source_branch.blank? messages = validate_branches(merge_request)
message = return build_failed(merge_request, messages) unless messages.empty?
if params[:source_branch] || params[:target_branch]
"You must select source and target branch"
end
return build_failed(merge_request, message)
end
if merge_request.source_project == merge_request.target_project &&
merge_request.target_branch == merge_request.source_branch
return build_failed(merge_request, 'You must select different branches')
end
compare = CompareService.new.execute( compare = CompareService.new.execute(
merge_request.source_project, merge_request.source_project,
...@@ -43,6 +31,34 @@ module MergeRequests ...@@ -43,6 +31,34 @@ module MergeRequests
private private
def validate_branches(merge_request)
messages = []
if merge_request.target_branch.blank? || merge_request.source_branch.blank?
messages <<
if params[:source_branch] || params[:target_branch]
"You must select source and target branch"
end
end
if merge_request.source_project == merge_request.target_project &&
merge_request.target_branch == merge_request.source_branch
messages << 'You must select different branches'
end
# See if source and target branches exist
unless merge_request.source_project.commit(merge_request.source_branch)
messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
end
unless merge_request.target_project.commit(merge_request.target_branch)
messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
end
messages
end
# When your branch name starts with an iid followed by a dash this pattern will be # When your branch name starts with an iid followed by a dash this pattern will be
# interpreted as the user wants to close that issue on this project. # interpreted as the user wants to close that issue on this project.
# #
...@@ -91,8 +107,10 @@ module MergeRequests ...@@ -91,8 +107,10 @@ module MergeRequests
merge_request merge_request
end end
def build_failed(merge_request, message) def build_failed(merge_request, messages)
merge_request.errors.add(:base, message) unless message.nil? messages.compact.each do |message|
merge_request.errors.add(:base, message)
end
merge_request.compare_commits = [] merge_request.compare_commits = []
merge_request.can_be_created = false merge_request.can_be_created = false
merge_request merge_request
......
= render 'devise/shared/tab_single', tab_title: 'Sign in preview' = render 'devise/shared/tab_single', tab_title: 'Sign in preview'
.login-box .login-box
%form.show-gl-field-errors %form.gl-show-field-errors
.form-group .form-group
= label_tag :login = label_tag :login
= text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.' = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false) = visibility_level_icon(group.visibility_level, fw: false)
.image-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to [:admin, group], class: 'group-name' do = link_to [:admin, group], class: 'group-name' do
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
Group info: Group info:
%ul.well-list %ul.well-list
%li %li
.image-container.s60
= image_tag group_icon(@group), class: "avatar s60" = image_tag group_icon(@group), class: "avatar s60"
%li %li
%span.light Name: %span.light Name:
......
...@@ -76,6 +76,7 @@ ...@@ -76,6 +76,7 @@
.title .title
= link_to [:admin, project.namespace.becomes(Namespace), project] do = link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar .dash-project-avatar
.image-container.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40') = project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name %span.project-full-name
%span.namespace-name %span.namespace-name
......
= render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions' = render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions'
.login-box .login-box
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
.form-group .form-group
......
= render 'devise/shared/tab_single', tab_title:'Change your password' = render 'devise/shared/tab_single', tab_title:'Change your password'
.login-box .login-box
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
= f.hidden_field :reset_password_token = f.hidden_field :reset_password_token
......
= render 'devise/shared/tab_single', tab_title: 'Reset Password' = render 'devise/shared/tab_single', tab_title: 'Reset Password'
.login-box .login-box
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
.form-group .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 %div.form-group
= f.label "Username or email", for: :login = 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." = 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 .form-group
= label_tag :username, 'Username or email' = label_tag :username, 'Username or email'
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true } = 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 .form-group
= label_tag :username, "#{server['label']} Username" = label_tag :username, "#{server['label']} Username"
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } = text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.login-box .login-box
.login-body .login-body
- if @user.two_factor_otp_enabled? - 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 - resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div %div
......
#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' } #register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
%div.form-group %div.form-group
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required." = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
%div.username.form-group %div.username.form-group
= f.label :username = 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-error.hide Username is already taken.
%p.validation-success.hide Username is available. %p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability... %p.validation-pending.hide Checking username availability...
......
= render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions' = render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions'
.login-box .login-box
.login-body .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-errors
= devise_error_messages! = devise_error_messages!
.form-group.append-bottom-20 .form-group.append-bottom-20
......
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
.panel-heading .panel-heading
Group settings Group settings
.panel-body .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) = form_errors(@group)
= render 'shared/group_form', f: f = render 'shared/group_form', f: f
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
.image-container.s160
= image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
%p.light %p.light
- if @group.avatar? - if @group.avatar?
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
New Group New Group
%hr %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) = form_errors(@group)
= render 'shared/group_form', f: f, autofocus: true = render 'shared/group_form', f: f, autofocus: true
......
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
.cover-block.groups-cover-block .cover-block.groups-cover-block
%div{ class: container_class } %div{ class: container_class }
= image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile" .image-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
.group-info .group-info
.cover-title .cover-title
%h1 %h1
......
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
-# total_pages: total number of pages -# total_pages: total number of pages
-# per_page: number of items to fetch per page -# per_page: number of items to fetch per page
-# remote: data-remote -# remote: data-remote
%li{class: "page"} %li
%span.page.gap %span.gap
= raw(t 'views.pagination.truncate') = raw(t 'views.pagination.truncate')
...@@ -6,5 +6,5 @@ ...@@ -6,5 +6,5 @@
-# total_pages: total number of pages -# total_pages: total number of pages
-# per_page: number of items to fetch per page -# per_page: number of items to fetch per page
-# remote: data-remote -# remote: data-remote
%li{class: "page#{' active' if page.current?}"} %li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"}
= link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil} = link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil}
...@@ -29,10 +29,6 @@ ...@@ -29,10 +29,6 @@
= icon('bell fw') = icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_pending_count = todos_pending_count
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('plus fw')
- if Gitlab::Sherlock.enabled? - if Gitlab::Sherlock.enabled?
%li %li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions', = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
...@@ -48,6 +44,8 @@ ...@@ -48,6 +44,8 @@
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li %li
= link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
%li
= link_to "Help", help_path, aria: { label: "Help" }
%li.divider %li.divider
%li %li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
......
%ul.nav.nav-sidebar .nav-sidebar
.sidebar-header Across GitLab
%ul.nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
%span %span
Projects Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
%span
Todos
%span.count.js-todos-count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do = nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
%span %span
...@@ -39,11 +36,3 @@ ...@@ -39,11 +36,3 @@
= link_to dashboard_snippets_path, title: 'Snippets' do = link_to dashboard_snippets_path, title: 'Snippets' do
%span %span
Snippets Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
%span
Help
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
%span
Profile Settings
- empty_repo = @project.empty_repo? - empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) } .project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
%div{ class: container_class } %div{ class: container_class }
= project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile') .image-container.s70.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title %h1.project-title
= @project.name = @project.name
%span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
.board-inner .board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
%span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
data: { container: "body", placement: "bottom" } }
{{ list.title }} {{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" } .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
%span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" } %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
......
.content-block.build-header .content-block.build-header
.header-content
= ci_status_with_icon(@build.status) = ci_status_with_icon(@build.status)
Build Build
%strong ##{@build.id} %strong ##{@build.id}
...@@ -12,5 +13,7 @@ ...@@ -12,5 +13,7 @@
- if @build.user - if @build.user
= render "user" = render "user"
= time_ago_with_tooltip(@build.created_at) = time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left') = icon('angle-double-left')
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
.title .title
Build details Build details
- if can?(current_user, :update_build, @build) && @build.retryable? - if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request - if @build.merge_request
%p.build-detail-row %p.build-detail-row
%span.build-light-text Merge Request: %span.build-light-text Merge Request:
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span Fork %span Fork
- else - else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span Fork %span Fork
%div.count-with-arrow %div.count-with-arrow
%span.arrow %span.arrow
= link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do
= @project.forks_count = @project.forks_count
- if current_user - if current_user
= link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project) - if current_user.starred?(@project)
= icon('star') = icon('star')
%span.starred Unstar %span.starred Unstar
......
...@@ -118,6 +118,7 @@ ...@@ -118,6 +118,7 @@
Project avatar Project avatar
.form-group .form-group
- if @project.avatar? - if @project.avatar?
.image-container.s160
= project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
%p.light %p.light
- if @project.avatar_in_git - if @project.avatar_in_git
...@@ -180,6 +181,7 @@ ...@@ -180,6 +181,7 @@
%ul %ul
%li Build traces and artifacts %li Build traces and artifacts
%li LFS objects %li LFS objects
%li Container registry images
%hr %hr
- if can? current_user, :archive_project, @project - if can? current_user, :archive_project, @project
.row.prepend-top-default .row.prepend-top-default
......
...@@ -33,7 +33,12 @@ ...@@ -33,7 +33,12 @@
.form-actions .form-actions
- if @page && @page.persisted? - if @page && @page.persisted?
= f.submit 'Save changes', class: "btn-save btn" = f.submit 'Save changes', class: "btn-save btn"
= link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel" .pull-right
- if can?(current_user, :admin_wiki, @project)
= link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do
Delete
= link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped"
- else - else
= f.submit 'Create page', class: "btn-create btn" = f.submit 'Create page', class: "btn-create btn"
.pull-right
= link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel"
...@@ -7,6 +7,3 @@ ...@@ -7,6 +7,3 @@
- if can?(current_user, :create_wiki, @project) - if can?(current_user, :create_wiki, @project)
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
Edit Edit
- if can?(current_user, :admin_wiki, @project)
= link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
Delete
...@@ -19,7 +19,5 @@ ...@@ -19,7 +19,5 @@
- if can?(current_user, :create_wiki, @project) - if can?(current_user, :create_wiki, @project)
= link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
New Page New Page
= render 'main_links'
= render 'form' = render 'form'
%ul.nav-links.event-filter.scrolling-tabs %ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, 'All' = event_filter_link EventFilter.all, 'All'
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.push, 'Push events'
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.merged, 'Merge events'
- if event_filter_visible(:issues)
= event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team' = event_filter_link EventFilter.team, 'Team'
...@@ -9,11 +9,13 @@ ...@@ -9,11 +9,13 @@
= f.label :path, class: 'control-label' do = f.label :path, class: 'control-label' do
Group path Group path
.col-sm-10 .col-sm-10
.input-group .input-group.gl-field-error-anchor
.input-group-addon .input-group-addon
= root_url = root_url
= f.text_field :path, placeholder: 'open-source', class: 'form-control', = 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? - if @group.persisted?
.alert.alert-warning.prepend-top-10 .alert.alert-warning.prepend-top-10
%ul %ul
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false) = visibility_level_icon(group.visibility_level, fw: false)
.image-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to group, class: 'group-name' do = link_to group, class: 'group-name' do
......
- project = @target_project || @project - project = @target_project || @project
- extra_class = extra_class || '' - extra_class = extra_class || ''
- show_menu_above = show_menu_above || false - show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) - selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
- if selected.present? - if selected.present?
= hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project - if project
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
= link_to project_path(project), class: dom_class(project) do = link_to project_path(project), class: dom_class(project) do
- if avatar - if avatar
.dash-project-avatar .dash-project-avatar
.image-container.s40
- if use_creator_avatar - if use_creator_avatar
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- else - else
......
.clearfix .clearfix
- groups.each do |group| - groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do = link_to group, class: 'profile-groups-avatars inline', title: group.name do
.image-container.s40
= image_tag group_icon(group), class: 'avatar group-avatar s40' = image_tag group_icon(group), class: 'avatar group-avatar s40'
...@@ -16,7 +16,7 @@ class PostReceive ...@@ -16,7 +16,7 @@ class PostReceive
post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
if post_received.project.nil? if post_received.project.nil?
log("Triggered hook for non-existing project with full path \"#{repo_path} \"") log("Triggered hook for non-existing project with full path \"#{repo_path}\"")
return false return false
end end
...@@ -25,7 +25,7 @@ class PostReceive ...@@ -25,7 +25,7 @@ class PostReceive
elsif post_received.regular_project? elsif post_received.regular_project?
process_project_changes(post_received) process_project_changes(post_received)
else else
log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"") log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"")
false false
end end
end end
...@@ -37,7 +37,7 @@ class PostReceive ...@@ -37,7 +37,7 @@ class PostReceive
@user ||= post_received.identify(newrev) @user ||= post_received.identify(newrev)
unless @user unless @user
log("Triggered hook for non-existing user \"#{post_received.identifier} \"") log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
return false return false
end end
......
#!/usr/bin/env ruby
#
# Generate a changelog entry file in the correct location.
#
# Automatically stages the file and amends the previous commit if the `--amend`
# argument is used.
require 'optparse'
require 'yaml'
Options = Struct.new(
:amend,
:author,
:dry_run,
:force,
:merge_request,
:title
)
class ChangelogOptionParser
def self.parse(argv)
options = Options.new
parser = OptionParser.new do |opts|
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
opts.on('--amend', 'Amend the previous commit') do |value|
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
opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
options.dry_run = value
end
opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
options.author = git_user_name if value
end
opts.on('-h', '--help', 'Print help message') do
$stdout.puts opts
exit
end
end
parser.parse!(argv)
# Title is everything that remains, but let's clean it up a bit
options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
options
end
def self.git_user_name
%x{git config user.name}.strip
end
end
class ChangelogEntry
attr_reader :options
def initialize(options)
@options = options
assert_feature_branch!
assert_new_file!
assert_title!
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
$stdout.puts contents
unless options.dry_run
write
amend_commit if options.amend
end
end
def contents
YAML.dump(
'title' => title,
'merge_request' => options.merge_request,
'author' => options.author
)
end
def write
File.write(file_path, contents)
end
def amend_commit
%x{git add #{file_path}}
exec("git commit --amend")
end
private
def fail_with(message)
$stderr.puts "\e[31merror\e[0m #{message}"
exit 1
end
def assert_feature_branch!
return unless branch_name == 'master'
fail_with "Create a branch first!"
end
def assert_new_file!
return unless File.exist?(file_path)
return if options.force
fail_with "#{file_path} already exists! Use `--force` to overwrite."
end
def assert_title!
return if options.title.length > 0 || options.amend
fail_with "Provide a title for the changelog entry or use `--amend`" \
" to use the title from the previous commit."
end
def title
if options.title.empty?
last_commit_subject
else
options.title
end
end
def last_commit_subject
%x{git log --format="%s" -1}.strip
end
def file_path
File.join(
unreleased_path,
branch_name.gsub(/[^\w-]/, '-') << '.yml'
)
end
def unreleased_path
File.join('changelogs', 'unreleased').tap do |path|
path << '-ee' if ee?
end
end
def ee?
@ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__))
end
def branch_name
@branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '')
end
end
if $0 == __FILE__
options = ChangelogOptionParser.parse(ARGV)
ChangelogEntry.new(options)
end
# vim: ft=ruby
...@@ -432,7 +432,9 @@ production: &base ...@@ -432,7 +432,9 @@ production: &base
## Repositories settings ## Repositories settings
repositories: repositories:
# Paths where repositories can be stored. Give the canonicalized absolute pathname. # Paths where repositories can be stored. Give the canonicalized absolute pathname.
# NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!! # IMPORTANT: None of the path components may be symlink, because
# gitlab-shell invokes Dir.pwd inside the repository path and that results
# real path not the symlink.
storages: # You must have at least a `default` storage path. storages: # You must have at least a `default` storage path.
default: /home/git/repositories/ default: /home/git/repositories/
......
# Post deployment migrations are included by default. This file must be loaded
# before other initializers as Rails may otherwise memoize a list of migrations
# excluding the post deployment migrations.
unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
path = Rails.root.join('db', 'post_migrate').to_s
Rails.application.config.paths['db/migrate'] << path
# Rails memoizes migrations at certain points where it won't read the above
# path just yet. As such we must also update the following list of paths.
ActiveRecord::Migrator.migrations_paths << path
end
...@@ -42,3 +42,19 @@ end ...@@ -42,3 +42,19 @@ end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = redis_config_hash config.redis = redis_config_hash
end end
# The Sidekiq client API always adds the queue to the Sidekiq queue
# list, but mail_room and gitlab-shell do not. This is only necessary
# for monitoring.
config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
begin
Sidekiq.redis do |conn|
conn.pipelined do
config[:queues].each do |queue|
conn.sadd('queues', queue[0])
end
end
end
rescue Redis::BaseError, SocketError
end
# Maintenance Rake Tasks
## Gather information about GitLab and the system it runs on
This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:env:info
```
**Source Installation**
```
bundle exec rake gitlab:env:info RAILS_ENV=production
```
Example output:
```
System information
System: Debian 7.8
Current User: git
Using RVM: no
Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
Sidekiq Version: 2.17.8
GitLab information
Version: 7.7.1
Revision: 41ab9e1
Directory: /home/git/gitlab
DB Adapter: postgresql
URL: https://gitlab.example.com
HTTP Clone URL: https://gitlab.example.com/some-project.git
SSH Clone URL: git@gitlab.example.com:some-project.git
Using LDAP: no
Using Omniauth: no
GitLab Shell
Version: 2.4.1
Repositories: /home/git/repositories/
Hooks: /home/git/gitlab-shell/hooks/
Git: /usr/bin/git
```
## Check GitLab configuration
Runs the following rake tasks:
- `gitlab:gitlab_shell:check`
- `gitlab:sidekiq:check`
- `gitlab:app:check`
It will check that each component was setup according to the installation guide and suggest fixes for issues found.
You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
**Omnibus Installation**
```
sudo gitlab-rake gitlab:check
```
**Source Installation**
```
bundle exec rake gitlab:check RAILS_ENV=production
```
NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output.
Example output:
```
Checking Environment ...
Git configured for git user? ... yes
Has python2? ... yes
python2 is supported version? ... yes
Checking Environment ... Finished
Checking GitLab Shell ...
GitLab Shell version? ... OK (1.2.0)
Repo base directory exists? ... yes
Repo base directory is a symlink? ... no
Repo base owned by git:git? ... yes
Repo base access is drwxrws---? ... yes
post-receive hook up-to-date? ... yes
post-receive hooks in repos are links: ... yes
Checking GitLab Shell ... Finished
Checking Sidekiq ...
Running? ... yes
Checking Sidekiq ... Finished
Checking GitLab ...
Database config exists? ... yes
Database is SQLite ... no
All migrations up? ... yes
GitLab config exists? ... yes
GitLab config outdated? ... no
Log directory writable? ... yes
Tmp directory writable? ... yes
Init script exists? ... yes
Init script up-to-date? ... yes
Redis version >= 2.0.0? ... yes
Checking GitLab ... Finished
```
## Rebuild authorized_keys file
In some case it is necessary to rebuild the `authorized_keys` file.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:shell:setup
```
**Source Installation**
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
```
```
This will rebuild an authorized_keys file.
You will lose any data stored in authorized_keys file.
Do you want to continue (yes/no)? yes
```
## Clear redis cache
If for some reason the dashboard shows wrong information you might want to
clear Redis' cache.
**Omnibus Installation**
```
sudo gitlab-rake cache:clear
```
**Source Installation**
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
```
## Precompile the assets
Sometimes during version upgrades you might end up with some wrong CSS or
missing some icons. In that case, try to precompile the assets again.
Note that this only applies to source installations and does NOT apply to
Omnibus packages.
**Source Installation**
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
```
For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
the release of upstream GitLab. The omnibus version includes optimized versions
of those assets. Unless you are modifying the JavaScript / CSS code on your
production machine after installing the package, there should be no reason to redo
rake assets:precompile on the production machine. If you suspect that assets
have been corrupted, you should reinstall the omnibus package.
## Tracking Deployments
GitLab provides a Rake task that lets you track deployments in GitLab
Performance Monitoring. This Rake task simply stores the current GitLab version
in the GitLab Performance Monitoring database.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:track_deployment
```
**Source Installation**
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
```
## Create or repair repository hooks symlink
If the GitLab shell hooks directory location changes or another circumstance
leads to the hooks symlink becoming missing or invalid, run this Rake task
to create or repair the symlinks.
**Omnibus Installation**
```
sudo gitlab-rake gitlab:shell:create_hooks
```
**Source Installation**
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production
```
...@@ -107,7 +107,7 @@ downtime. Otherwise skip to the next section. ...@@ -107,7 +107,7 @@ downtime. Otherwise skip to the next section.
1. To see the current threads, run: 1. To see the current threads, run:
``` ```
apply all thread bt thread apply all bt
``` ```
1. Once you're done debugging with `gdb`, be sure to detach from the process and exit: 1. Once you're done debugging with `gdb`, be sure to detach from the process and exit:
......
...@@ -1139,6 +1139,7 @@ Parameters: ...@@ -1139,6 +1139,7 @@ Parameters:
| `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events | | `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Edit project hook ### Edit project hook
...@@ -1164,6 +1165,7 @@ Parameters: ...@@ -1164,6 +1165,7 @@ Parameters:
| `pipeline_events` | boolean | no | Trigger hook on pipeline events | | `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events | | `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Delete project hook ### Delete project hook
......
...@@ -28,9 +28,12 @@ Example response: ...@@ -28,9 +28,12 @@ Example response:
```json ```json
[ [
{ {
"id" : 1, "id":1,
"url" : "https://gitlab.example.com/hook", "url":"https://gitlab.example.com/hook",
"created_at" : "2015-11-04T20:07:35.874Z" "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 ...@@ -48,6 +51,10 @@ POST /hooks
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `url` | string | yes | The hook URL | | `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: Example request:
...@@ -60,9 +67,12 @@ Example response: ...@@ -60,9 +67,12 @@ Example response:
```json ```json
[ [
{ {
"id" : 2, "id":1,
"url" : "https://gitlab.example.com/hook", "url":"https://gitlab.example.com/hook",
"created_at" : "2015-11-04T20:07:35.874Z" "created_at":"2016-10-31T12:32:15.192Z",
"push_events":true,
"tag_push_events":false,
"enable_ssl_verification":true
} }
] ]
``` ```
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
## Process ## Process
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed. - [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Merge request performance guidelines](merge_request_performance_guidelines.md) - [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance for ensuring merge requests do not negatively impact GitLab performance
...@@ -41,6 +42,7 @@ ...@@ -41,6 +42,7 @@
- [What requires downtime?](what_requires_downtime.md) - [What requires downtime?](what_requires_downtime.md)
- [Adding database indexes](adding_database_indexes.md) - [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md)
## Compliance ## Compliance
......
# Generate a changelog entry
This guide contains instructions for generating a changelog entry data file, as
well as information and history about our changelog process.
## Overview
Each bullet point, or **entry**, in our [`CHANGELOG.md`][changelog.md] file is
generated from a single data file in the [`changelogs/unreleased/`][unreleased]
(or corresponding EE) folder. The file is expected to be a [YAML] file in the
following format:
```yaml
---
title: "Going through change[log]s"
merge_request: 1972
author: Ozzy Osbourne
```
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.
[changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md
[unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/
[YAML]: https://en.wikipedia.org/wiki/YAML
## Instructions
A `bin/changelog` script is available to generate the changelog entry file
automatically.
Its simplest usage is to provide the value for `title`:
```text
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
create changelogs/unreleased/my-feature.yml
---
title: Hey DZ, I added a feature to GitLab!
merge_request:
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.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 |
| `--help` | `-h` | Print help message |
#### `--amend`
You can pass the **`--amend`** argument to automatically stage the generated
file and amend it to the previous commit.
If you use **`--amend`** and don't provide a title, it will automatically use
the "subject" of the previous commit, which is the first line of the commit
message:
```text
$ git show --oneline
ab88683 Added an awesome new feature to GitLab
$ bin/changelog --amend
create changelogs/unreleased/feature-hey-dz.yml
---
title: Added an awesome new feature to GitLab
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
`merge_request` value:
```text
$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -m 1983
create changelogs/unreleased/feature-hey-dz.yml
---
title: Hey DZ, I added a feature to GitLab!
merge_request: 1983
author:
```
#### `--dry-run` or `-n`
Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
committing anything:
```text
$ bin/changelog --amend --dry-run
create changelogs/unreleased/feature-hey-dz.yml
---
title: Added an awesome new feature to GitLab
merge_request:
author:
$ ls changelogs/unreleased/
```
#### `--git-username` or `-u`
Use the **`--git-username`** or **`-u`** argument to automatically fill in the
`author` value with your configured Git `user.name` value:
```text
$ git config user.name
Jane Doe
$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!'
create changelogs/unreleased/feature-hey-dz.yml
---
title: Hey DZ, I added a feature to GitLab!
merge_request:
author: Jane Doe
```
## History and Reasoning
Our `CHANGELOG` file was previously updated manually by each contributor that
felt their change warranted an entry. When two merge requests added their own
entries at the same spot in the list, it created a merge conflict in one as soon
as the other was merged. When we had dozens of merge requests fighting for the
same changelog entry location, this quickly became a major source of merge
conflicts and delays in development.
This led us to a [boring solution] of "add your entry in a random location in
the list." This actually worked pretty well as we got further along in each
monthly release cycle, but at the start of a new cycle, when a new version
section was added and there were fewer places to "randomly" add an entry, the
conflicts became a problem again until we had a sufficient number of entries.
On top of all this, it created an entirely different headache for [release managers]
when they cherry-picked a commit into a stable branch for a patch release. If
the commit included an entry in the `CHANGELOG`, it would include the entire
changelog for the latest version in `master`, so the release manager would have
to manually remove the later entries. They often would have had to do this
multiple times per patch release. This was compounded when we had to release
multiple patches at once due to a security issue.
We needed to automate all of this manual work. So we [started brainstorming].
After much discussion we settled on the current solution of one file per entry,
and then compiling the entries into the overall `CHANGELOG.md` file during the
[release process].
[boring solution]: https://about.gitlab.com/handbook/#boring-solutions
[release managers]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/release-manager.md
[started brainstorming]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17826
[release process]: https://gitlab.com/gitlab-org/release-tools
---
[Return to Development documentation](README.md)
# Post Deployment Migrations
Post deployment migrations are regular Rails migrations that can optionally be
executed after a deployment. By default these migrations are executed alongside
the other migrations. To skip these migrations you will have to set the
environment variable `SKIP_POST_DEPLOYMENT_MIGRATIONS` to a non-empty value
when running `rake db:migrate`.
For example, this would run all migrations including any post deployment
migrations:
```bash
bundle exec rake db:migrate
```
This however will skip post deployment migrations:
```bash
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bundle exec rake db:migrate
```
## Deployment Integration
Say you're using Chef for deploying new versions of GitLab and you'd like to run
post deployment migrations after deploying a new version. Let's assume you
normally use the command `chef-client` to do so. To make use of this feature
you'd have to run this command as follows:
```bash
SKIP_POST_DEPLOYMENT_MIGRATIONS=true sudo chef-client
```
Once all servers have been updated you can run `chef-client` again on a single
server _without_ the environment variable.
The process is similar for other deployment techniques: first you would deploy
with the environment variable set, then you'll essentially re-deploy a single
server but with the variable _unset_.
## Creating Migrations
To create a post deployment migration you can use the following Rails generator:
```bash
bundle exec rails g post_deployment_migration migration_name_here
```
This will generate the migration file in `db/post_migrate`. These migrations
behave exactly like regular Rails migrations.
## Use Cases
Post deployment migrations can be used to perform migrations that mutate state
that an existing version of GitLab depends on. For example, say you want to
remove a column from a table. This requires downtime as a GitLab instance
depends on this column being present while it's running. Normally you'd follow
these steps in such a case:
1. Stop the GitLab instance
2. Run the migration removing the column
3. Start the GitLab instance again
Using post deployment migrations we can instead follow these steps:
1. Deploy a new version of GitLab while ignoring post deployment migrations
2. Re-run `rake db:migrate` but without the environment variable set
Here we don't need any downtime as the migration takes place _after_ a new
version (which doesn't depend on the column anymore) has been deployed.
Some other examples where these migrations are useful:
* Cleaning up data generated due to a bug in GitLab
* Removing tables
* Migrating jobs from one Sidekiq queue to another
...@@ -132,6 +132,42 @@ Adding new Spinach scenarios is acceptable _only if_ the new scenario requires ...@@ -132,6 +132,42 @@ Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
no more than one new `step` definition. If more than that is required, the no more than one new `step` definition. If more than that is required, the
test should be re-implemented using RSpec instead. test should be re-implemented using RSpec instead.
## Testing Rake Tasks
To make testing Rake tasks a little easier, there is a helper that can be included
in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
a few other things to make testing Rake tasks easier.
At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/rake_helpers.rb` for all available
methods.
Example:
```ruby
require 'rake_helper'
describe 'gitlab:shell rake tasks' do
before do
Rake.application.rake_require 'tasks/gitlab/shell'
stub_warn_user_is_not_gitlab
end
describe 'install task' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
run_rake_task('gitlab:shell:install')
end
end
end
```
--- ---
[Return to Development documentation](README.md) [Return to Development documentation](README.md)
...@@ -271,9 +271,9 @@ sudo usermod -aG redis git ...@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### Configure It
......
...@@ -135,7 +135,7 @@ password as they will be needed when configuring GitLab in the next section. ...@@ -135,7 +135,7 @@ password as they will be needed when configuring GitLab in the next section.
JIRA configuration in GitLab is done via a project's **Services**. JIRA configuration in GitLab is done via a project's **Services**.
#### GitLab 13.0 with JIRA v1000.x #### GitLab 8.13.0 with JIRA v1000.x
To enable JIRA integration in a project, navigate to the project's To enable JIRA integration in a project, navigate to the project's
and open the context menu clicking on the top right gear icon, then go to and open the context menu clicking on the top right gear icon, then go to
...@@ -160,7 +160,7 @@ with the linked JIRA project. ...@@ -160,7 +160,7 @@ with the linked JIRA project.
#### GitLab 6.x-7.7 with JIRA v6.x #### GitLab 6.x-7.7 with JIRA v6.x
_**Note:** GitLab versions 13.0 and up contain various integration improvements. _**Note:** GitLab versions 8.13.0 and up contain various integration improvements.
We strongly recommend upgrading._ We strongly recommend upgrading._
In `gitlab.yml` enable the JIRA issue tracker section by In `gitlab.yml` enable the JIRA issue tracker section by
......
...@@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro ...@@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro
the SAML request, but this error never reaches GitLab due to the CSRF check. the SAML request, but this error never reaches GitLab due to the CSRF check.
To bypass this you can add `skip_before_action :verify_authenticity_token` to the To bypass this you can add `skip_before_action :verify_authenticity_token` to the
`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, `omniauth_callbacks_controller.rb` file immediately after the `class` line and
where it can then be seen in the usual logs, or as a flash message in the login comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This
screen. will allow the error to hit GitLab, where it can then be seen in the usual logs,
or as a flash message on the login screen.
That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
installations from source. for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for
installations from source. Restart Unicorn using the `sudo gitlab-ctl restart unicorn`
command on Omnibus installations and `sudo service gitlab restart` on installations
from source.
You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer)
(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace)
(Chrome) browser extensions useful in your debugging.
### Invalid audience ### Invalid audience
......
# Maintenance # Maintenance Rake Tasks
## Gather information about GitLab and the system it runs on This document was moved to [administration/raketasks/maintenance](../administration/raketasks/maintenance.md).
This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
```
# omnibus-gitlab
sudo gitlab-rake gitlab:env:info
# installation from source
bundle exec rake gitlab:env:info RAILS_ENV=production
```
Example output:
```
System information
System: Debian 7.8
Current User: git
Using RVM: no
Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
Sidekiq Version: 2.17.8
GitLab information
Version: 7.7.1
Revision: 41ab9e1
Directory: /home/git/gitlab
DB Adapter: postgresql
URL: https://gitlab.example.com
HTTP Clone URL: https://gitlab.example.com/some-project.git
SSH Clone URL: git@gitlab.example.com:some-project.git
Using LDAP: no
Using Omniauth: no
GitLab Shell
Version: 2.4.1
Repositories: /home/git/repositories/
Hooks: /home/git/gitlab-shell/hooks/
Git: /usr/bin/git
```
## Check GitLab configuration
Runs the following rake tasks:
- `gitlab:gitlab_shell:check`
- `gitlab:sidekiq:check`
- `gitlab:app:check`
It will check that each component was setup according to the installation guide and suggest fixes for issues found.
You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
```
# omnibus-gitlab
sudo gitlab-rake gitlab:check
# installation from source
bundle exec rake gitlab:check RAILS_ENV=production
```
NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output.
Example output:
```
Checking Environment ...
Git configured for git user? ... yes
Has python2? ... yes
python2 is supported version? ... yes
Checking Environment ... Finished
Checking GitLab Shell ...
GitLab Shell version? ... OK (1.2.0)
Repo base directory exists? ... yes
Repo base directory is a symlink? ... no
Repo base owned by git:git? ... yes
Repo base access is drwxrws---? ... yes
post-receive hook up-to-date? ... yes
post-receive hooks in repos are links: ... yes
Checking GitLab Shell ... Finished
Checking Sidekiq ...
Running? ... yes
Checking Sidekiq ... Finished
Checking GitLab ...
Database config exists? ... yes
Database is SQLite ... no
All migrations up? ... yes
GitLab config exists? ... yes
GitLab config outdated? ... no
Log directory writable? ... yes
Tmp directory writable? ... yes
Init script exists? ... yes
Init script up-to-date? ... yes
Redis version >= 2.0.0? ... yes
Checking GitLab ... Finished
```
## Rebuild authorized_keys file
In some case it is necessary to rebuild the `authorized_keys` file.
For Omnibus-packages:
```
sudo gitlab-rake gitlab:shell:setup
```
For installations from source:
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
```
```
This will rebuild an authorized_keys file.
You will lose any data stored in authorized_keys file.
Do you want to continue (yes/no)? yes
```
## Clear redis cache
If for some reason the dashboard shows wrong information you might want to
clear Redis' cache.
For Omnibus-packages:
```
sudo gitlab-rake cache:clear
```
For installations from source:
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
```
## Precompile the assets
Sometimes during version upgrades you might end up with some wrong CSS or
missing some icons. In that case, try to precompile the assets again.
Note that this only applies to source installations and does NOT apply to
omnibus packages.
For installations from source:
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
```
For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
the release of upstream GitLab. The omnibus version includes optimized versions
of those assets. Unless you are modifying the JavaScript / CSS code on your
production machine after installing the package, there should be no reason to redo
rake assets:precompile on the production machine. If you suspect that assets
have been corrupted, you should reinstall the omnibus package.
## Tracking Deployments
GitLab provides a Rake task that lets you track deployments in GitLab
Performance Monitoring. This Rake task simply stores the current GitLab version
in the GitLab Performance Monitoring database.
For Omnibus-packages:
```
sudo gitlab-rake gitlab:track_deployment
```
For installations from source:
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
```
# 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.
...@@ -85,6 +85,8 @@ possible. ...@@ -85,6 +85,8 @@ possible.
- [MySQL installation guide](../install/database_mysql.md) contains additional - [MySQL installation guide](../install/database_mysql.md) contains additional
information about configuring GitLab to work with a MySQL database. information about configuring GitLab to work with a MySQL database.
- [Restoring from backup after a failed upgrade](restore_after_failure.md) - [Restoring from backup after a failed upgrade](restore_after_failure.md)
- [Upgrading PostgreSQL Using Slony](upgrading_postgresql_using_slony.md), for
upgrading a PostgreSQL database with minimal downtime.
[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html
[source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update
......
# Upgrading PostgreSQL Using Slony
This guide describes the steps one can take to upgrade their PostgreSQL database
to the latest version without the need for hours of downtime. This guide assumes
you have two database servers: one database server running an older version of
PostgreSQL (e.g. 9.2.18) and one server running a newer version (e.g. 9.6.0).
For this process we'll use a PostgreSQL replication tool called
["Slony"](http://www.slony.info/). Slony allows replication between different
PostgreSQL versions and as such can be used to upgrade a cluster with a minimal
amount of downtime.
In various places we'll refer to the user `gitlab-psql`. This user should be the
user used to run the various PostgreSQL OS processes. If you're using a
different user (e.g. `postgres`) you should replace `gitlab-psql` with the name
of said user. This guide also assumes your database is called
`gitlabhq_production`. If you happen to use a different database name you should
change this accordingly.
## Database Dumps
Slony only replicates data and not any schema changes. As a result we must
ensure that all databases have the same database structure.
To do so we'll generate a dump of our current database. This dump will only
contain the structure, not any data. To generate this dump run the following
command on your active database server:
```bash
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql -p 5432 -U gitlab-psql -s -f /tmp/structure.sql gitlabhq_production
```
If you're not using GitLab's Omnibus package you may have to adjust the paths to
`pg_dump` and the PostgreSQL installation directory to match the paths of your
configuration.
Once the structure dump is generated we also need to generate a dump for the
`schema_migrations` table. This table doesn't have any primary keys and as such
can't be replicated easily by Slony. To generate this dump run the following
command on your active database server:
```bash
sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql/ -p 5432 -U gitlab-psql -a -t schema_migrations -f /tmp/migrations.sql gitlabhq_production
```
Next we'll need to move these files somewhere accessible by the new database
server. The easiest way is to simply download these files to your local system:
```bash
scp your-user@production-database-host:/tmp/*.sql /tmp
```
This will copy all the SQL files located in `/tmp` to your local system's
`/tmp` directory. Once copied you can safely remove the files from the database
server.
## Installing Slony
Slony will be used to upgrade the database without requiring long downtimes.
Slony can be downloaded from http://www.slony.info/. If you have installed
PostgreSQL using your operating system's package manager you may also be able to
install Slony using said package manager.
When compiling Slony from source you *must* use the following commands to do so:
```bash
./configure --prefix=/path/to/installation/directory --with-perltools --with-pgconfigdir=/path/to/directory/containing/pg_config/bin
make
make install
```
Omnibus users can use the following commands:
```bash
./configure --prefix=/opt/gitlab/embedded --with-perltools --with-pgconfigdir=/opt/gitlab/embedded/bin
make
make install
```
This assumes you have installed GitLab into /opt/gitlab.
To test if Slony is installed properly, run the following commands:
```bash
test -f /opt/gitlab/embedded/bin/slonik && echo 'Slony installed' || echo 'Slony not installed'
test -f /opt/gitlab/embedded/bin/slonik_init_cluster && echo 'Slony Perl tools are available' || echo 'Slony Perl tools are not available'
/opt/gitlab/embedded/bin/slonik -v
```
This assumes Slony was installed to `/opt/gitlab/embedded`. If Slony was
installed properly the output of these commands will be (the mentioned "slonik"
version may be different):
```
Slony installed
Slony Perl tools are available
slonik version 2.2.5
```
## Slony User
Next we must set up a PostgreSQL user that Slony can use to replicate your
database. To do so, log in to your production database using `psql` using a
super user account. Once done run the following SQL queries:
```sql
CREATE ROLE slony WITH SUPERUSER LOGIN REPLICATION ENCRYPTED PASSWORD 'password string here';
ALTER ROLE slony SET statement_timeout TO 0;
```
Make sure you replace "password string here" with the actual password for the
user. A password is *required*. This user must be created on _both_ the old and
new database server using the same password.
Once the user has been created make sure you note down the password as we will
need it later on.
## Configuring Slony
Now we can finally start configuring Slony. Slony uses a configuration file for
most of the work so we'll need to set this one up. This configuration file
specifies where to put log files, how Slony should connect to the databases,
etc.
First we'll need to create some required directories and set the correct
permissions. To do so, run the following commands on both the old and new
database server:
```bash
sudo mkdir -p /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony
sudo chown gitlab-psql:root /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony
```
Here `gitlab-psql` is the user used to run the PostgreSQL database processes. If
you're using a different user you should replace this with the name of said
user.
Now that the directories are in place we can create the configuration file. For
this we can use the following template:
```perl
if ($ENV{"SLONYNODES"}) {
require $ENV{"SLONYNODES"};
} else {
$CLUSTER_NAME = 'slony_replication';
$LOGDIR = '/var/log/gitlab/slony';
$MASTERNODE = 1;
$DEBUGLEVEL = 2;
add_node(host => 'OLD_HOST', dbname => 'gitlabhq_production', port =>5432,
user=>'slony', password=>'SLONY_PASSWORD', node=>1);
add_node(host => 'NEW_HOST', dbname => 'gitlabhq_production', port =>5432,
user=>'slony', password=>'SLONY_PASSWORD', node=>2, parent=>1 );
}
$SLONY_SETS = {
"set1" => {
"set_id" => 1,
"table_id" => 1,
"sequence_id" => 1,
"pkeyedtables" => [
TABLES
],
},
};
if ($ENV{"SLONYSET"}) {
require $ENV{"SLONYSET"};
}
# Please do not add or change anything below this point.
1;
```
In this configuration file you should replace a few placeholders before you can
use it. The following placeholders should be replaced:
* `OLD_HOST`: the address of the old database server.
* `NEW_HOST`: the address of the new database server.
* `SLONY_PASSWORD`: the password of the Slony user created earlier.
* `TABLES`: the tables to replicate.
The list of tables to replicate can be generated by running the following
command on your old PostgreSQL database:
```
sudo gitlab-psql gitlabhq_production -c "select concat('\"', schemaname, '.', tablename, '\",') from pg_catalog.pg_tables where schemaname = 'public' and tableowner = 'gitlab' and tablename != 'schema_migrations' order by tablename asc;" -t
```
If you're not using Omnibus you should replace `gitlab-psql` with the
appropriate path to the `psql` executable.
The above command outputs a list of tables in a format that can be copy-pasted
directly into the above configuration file. Make sure to _replace_ `TABLES` with
this output, don't just append it below it. Once done you'll end up with
something like this:
```perl
"pkeyedtables" => [
"public.abuse_reports",
"public.appearances",
"public.application_settings",
... more rows here ...
]
```
Once you have the configuration file generated you must install it on both the
old and new database. To do so, place it in
`/var/opt/gitlab/postgresql/slony/slon_tools.conf` (for which we created the
directory earlier on).
Now that the configuration file is in place we can _finally_ start replicating
our database. First we must set up the schema in our new database. To do so make
sure that the SQL files we generated earlier can be found in the `/tmp`
directory of the new server. Once these files are in place start a `psql`
session on this server:
```
sudo gitlab-psql gitlabhq_production
```
Now run the following commands:
```
\i /tmp/structure.sql
\i /tmp/migrations.sql
```
To verify if the structure is in place close the session, start it again, then
run `\d`. If all went well you should see output along the lines of the
following:
```
List of relations
Schema | Name | Type | Owner
--------+---------------------------------------------+----------+-------------
public | abuse_reports | table | gitlab
public | abuse_reports_id_seq | sequence | gitlab
public | appearances | table | gitlab
public | appearances_id_seq | sequence | gitlab
public | application_settings | table | gitlab
public | application_settings_id_seq | sequence | gitlab
public | approvals | table | gitlab
... more rows here ...
```
Now we can initialize the required tables and what not that Slony will use for
its replication process. To do so, run the following on the old database:
```
sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_init_cluster --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
```
If all went well this will produce something along the lines of:
```
<stdin>:10: Set up replication nodes
<stdin>:13: Next: configure paths for each node/origin
<stdin>:16: Replication nodes prepared
<stdin>:17: Please start a slon replication daemon for each node
```
Next we need to start a replication node on every server. To do so, run the
following on the old database:
```
sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
```
If all went well this will produce output such as:
```
Invoke slon for node 1 - /opt/gitlab/embedded/bin/slon -p /var/run/slony1/slony_replication_node1.pid -s 1000 -d2 slony_replication 'host=192.168.0.7 dbname=gitlabhq_production user=slony port=5432 password=hieng8ezohHuCeiqu0leeghai4aeyahp' > /var/log/gitlab/slony/node1/gitlabhq_production-2016-10-06.log 2>&1 &
Slon successfully started for cluster slony_replication, node node1
PID [26740]
Start the watchdog process as well...
```
Next we need to run the following command on the _new_ database server:
```
sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
```
This will produce similar output if all went well.
Next we need to tell the new database server what it should replicate. This can
be done by running the following command on the _new_ database server:
```
sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_create_set 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
```
This should produce output along the lines of the following:
```
<stdin>:11: Subscription set 1 (set1) created
<stdin>:12: Adding tables to the subscription set
<stdin>:16: Add primary keyed table public.abuse_reports
<stdin>:20: Add primary keyed table public.appearances
<stdin>:24: Add primary keyed table public.application_settings
... more rows here ...
<stdin>:327: Adding sequences to the subscription set
<stdin>:328: All tables added
```
Finally we can start the replication process by running the following on the
_new_ database server:
```
sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_subscribe_set 1 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
```
This should produce the following output:
```
<stdin>:6: Subscribed nodes to set 1
```
At this point the new database server will start replicating the data of the old
database server. This process can take anywhere from a few minutes to hours, if
not days. Unfortunately Slony itself doesn't really provide a way of knowing
when the two databases are in sync. To get an estimate of the progress you can
use the following shell script:
```
#!/usr/bin/env bash
set -e
user='slony'
pass='SLONY_PASSWORD'
function main {
while :
do
local source
local target
source=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h OLD_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A)
target=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h NEW_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A)
echo "$(date): ${target} of ${source}" >> progress.log
echo "$(date): ${target} of ${source}"
sleep 60
done
}
main
```
This script will compare the sizes of the old and new database every minute and
print the result to STDOUT as well as logging it to a file. Make sure to replace
`SLONY_PASSWORD`, `OLD_HOST`, and `NEW_HOST` with the correct values.
## Stopping Replication
At some point the two databases are in sync. Once this is the case you'll need
to plan for a few minutes of downtime. This small downtime window is used to
stop the replication process, remove any Slony data from both databases, restart
GitLab so it can use the new database, etc.
First, let's stop all of GitLab. Omnibus users can do so by running the
following on their GitLab server(s):
```
sudo gitlab-ctl stop unicorn
sudo gitlab-ctl stop sidekiq
sudo gitlab-ctl stop mailroom
```
If you have any other processes that use PostgreSQL you should also stop those.
Once everything has been stopped you should update any configuration settings,
DNS records, etc so they all point to the new database.
Once the settings have been taken care of we need to stop the replication
process. It's crucial that no new data is written to the databases at this point
as this data will be lost.
To stop replication, run the following on both database servers:
```bash
sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_kill --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
```
This will stop all the Slony processes on the host the command was executed on.
## Resetting Sequences
The above setup does not replicate database sequences, as such these must be
reset manually in the target database. You can use the following script for
this:
```bash
#!/usr/bin/env bash
set -e
function main {
local fix_sequences
local fix_owners
fix_sequences='/tmp/fix_sequences.sql'
fix_owners='/tmp/fix_owners.sql'
# The SQL queries were taken from
# https://wiki.postgresql.org/wiki/Fixing_Sequences
sudo gitlab-psql gitlabhq_production -t -c "
SELECT 'ALTER SEQUENCE '|| quote_ident(MIN(schema_name)) ||'.'|| quote_ident(MIN(seq_name))
||' OWNED BY '|| quote_ident(MIN(TABLE_NAME)) ||'.'|| quote_ident(MIN(column_name)) ||';'
FROM (
SELECT
n.nspname AS schema_name,
c.relname AS TABLE_NAME,
a.attname AS column_name,
SUBSTRING(d.adsrc FROM E'^nextval\\(''([^'']*)''(?:::text|::regclass)?\\)') AS seq_name
FROM pg_class c
JOIN pg_attribute a ON (c.oid=a.attrelid)
JOIN pg_attrdef d ON (a.attrelid=d.adrelid AND a.attnum=d.adnum)
JOIN pg_namespace n ON (c.relnamespace=n.oid)
WHERE has_schema_privilege(n.oid,'USAGE')
AND n.nspname NOT LIKE 'pg!_%' escape '!'
AND has_table_privilege(c.oid,'SELECT')
AND (NOT a.attisdropped)
AND d.adsrc ~ '^nextval'
) seq
GROUP BY seq_name HAVING COUNT(*)=1;
" > "${fix_owners}"
sudo gitlab-psql gitlabhq_production -t -c "
SELECT 'SELECT SETVAL(' ||
quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
FROM pg_class AS S,
pg_depend AS D,
pg_class AS T,
pg_attribute AS C,
pg_tables AS PGT
WHERE S.relkind = 'S'
AND S.oid = D.objid
AND D.refobjid = T.oid
AND D.refobjid = C.attrelid
AND D.refobjsubid = C.attnum
AND T.relname = PGT.tablename
ORDER BY S.relname;
" > "${fix_sequences}"
sudo gitlab-psql gitlabhq_production -f "${fix_owners}"
sudo gitlab-psql gitlabhq_production -f "${fix_sequences}"
rm "${fix_owners}" "${fix_sequences}"
}
main
```
Upload this script to the _target_ server and execute it as follows:
```bash
bash path/to/the/script/above.sh
```
This will correct the ownership of sequences and reset the next value for the
`id` column to the next available value.
## Removing Slony
Next we need to remove all Slony related data. To do so, run the following
command on the _target_ server:
```bash
sudo gitlab-psql gitlabhq_production -c "DROP SCHEMA _slony_replication CASCADE;"
```
Once done you can safely remove any Slony related files (e.g. the log
directory), and uninstall Slony if desired. At this point you can start your
GitLab instance again and if all went well it should be using your new database
server.
...@@ -18,7 +18,7 @@ Feature: Dashboard Active Tab ...@@ -18,7 +18,7 @@ Feature: Dashboard Active Tab
Then the active main tab should be Merge Requests Then the active main tab should be Merge Requests
And no other main tabs should be active And no other main tabs should be active
Scenario: On Dashboard Help Scenario: On Dashboard Groups
Given I visit dashboard help page Given I visit dashboard groups page
Then the active main tab should be Help Then the active main tab should be Groups
And no other main tabs should be active And no other main tabs should be active
...@@ -11,7 +11,6 @@ Feature: Dashboard ...@@ -11,7 +11,6 @@ Feature: Dashboard
And I visit dashboard page And I visit dashboard page
Scenario: I should see projects list Scenario: I should see projects list
Then I should see "New Project" link
Then I should see "Shop" project link Then I should see "Shop" project link
Then I should see "Shop" project CI status Then I should see "Shop" project CI status
......
...@@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps ...@@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
end end
step 'I visit the "Rake Tasks" help page' do step 'I visit the "Rake Tasks" help page' do
visit help_page_path("raketasks/maintenance") visit help_page_path("administration/raketasks/maintenance")
end end
step 'I should see "Rake Tasks" page markdown rendered' do step 'I should see "Rake Tasks" page markdown rendered' do
......
module SharedSidebarActiveTab module SharedSidebarActiveTab
include Spinach::DSL include Spinach::DSL
step 'the active main tab should be Help' do
ensure_active_main_tab('Help')
end
step 'no other main tabs should be active' do step 'no other main tabs should be active' do
expect(page).to have_selector('.nav-sidebar > li.active', count: 1) expect(page).to have_selector('.nav-sidebar li.active', count: 1)
end end
def ensure_active_main_tab(content) def ensure_active_main_tab(content)
...@@ -17,6 +13,10 @@ module SharedSidebarActiveTab ...@@ -17,6 +13,10 @@ module SharedSidebarActiveTab
ensure_active_main_tab('Projects') ensure_active_main_tab('Projects')
end end
step 'the active main tab should be Groups' do
ensure_active_main_tab('Groups')
end
step 'the active main tab should be Projects' do step 'the active main tab should be Projects' do
ensure_active_main_tab('Projects') ensure_active_main_tab('Projects')
end end
...@@ -28,8 +28,4 @@ module SharedSidebarActiveTab ...@@ -28,8 +28,4 @@ module SharedSidebarActiveTab
step 'the active main tab should be Merge Requests' do step 'the active main tab should be Merge Requests' do
ensure_active_main_tab('Merge Requests') ensure_active_main_tab('Merge Requests')
end end
step 'the active main tab should be Help' do
ensure_active_main_tab('Help')
end
end end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class <%= migration_class_name %> < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
# 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
end
end
...@@ -43,14 +43,13 @@ module API ...@@ -43,14 +43,13 @@ module API
end end
class Hook < Grape::Entity class Hook < Grape::Entity
expose :id, :url, :created_at expose :id, :url, :created_at, :push_events, :tag_push_events
expose :enable_ssl_verification
end end
class ProjectHook < Hook class ProjectHook < Hook
expose :project_id, :push_events expose :project_id, :issues_events, :merge_requests_events
expose :issues_events, :merge_requests_events, :tag_push_events
expose :note_events, :build_events, :pipeline_events, :wiki_page_events expose :note_events, :build_events, :pipeline_events, :wiki_page_events
expose :enable_ssl_verification
end end
class BasicProjectDetails < Grape::Entity class BasicProjectDetails < Grape::Entity
......
...@@ -47,7 +47,8 @@ module API ...@@ -47,7 +47,8 @@ module API
:build_events, :build_events,
:pipeline_events, :pipeline_events,
:wiki_page_events, :wiki_page_events,
:enable_ssl_verification :enable_ssl_verification,
:token
] ]
@hook = user_project.hooks.new(attrs) @hook = user_project.hooks.new(attrs)
...@@ -82,7 +83,8 @@ module API ...@@ -82,7 +83,8 @@ module API
:build_events, :build_events,
:pipeline_events, :pipeline_events,
:wiki_page_events, :wiki_page_events,
:enable_ssl_verification :enable_ssl_verification,
:token
] ]
if @hook.update_attributes attrs if @hook.update_attributes attrs
......
...@@ -12,6 +12,7 @@ module API ...@@ -12,6 +12,7 @@ module API
end end
get do get do
hooks = SystemHook.all hooks = SystemHook.all
present hooks, with: Entities::Hook present hooks, with: Entities::Hook
end end
...@@ -19,10 +20,14 @@ module API ...@@ -19,10 +20,14 @@ module API
success Entities::Hook success Entities::Hook
end end
params do 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 end
post do post do
hook = SystemHook.new declared(params).to_h hook = SystemHook.new declared(params, include_missing: false).to_h
if hook.save if hook.save
present hook, with: Entities::Hook present hook, with: Entities::Hook
......
require 'rails/generators'
module Rails
class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase
def create_migration_file
timestamp = Time.now.strftime('%Y%m%d%H%I%S')
template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb"
end
def migration_class_name
file_name.camelize
end
end
end
...@@ -3,10 +3,25 @@ module Gitlab ...@@ -3,10 +3,25 @@ module Gitlab
class AttributeCleaner class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
def self.clean!(relation_hash:) def self.clean(*args)
relation_hash.reject! do |key, _value| new(*args).clean
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key) 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 end
private
def prohibited_key?(key)
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end end
end end
end end
......
...@@ -43,6 +43,14 @@ module Gitlab ...@@ -43,6 +43,14 @@ module Gitlab
raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result 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 true
end end
end end
......
...@@ -55,7 +55,12 @@ module Gitlab ...@@ -55,7 +55,12 @@ module Gitlab
end end
def member_hash(member) 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 end
def find_project_user_query(member) def find_project_user_query(member)
......
...@@ -9,8 +9,14 @@ module Gitlab ...@@ -9,8 +9,14 @@ module Gitlab
end end
def restore def restore
begin
json = IO.read(@path) json = IO.read(@path)
@tree_hash = ActiveSupport::JSON.decode(json) @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') @project_members = @tree_hash.delete('project_members')
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
priorities: :label_priorities, priorities: :label_priorities,
label: :project_label }.freeze 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 PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
...@@ -30,7 +30,7 @@ module Gitlab ...@@ -30,7 +30,7 @@ module Gitlab
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:) def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym @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 @members_mapper = members_mapper
@user = user @user = user
@imported_object_retries = 0 @imported_object_retries = 0
...@@ -172,11 +172,8 @@ module Gitlab ...@@ -172,11 +172,8 @@ module Gitlab
end end
def parsed_relation_hash def parsed_relation_hash
@parsed_relation_hash ||= begin @parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash,
Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash) relation_class: relation_class)
@relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end
end end
def set_st_diffs def set_st_diffs
......
...@@ -24,12 +24,19 @@ module Gitlab ...@@ -24,12 +24,19 @@ module Gitlab
end end
def verify_version!(version) 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}") raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
else else
true true
end end
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 end
end end
...@@ -63,10 +63,10 @@ namespace :gitlab do ...@@ -63,10 +63,10 @@ namespace :gitlab do
# Launch installation process # Launch installation process
system(*%W(bin/install) + repository_storage_paths_args) system(*%W(bin/install) + repository_storage_paths_args)
end
# (Re)create hooks # (Re)create hooks
system(*%W(bin/create-hooks) + repository_storage_paths_args) Rake::Task['gitlab:shell:create_hooks'].invoke
end
# Required for debian packaging with PKGR: Setup .ssh/environment with # Required for debian packaging with PKGR: Setup .ssh/environment with
# the current PATH, so that the correct ruby version gets loaded # the current PATH, so that the correct ruby version gets loaded
...@@ -102,6 +102,15 @@ namespace :gitlab do ...@@ -102,6 +102,15 @@ namespace :gitlab do
end end
end end
end end
desc 'Create or repair repository hooks symlink'
task create_hooks: :environment do
warn_user_is_not_gitlab
puts 'Creating/Repairing hooks symlinks for all repositories'
system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
puts 'done'.color(:green)
end
end end
def setup def setup
......
require 'spec_helper'
load File.expand_path('../../bin/changelog', __dir__)
describe 'bin/changelog' do
describe ChangelogOptionParser do
it 'parses --ammend' do
options = described_class.parse(%w[foo bar --amend])
expect(options.amend).to eq true
end
it 'parses --force' do
options = described_class.parse(%w[foo --force bar])
expect(options.force).to eq true
end
it 'parses -f' do
options = described_class.parse(%w[foo -f bar])
expect(options.force).to eq true
end
it 'parses --merge-request' do
options = described_class.parse(%w[foo --merge-request 1234 bar])
expect(options.merge_request).to eq 1234
end
it 'parses -m' do
options = described_class.parse(%w[foo -m 4321 bar])
expect(options.merge_request).to eq 4321
end
it 'parses --dry-run' do
options = described_class.parse(%w[foo --dry-run bar])
expect(options.dry_run).to eq true
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
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])
expect(options.author).to eq 'John Smith'
end
it 'parses -h' do
expect do
$stdout = StringIO.new
described_class.parse(%w[foo -h bar])
end.to raise_error(SystemExit)
end
it 'assigns title' do
options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
expect(options.title).to eq 'foo bar baz'
end
end
end
...@@ -53,7 +53,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -53,7 +53,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'with lists' do context 'with lists' do
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:planning) { create(:label, project: project, name: 'Planning') } let(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
let(:development) { create(:label, project: project, name: 'Development') } let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') } let(:testing) { create(:label, project: project, name: 'Testing') }
let(:bug) { create(:label, project: project, name: 'Bug') } let(:bug) { create(:label, project: project, name: 'Bug') }
...@@ -91,6 +91,12 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -91,6 +91,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.board', count: 4) expect(page).to have_selector('.board', count: 4)
end end
it 'shows description tooltip on list title' do
page.within('.board:nth-child(2)') do
expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
end
end
it 'shows issues in lists' do it 'shows issues in lists' do
wait_for_board_cards(2, 2) wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2) wait_for_board_cards(3, 2)
......
...@@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(Milestone::None.title) filter_by_milestone(Milestone::None.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone')
expect(page).to have_css('.issue', count: 1) expect(page).to have_css('.issue', count: 1)
end end
...@@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title) filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 0) expect(page).to have_css('.issue', count: 0)
end end
...@@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title) filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 1) expect(page).to have_css('.issue', count: 1)
end end
...@@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title) filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 0) expect(page).to have_css('.issue', count: 0)
end end
end end
...@@ -55,6 +59,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -55,6 +59,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(milestone.title) filter_by_milestone(milestone.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
expect(page).to have_css('.issue', count: 1) expect(page).to have_css('.issue', count: 1)
end end
...@@ -70,6 +75,7 @@ feature 'Issue filtering by Milestone', feature: true do ...@@ -70,6 +75,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project) visit_issues(project)
filter_by_milestone(milestone.title) filter_by_milestone(milestone.title)
expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
expect(page).to have_css('.issue', count: 1) expect(page).to have_css('.issue', count: 1)
end end
end end
......
...@@ -59,4 +59,12 @@ feature 'Create New Merge Request', feature: true, js: true do ...@@ -59,4 +59,12 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_css('a.btn.active', text: 'Side-by-side') expect(page).to have_css('a.btn.active', text: 'Side-by-side')
end end
end end
it 'does not allow non-existing branches' do
visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
expect(page).to have_content('The form contains the following errors')
expect(page).to have_content('Source branch "non-exist-source" does not exist')
expect(page).to have_content('Target branch "non-exist-target" does not exist')
end
end end
...@@ -216,7 +216,9 @@ describe "Builds" do ...@@ -216,7 +216,9 @@ describe "Builds" do
@build.run! @build.run!
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Cancel' click_link 'Cancel'
click_link 'Retry' page.within('.build-header') do
click_link 'Retry build'
end
end end
it 'shows the right status and buttons' do it 'shows the right status and buttons' do
......
...@@ -164,5 +164,23 @@ describe 'Edit Project Settings', feature: true do ...@@ -164,5 +164,23 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_content "Customize your workflow!" expect(page).to have_content "Customize your workflow!"
end end
it "hides project activity tabs" do
select "Disabled", from: "project_project_feature_attributes_repository_access_level"
select "Disabled", from: "project_project_feature_attributes_issues_access_level"
select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
click_button "Save changes"
wait_for_ajax
visit activity_namespace_project_path(project.namespace, project)
page.within(".event-filter") do
expect(page).to have_selector("a", count: 2)
expect(page).not_to have_content("Push events")
expect(page).not_to have_content("Merge events")
expect(page).not_to have_content("Comments")
end
end
end end
end end
%form.show-gl-field-errors{action: 'submit', method: 'post'} %form.gl-show-field-errors{action: 'submit', method: 'post'}
.form-group .form-group
%input.required-text{required: true, type: 'text'} Text %input.required-text{required: true, type: 'text'} Text
.form-group .form-group
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
.form-group .form-group
%input.hidden{ type:'hidden' } %input.hidden{ type:'hidden' }
.form-group .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 .form-group
%input.submit{type: 'submit'} Submit %input.submit{type: 'submit'} Submit
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
describe('GL Style Field Errors', function() { describe('GL Style Field Errors', function() {
beforeEach(function() { beforeEach(function() {
fixture.load('gl_field_errors.html'); 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); this.fieldErrors = new global.GlFieldErrors($form);
}); });
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
}); });
it('should ignore elements with custom error handling', function() { it('should ignore elements with custom error handling', function() {
const customErrorFlag = 'no-gl-field-errors'; const customErrorFlag = 'gl-field-error-ignore';
const customErrorElem = $(`.${customErrorFlag}`); const customErrorElem = $(`.${customErrorFlag}`);
expect(customErrorElem.length).toBe(1); expect(customErrorElem.length).toBe(1);
......
...@@ -122,6 +122,14 @@ describe Gitlab::GitAccess, lib: true do ...@@ -122,6 +122,14 @@ describe Gitlab::GitAccess, lib: true do
describe 'build authentication_abilities permissions' do describe 'build authentication_abilities permissions' do
let(:authentication_abilities) { build_authentication_abilities } let(:authentication_abilities) { build_authentication_abilities }
describe 'owner' do
let(:project) { create(:project, namespace: user.namespace) }
context 'pull code' do
it { expect(subject).to be_allowed }
end
end
describe 'reporter user' do describe 'reporter user' do
before { project.team << [user, :reporter] } before { project.team << [user, :reporter] }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::AttributeCleaner, lib: true do describe Gitlab::ImportExport::AttributeCleaner, lib: true do
let(:relation_class){ double('relation_class').as_null_object }
let(:unsafe_hash) do let(:unsafe_hash) do
{ {
'id' => 101,
'service_id' => 99, 'service_id' => 99,
'moved_to_id' => 99, 'moved_to_id' => 99,
'namespace_id' => 99, 'namespace_id' => 99,
...@@ -27,8 +29,9 @@ describe Gitlab::ImportExport::AttributeCleaner, lib: true do ...@@ -27,8 +29,9 @@ describe Gitlab::ImportExport::AttributeCleaner, lib: true do
end end
it 'removes unwanted attributes from the hash' do 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
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' require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
describe 'restore project tree' do describe 'restore project tree' do
...@@ -175,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -175,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
end end
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 end
end end
require 'spec_helper' require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::VersionChecker, services: true do describe Gitlab::ImportExport::VersionChecker, services: true do
describe 'bundle a project Git repo' do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
describe 'bundle a project Git repo' do
let(:version) { Gitlab::ImportExport.version } let(:version) { Gitlab::ImportExport.version }
before do before do
...@@ -27,4 +29,16 @@ describe Gitlab::ImportExport::VersionChecker, services: true do ...@@ -27,4 +29,16 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
end end
end 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 end
require 'spec_helper' require 'spec_helper'
describe JiraService, models: true do describe JiraService, models: true do
include Gitlab::Routing.url_helpers
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
...@@ -66,6 +68,29 @@ describe JiraService, models: true do ...@@ -66,6 +68,29 @@ describe JiraService, models: true do
).once ).once
end end
it "references the GitLab commit/merge request" do
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
).once
end
it "references the GitLab commit/merge request (relative URL)" do
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
allow(JiraService).to receive(:default_url_options) do
{ script_name: '/gitlab' }
end
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
).once
end
it "calls the api with jira_issue_transition_id" do it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id' @jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
......
...@@ -6,6 +6,7 @@ describe ProjectPolicy, models: true do ...@@ -6,6 +6,7 @@ describe ProjectPolicy, models: true do
let(:dev) { create(:user) } let(:dev) { create(:user) }
let(:master) { create(:user) } let(:master) { create(:user) }
let(:owner) { create(:user) } let(:owner) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
let(:guest_permissions) do let(:guest_permissions) do
...@@ -152,6 +153,19 @@ describe ProjectPolicy, models: true do ...@@ -152,6 +153,19 @@ describe ProjectPolicy, models: true do
context 'owner' do context 'owner' do
let(:current_user) { owner } let(:current_user) { owner }
it do
is_expected.to include(*guest_permissions)
is_expected.to include(*reporter_permissions)
is_expected.to include(*team_member_reporter_permissions)
is_expected.to include(*developer_permissions)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
end
context 'admin' do
let(:current_user) { admin }
it do it do
is_expected.to include(*guest_permissions) is_expected.to include(*guest_permissions)
is_expected.to include(*reporter_permissions) is_expected.to include(*reporter_permissions)
......
require 'spec_helper'
require 'rake'
RSpec.configure do |config|
config.include RakeHelpers
# Redirect stdout so specs don't have so much noise
config.before(:all) do
$stdout = StringIO.new
Rake.application.rake_require 'tasks/gitlab/task_helpers'
Rake::Task.define_task :environment
end
# Reset stdout
config.after(:all) do
$stdout = STDOUT
end
end
...@@ -88,6 +88,7 @@ describe API::API, 'ProjectHooks', api: true do ...@@ -88,6 +88,7 @@ describe API::API, 'ProjectHooks', api: true do
expect do expect do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
end.to change {project.hooks.count}.by(1) end.to change {project.hooks.count}.by(1)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['url']).to eq('http://example.com') expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true) expect(json_response['issues_events']).to eq(true)
...@@ -99,6 +100,24 @@ describe API::API, 'ProjectHooks', api: true do ...@@ -99,6 +100,24 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['pipeline_events']).to eq(false) expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true)
expect(json_response).not_to include('token')
end
it "adds the token without including it in the response" do
token = "secret token"
expect do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
end.to change {project.hooks.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response["url"]).to eq("http://example.com")
expect(json_response).not_to include("token")
hook = project.hooks.find(json_response["id"])
expect(hook.url).to eq("http://example.com")
expect(hook.token).to eq(token)
end end
it "returns a 400 error if url not given" do it "returns a 400 error if url not given" do
...@@ -129,6 +148,19 @@ describe API::API, 'ProjectHooks', api: true do ...@@ -129,6 +148,19 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end end
it "adds the token without including it in the response" do
token = "secret token"
put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
expect(response).to have_http_status(200)
expect(json_response["url"]).to eq("http://example.org")
expect(json_response).not_to include("token")
expect(hook.reload.url).to eq("http://example.org")
expect(hook.reload.token).to eq(token)
end
it "returns 404 error if hook id not found" do it "returns 404 error if hook id not found" do
put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
......
...@@ -13,6 +13,7 @@ describe API::API, api: true do ...@@ -13,6 +13,7 @@ describe API::API, api: true do
context "when no user" do context "when no user" do
it "returns authentication error" do it "returns authentication error" do
get api("/hooks") get api("/hooks")
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
end end
...@@ -20,6 +21,7 @@ describe API::API, api: true do ...@@ -20,6 +21,7 @@ describe API::API, api: true do
context "when not an admin" do context "when not an admin" do
it "returns forbidden error" do it "returns forbidden error" do
get api("/hooks", user) get api("/hooks", user)
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
end end
...@@ -27,9 +29,12 @@ describe API::API, api: true do ...@@ -27,9 +29,12 @@ describe API::API, api: true do
context "when authenticated as admin" do context "when authenticated as admin" do
it "returns an array of hooks" do it "returns an array of hooks" do
get api("/hooks", admin) get api("/hooks", admin)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url) 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 end
end end
...@@ -43,6 +48,7 @@ describe API::API, api: true do ...@@ -43,6 +48,7 @@ describe API::API, api: true do
it "responds with 400 if url not given" do it "responds with 400 if url not given" do
post api("/hooks", admin) post api("/hooks", admin)
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
...@@ -51,6 +57,14 @@ describe API::API, api: true do ...@@ -51,6 +57,14 @@ describe API::API, api: true do
post api("/hooks", admin) post api("/hooks", admin)
end.not_to change { SystemHook.count } end.not_to change { SystemHook.count }
end 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 end
describe "GET /hooks/:id" do describe "GET /hooks/:id" do
......
...@@ -284,7 +284,17 @@ describe 'Git LFS API and storage' do ...@@ -284,7 +284,17 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
shared_examples 'can download LFS only from own projects' do shared_examples 'can download LFS only from own projects' do
context 'for own project' do context 'for owned project' do
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'for member of project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_permissions) do let(:update_permissions) do
......
...@@ -245,6 +245,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -245,6 +245,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it_behaves_like 'a pullable' it_behaves_like 'a pullable'
end end
context 'when you are owner' do
let(:project) { create(:empty_project, namespace: current_user.namespace) }
it_behaves_like 'a pullable'
end
end end
context 'for private' do context 'for private' do
...@@ -266,6 +272,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -266,6 +272,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it_behaves_like 'a pullable' it_behaves_like 'a pullable'
end end
context 'when you are owner' do
let(:project) { create(:empty_project, namespace: current_user.namespace) }
it_behaves_like 'a pullable'
end
end end
end end
end end
...@@ -276,6 +288,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -276,6 +288,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end end
context 'disallow for all' do context 'disallow for all' do
context 'when you are member' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
before do before do
...@@ -284,6 +297,13 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -284,6 +297,13 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
context 'when you are owner' do
let(:project) { create(:empty_project, :public, namespace: current_user.namespace) }
it_behaves_like 'an inaccessible'
end
end
end end
end end
......
...@@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do ...@@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do
before do before do
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
allow(project).to receive(:commit).and_return(commit_1)
allow(project).to receive(:commit).and_return(commit_2)
end end
describe 'execute' do describe 'execute' do
...@@ -193,5 +195,52 @@ describe MergeRequests::BuildService, services: true do ...@@ -193,5 +195,52 @@ describe MergeRequests::BuildService, services: true do
end end
end end
end end
context 'source branch does not exist' do
before do
allow(project).to receive(:commit).with(source_branch).and_return(nil)
allow(project).to receive(:commit).with(target_branch).and_return(commit_1)
end
it 'forbids the merge request from being created' do
expect(merge_request.can_be_created).to eq(false)
end
it 'adds an error message to the merge request' do
expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist')
end
end
context 'target branch does not exist' do
before do
allow(project).to receive(:commit).with(source_branch).and_return(commit_1)
allow(project).to receive(:commit).with(target_branch).and_return(nil)
end
it 'forbids the merge request from being created' do
expect(merge_request.can_be_created).to eq(false)
end
it 'adds an error message to the merge request' do
expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist')
end
end
context 'both source and target branches do not exist' do
before do
allow(project).to receive(:commit).and_return(nil)
end
it 'forbids the merge request from being created' do
expect(merge_request.can_be_created).to eq(false)
end
it 'adds both error messages to the merge request' do
expect(merge_request.errors).to contain_exactly(
'Source branch "feature-branch" does not exist',
'Target branch "master" does not exist'
)
end
end
end 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
module RakeHelpers
def run_rake_task(task_name)
Rake::Task[task_name].reenable
Rake.application.invoke_task task_name
end
def stub_warn_user_is_not_gitlab
allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab)
end
end
require 'rake_helper'
describe 'gitlab:shell rake tasks' do
before do
Rake.application.rake_require 'tasks/gitlab/shell'
stub_warn_user_is_not_gitlab
end
describe 'install task' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
run_rake_task('gitlab:shell:install')
end
end
describe 'create_hooks task' do
it 'calls gitlab-shell bin/create_hooks' do
expect_any_instance_of(Object).to receive(:system)
.with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args)
run_rake_task('gitlab:shell:create_hooks')
end
end
end
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
* @license MIT * @license MIT
*/ */
(function sortableModule(factory) {
(function (factory) {
"use strict"; "use strict";
if (typeof define === "function" && define.amd) { if (typeof define === "function" && define.amd) {
...@@ -15,15 +14,22 @@ ...@@ -15,15 +14,22 @@
module.exports = factory(); module.exports = factory();
} }
else if (typeof Package !== "undefined") { else if (typeof Package !== "undefined") {
//noinspection JSUnresolvedVariable
Sortable = factory(); // export for Meteor.js Sortable = factory(); // export for Meteor.js
} }
else { else {
/* jshint sub:true */ /* jshint sub:true */
window["Sortable"] = factory(); window["Sortable"] = factory();
} }
})(function () { })(function sortableFactory() {
"use strict"; "use strict";
if (typeof window == "undefined" || !window.document) {
return function sortableError() {
throw new Error("Sortable.js requires a window with a document");
};
}
var dragEl, var dragEl,
parentEl, parentEl,
ghostEl, ghostEl,
...@@ -33,6 +39,7 @@ ...@@ -33,6 +39,7 @@
scrollEl, scrollEl,
scrollParentEl, scrollParentEl,
scrollCustomFn,
lastEl, lastEl,
lastCSS, lastCSS,
...@@ -42,6 +49,8 @@ ...@@ -42,6 +49,8 @@
newIndex, newIndex,
activeGroup, activeGroup,
putSortable,
autoScroll = {}, autoScroll = {},
tapEvt, tapEvt,
...@@ -58,8 +67,15 @@ ...@@ -58,8 +67,15 @@
document = win.document, document = win.document,
parseInt = win.parseInt, parseInt = win.parseInt,
$ = win.jQuery || win.Zepto,
Polymer = win.Polymer,
supportDraggable = !!('draggable' in document.createElement('div')), supportDraggable = !!('draggable' in document.createElement('div')),
supportCssPointerEvents = (function (el) { supportCssPointerEvents = (function (el) {
// false when IE11
if (!!navigator.userAgent.match(/Trident.*rv[ :]?11\./)) {
return false;
}
el = document.createElement('x'); el = document.createElement('x');
el.style.cssText = 'pointer-events:auto'; el.style.cssText = 'pointer-events:auto';
return el.style.pointerEvents === 'auto'; return el.style.pointerEvents === 'auto';
...@@ -88,13 +104,17 @@ ...@@ -88,13 +104,17 @@
winHeight = window.innerHeight, winHeight = window.innerHeight,
vx, vx,
vy vy,
scrollOffsetX,
scrollOffsetY
; ;
// Delect scrollEl // Delect scrollEl
if (scrollParentEl !== rootEl) { if (scrollParentEl !== rootEl) {
scrollEl = options.scroll; scrollEl = options.scroll;
scrollParentEl = rootEl; scrollParentEl = rootEl;
scrollCustomFn = options.scrollFn;
if (scrollEl === true) { if (scrollEl === true) {
scrollEl = rootEl; scrollEl = rootEl;
...@@ -136,11 +156,18 @@ ...@@ -136,11 +156,18 @@
if (el) { if (el) {
autoScroll.pid = setInterval(function () { autoScroll.pid = setInterval(function () {
scrollOffsetY = vy ? vy * speed : 0;
scrollOffsetX = vx ? vx * speed : 0;
if ('function' === typeof(scrollCustomFn)) {
return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
}
if (el === win) { if (el === win) {
win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed); win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY);
} else { } else {
vy && (el.scrollTop += vy * speed); el.scrollTop += scrollOffsetY;
vx && (el.scrollLeft += vx * speed); el.scrollLeft += scrollOffsetX;
} }
}, 24); }, 24);
} }
...@@ -149,19 +176,39 @@ ...@@ -149,19 +176,39 @@
}, 30), }, 30),
_prepareGroup = function (options) { _prepareGroup = function (options) {
var group = options.group; function toFn(value, pull) {
if (value === void 0 || value === true) {
value = group.name;
}
if (!group || typeof group != 'object') { if (typeof value === 'function') {
group = options.group = {name: group}; return value;
} else {
return function (to, from) {
var fromGroup = from.options.group.name;
return pull
? value
: value && (value.join
? value.indexOf(fromGroup) > -1
: (fromGroup == value)
);
};
} }
}
var group = {};
var originalGroup = options.group;
['pull', 'put'].forEach(function (key) { if (!originalGroup || typeof originalGroup != 'object') {
if (!(key in group)) { originalGroup = {name: originalGroup};
group[key] = true;
} }
});
options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' '; group.name = originalGroup.name;
group.checkPull = toFn(originalGroup.pull, true);
group.checkPut = toFn(originalGroup.put);
options.group = group;
} }
; ;
...@@ -198,6 +245,7 @@ ...@@ -198,6 +245,7 @@
draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
ghostClass: 'sortable-ghost', ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen', chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
ignore: 'a, img', ignore: 'a, img',
filter: null, filter: null,
animation: 0, animation: 0,
...@@ -211,7 +259,8 @@ ...@@ -211,7 +259,8 @@
forceFallback: false, forceFallback: false,
fallbackClass: 'sortable-fallback', fallbackClass: 'sortable-fallback',
fallbackOnBody: false, fallbackOnBody: false,
fallbackTolerance: 0 fallbackTolerance: 0,
fallbackOffset: {x: 0, y: 0}
}; };
...@@ -224,7 +273,7 @@ ...@@ -224,7 +273,7 @@
// Bind all private methods // Bind all private methods
for (var fn in this) { for (var fn in this) {
if (fn.charAt(0) === '_') { if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
this[fn] = this[fn].bind(this); this[fn] = this[fn].bind(this);
} }
} }
...@@ -258,7 +307,7 @@ ...@@ -258,7 +307,7 @@
type = evt.type, type = evt.type,
touch = evt.touches && evt.touches[0], touch = evt.touches && evt.touches[0],
target = (touch || evt).target, target = (touch || evt).target,
originalTarget = target, originalTarget = evt.target.shadowRoot && evt.path[0] || target,
filter = options.filter, filter = options.filter,
startIndex; startIndex;
...@@ -271,13 +320,13 @@ ...@@ -271,13 +320,13 @@
return; // only left button or enabled return; // only left button or enabled
} }
target = _closest(target, options.draggable, el); if (options.handle && !_closest(originalTarget, options.handle, el)) {
if (!target) {
return; return;
} }
if (options.handle && !_closest(originalTarget, options.handle, el)) { target = _closest(target, options.draggable, el);
if (!target) {
return; return;
} }
...@@ -332,16 +381,18 @@ ...@@ -332,16 +381,18 @@
this._lastX = (touch || evt).clientX; this._lastX = (touch || evt).clientX;
this._lastY = (touch || evt).clientY; this._lastY = (touch || evt).clientY;
dragEl.style['will-change'] = 'transform';
dragStartFn = function () { dragStartFn = function () {
// Delayed drag has been triggered // Delayed drag has been triggered
// we can re-enable the events: touchmove/mousemove // we can re-enable the events: touchmove/mousemove
_this._disableDelayedDrag(); _this._disableDelayedDrag();
// Make the element draggable // Make the element draggable
dragEl.draggable = true; dragEl.draggable = _this.nativeDraggable;
// Chosen item // Chosen item
_toggleClass(dragEl, _this.options.chosenClass, true); _toggleClass(dragEl, options.chosenClass, true);
// Bind the events: dragstart/dragend // Bind the events: dragstart/dragend
_this._triggerDragStart(touch); _this._triggerDragStart(touch);
...@@ -408,7 +459,10 @@ ...@@ -408,7 +459,10 @@
try { try {
if (document.selection) { if (document.selection) {
// Timeout neccessary for IE9
setTimeout(function () {
document.selection.empty(); document.selection.empty();
});
} else { } else {
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
} }
...@@ -418,8 +472,11 @@ ...@@ -418,8 +472,11 @@
_dragStarted: function () { _dragStarted: function () {
if (rootEl && dragEl) { if (rootEl && dragEl) {
var options = this.options;
// Apply effect // Apply effect
_toggleClass(dragEl, this.options.ghostClass, true); _toggleClass(dragEl, options.ghostClass, true);
_toggleClass(dragEl, options.dragClass, false);
Sortable.active = this; Sortable.active = this;
...@@ -443,12 +500,11 @@ ...@@ -443,12 +500,11 @@
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY), var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
parent = target, parent = target,
groupName = ' ' + this.options.group.name + '',
i = touchDragOverListeners.length; i = touchDragOverListeners.length;
if (parent) { if (parent) {
do { do {
if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) { if (parent[expando]) {
while (i--) { while (i--) {
touchDragOverListeners[i]({ touchDragOverListeners[i]({
clientX: touchEvt.clientX, clientX: touchEvt.clientX,
...@@ -478,9 +534,10 @@ ...@@ -478,9 +534,10 @@
if (tapEvt) { if (tapEvt) {
var options = this.options, var options = this.options,
fallbackTolerance = options.fallbackTolerance, fallbackTolerance = options.fallbackTolerance,
fallbackOffset = options.fallbackOffset,
touch = evt.touches ? evt.touches[0] : evt, touch = evt.touches ? evt.touches[0] : evt,
dx = touch.clientX - tapEvt.clientX, dx = (touch.clientX - tapEvt.clientX) + fallbackOffset.x,
dy = touch.clientY - tapEvt.clientY, dy = (touch.clientY - tapEvt.clientY) + fallbackOffset.y,
translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
// only set the status to dragging, when we are actually dragging // only set the status to dragging, when we are actually dragging
...@@ -520,6 +577,7 @@ ...@@ -520,6 +577,7 @@
_toggleClass(ghostEl, options.ghostClass, false); _toggleClass(ghostEl, options.ghostClass, false);
_toggleClass(ghostEl, options.fallbackClass, true); _toggleClass(ghostEl, options.fallbackClass, true);
_toggleClass(ghostEl, options.dragClass, true);
_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
...@@ -545,13 +603,15 @@ ...@@ -545,13 +603,15 @@
this._offUpEvents(); this._offUpEvents();
if (activeGroup.pull == 'clone') { if (activeGroup.checkPull(this, this, dragEl, evt) == 'clone') {
cloneEl = dragEl.cloneNode(true); cloneEl = _clone(dragEl);
_css(cloneEl, 'display', 'none'); _css(cloneEl, 'display', 'none');
rootEl.insertBefore(cloneEl, dragEl); rootEl.insertBefore(cloneEl, dragEl);
_dispatchEvent(this, rootEl, 'clone', dragEl); _dispatchEvent(this, rootEl, 'clone', dragEl);
} }
_toggleClass(dragEl, options.dragClass, true);
if (useFallback) { if (useFallback) {
if (useFallback === 'touch') { if (useFallback === 'touch') {
// Bind touch events // Bind touch events
...@@ -581,10 +641,11 @@ ...@@ -581,10 +641,11 @@
var el = this.el, var el = this.el,
target, target,
dragRect, dragRect,
targetRect,
revert, revert,
options = this.options, options = this.options,
group = options.group, group = options.group,
groupPut = group.put, activeSortable = Sortable.active,
isOwner = (activeGroup === group), isOwner = (activeGroup === group),
canSort = options.sort; canSort = options.sort;
...@@ -598,9 +659,9 @@ ...@@ -598,9 +659,9 @@
if (activeGroup && !options.disabled && if (activeGroup && !options.disabled &&
(isOwner (isOwner
? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
: activeGroup.pull && groupPut && ( : (
(activeGroup.name === group.name) || // by Name putSortable === this ||
(groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array activeGroup.checkPull(this, activeSortable, dragEl, evt) && group.checkPut(this, activeSortable, dragEl, evt)
) )
) && ) &&
(evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
...@@ -614,6 +675,7 @@ ...@@ -614,6 +675,7 @@
target = _closest(evt.target, options.draggable, el); target = _closest(evt.target, options.draggable, el);
dragRect = dragEl.getBoundingClientRect(); dragRect = dragEl.getBoundingClientRect();
putSortable = this;
if (revert) { if (revert) {
_cloneHide(true); _cloneHide(true);
...@@ -633,7 +695,6 @@ ...@@ -633,7 +695,6 @@
if ((el.children.length === 0) || (el.children[0] === ghostEl) || if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
(el === evt.target) && (target = _ghostIsLast(el, evt)) (el === evt.target) && (target = _ghostIsLast(el, evt))
) { ) {
if (target) { if (target) {
if (target.animated) { if (target.animated) {
return; return;
...@@ -644,7 +705,7 @@ ...@@ -644,7 +705,7 @@
_cloneHide(isOwner); _cloneHide(isOwner);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) { if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt) !== false) {
if (!dragEl.contains(el)) { if (!dragEl.contains(el)) {
el.appendChild(dragEl); el.appendChild(dragEl);
parentEl = el; // actualization parentEl = el; // actualization
...@@ -661,9 +722,9 @@ ...@@ -661,9 +722,9 @@
lastParentCSS = _css(target.parentNode); lastParentCSS = _css(target.parentNode);
} }
targetRect = target.getBoundingClientRect();
var targetRect = target.getBoundingClientRect(), var width = targetRect.right - targetRect.left,
width = targetRect.right - targetRect.left,
height = targetRect.bottom - targetRect.top, height = targetRect.bottom - targetRect.top,
floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display) floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
|| (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0), || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
...@@ -671,7 +732,7 @@ ...@@ -671,7 +732,7 @@
isLong = (target.offsetHeight > dragEl.offsetHeight), isLong = (target.offsetHeight > dragEl.offsetHeight),
halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
nextSibling = target.nextElementSibling, nextSibling = target.nextElementSibling,
moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect), moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt),
after after
; ;
...@@ -784,6 +845,7 @@ ...@@ -784,6 +845,7 @@
} }
_disableDraggable(dragEl); _disableDraggable(dragEl);
dragEl.style['will-change'] = '';
// Remove class's // Remove class's
_toggleClass(dragEl, this.options.ghostClass, false); _toggleClass(dragEl, this.options.ghostClass, false);
...@@ -793,15 +855,16 @@ ...@@ -793,15 +855,16 @@
newIndex = _index(dragEl, options.draggable); newIndex = _index(dragEl, options.draggable);
if (newIndex >= 0) { if (newIndex >= 0) {
// drag from one list and drop into another
_dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
// Add event // Add event
_dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex); _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
// Remove event // Remove event
_dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex); _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
// drag from one list and drop into another
_dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
} }
} }
else { else {
...@@ -821,7 +884,8 @@ ...@@ -821,7 +884,8 @@
} }
if (Sortable.active) { if (Sortable.active) {
if (newIndex === null || newIndex === -1) { /* jshint eqnull:true */
if (newIndex == null || newIndex === -1) {
newIndex = oldIndex; newIndex = oldIndex;
} }
...@@ -837,7 +901,7 @@ ...@@ -837,7 +901,7 @@
this._nulling(); this._nulling();
}, },
_nulling: function () { _nulling: function() {
rootEl = rootEl =
dragEl = dragEl =
parentEl = parentEl =
...@@ -857,6 +921,7 @@ ...@@ -857,6 +921,7 @@
lastEl = lastEl =
lastCSS = lastCSS =
putSortable =
activeGroup = activeGroup =
Sortable.active = null; Sortable.active = null;
}, },
...@@ -1011,14 +1076,21 @@ ...@@ -1011,14 +1076,21 @@
if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) { if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
return el; return el;
} }
} /* jshint boss:true */
while (el !== ctx && (el = el.parentNode)); } while (el = _getParentOrHost(el));
} }
return null; return null;
} }
function _getParentOrHost(el) {
var parent = el.host;
return (parent && parent.nodeType) ? parent : el.parentNode;
}
function _globalDragOver(/**Event*/evt) { function _globalDragOver(/**Event*/evt) {
if (evt.dataTransfer) { if (evt.dataTransfer) {
evt.dataTransfer.dropEffect = 'move'; evt.dataTransfer.dropEffect = 'move';
...@@ -1094,8 +1166,10 @@ ...@@ -1094,8 +1166,10 @@
function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) { function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
sortable = (sortable || rootEl[expando]);
var evt = document.createEvent('Event'), var evt = document.createEvent('Event'),
options = (sortable || rootEl[expando]).options, options = sortable.options,
onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
evt.initEvent(name, true, true); evt.initEvent(name, true, true);
...@@ -1116,7 +1190,7 @@ ...@@ -1116,7 +1190,7 @@
} }
function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) { function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt) {
var evt, var evt,
sortable = fromEl[expando], sortable = fromEl[expando],
onMoveFn = sortable.options.onMove, onMoveFn = sortable.options.onMove,
...@@ -1135,7 +1209,7 @@ ...@@ -1135,7 +1209,7 @@
fromEl.dispatchEvent(evt); fromEl.dispatchEvent(evt);
if (onMoveFn) { if (onMoveFn) {
retVal = onMoveFn.call(sortable, evt); retVal = onMoveFn.call(sortable, evt, originalEvt);
} }
return retVal; return retVal;
...@@ -1157,7 +1231,12 @@ ...@@ -1157,7 +1231,12 @@
var lastEl = el.lastElementChild, var lastEl = el.lastElementChild,
rect = lastEl.getBoundingClientRect(); rect = lastEl.getBoundingClientRect();
return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta // 5 — min delta
// abs — нельзя добавлять, а то глюки при наведении сверху
return (
(evt.clientY - (rect.top + rect.height) > 5) ||
(evt.clientX - (rect.right + rect.width) > 5)
) && lastEl;
} }
...@@ -1251,6 +1330,15 @@ ...@@ -1251,6 +1330,15 @@
return dst; return dst;
} }
function _clone(el) {
return $
? $(el).clone(true)[0]
: (Polymer && Polymer.dom
? Polymer.dom(el).cloneNode(true)
: el.cloneNode(true)
);
}
// Export utils // Export utils
Sortable.utils = { Sortable.utils = {
...@@ -1265,6 +1353,7 @@ ...@@ -1265,6 +1353,7 @@
throttle: _throttle, throttle: _throttle,
closest: _closest, closest: _closest,
toggleClass: _toggleClass, toggleClass: _toggleClass,
clone: _clone,
index: _index index: _index
}; };
......
...@@ -39,3 +39,6 @@ captures/ ...@@ -39,3 +39,6 @@ captures/
# Keystore files # Keystore files
*.jks *.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
...@@ -7,6 +7,11 @@ ...@@ -7,6 +7,11 @@
*.obj *.obj
*.elf *.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers # Precompiled Headers
*.gch *.gch
*.pch *.pch
...@@ -34,3 +39,13 @@ ...@@ -34,3 +39,13 @@
# Debug files # Debug files
*.dSYM/ *.dSYM/
*.su *.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
modules.order
Module.symvers
Mkfile.old
dkms.conf
.architect .architect
bootstrap.css
bootstrap.js
bootstrap.json bootstrap.json
bootstrap.jsonp
build/ build/
classic.json
classic.jsonp
ext/ ext/
modern.json
modern.jsonp
resources/sass/.sass-cache/
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
# User-specific stuff: # User-specific stuff:
.idea/workspace.xml .idea/workspace.xml
.idea/tasks.xml .idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files: # Sensitive or high-churn files:
.idea/dataSources.ids .idea/dataSources.ids
......
Creative Commons Legal Code
CC0 1.0 Universal 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 Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator exclusive Copyright and Related Rights (defined below) upon the creator and
and subsequent owner(s) (each and all, an "owner") of an original work of subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work"). authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for Certain owners wish to permanently relinquish those rights to a Work for the
the purpose of contributing to a commons of creative, cultural and purpose of contributing to a commons of creative, cultural and scientific
scientific works ("Commons") that the public can reliably and without fear works ("Commons") that the public can reliably and without fear of later
of later claims of infringement build upon, modify, incorporate in other claims of infringement build upon, modify, incorporate in other works, reuse
works, reuse and redistribute as freely as possible in any form whatsoever and redistribute as freely as possible in any form whatsoever and for any
and for any purposes, including without limitation commercial purposes. purposes, including without limitation commercial purposes. These owners may
These owners may contribute to the Commons to promote the ideal of a free contribute to the Commons to promote the ideal of a free culture and the
culture and the further production of creative, cultural and scientific further production of creative, cultural and scientific works, or to gain
works, or to gain reputation or greater distribution for their Work in reputation or greater distribution for their Work in part through the use and
part through the use and efforts of others. efforts of others.
For these and/or other purposes and motivations, and without any For these and/or other purposes and motivations, and without any expectation
expectation of additional consideration or compensation, the person of additional consideration or compensation, the person associating CC0 with a
associating CC0 with a Work (the "Affirmer"), to the extent that he or she Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
is an owner of Copyright and Related Rights in the Work, voluntarily and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
elects to apply CC0 to the Work and publicly distribute the Work under its and publicly distribute the Work under its terms, with knowledge of his or her
terms, with knowledge of his or her Copyright and Related Rights in the Copyright and Related Rights in the Work and the meaning and intended legal
Work and the meaning and intended legal effect of CC0 on those rights. effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be 1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not Related Rights"). Copyright and Related Rights include, but are not limited
limited to, the following: to, the following:
i. the right to reproduce, adapt, distribute, perform, display, communicate,
and translate a Work;
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); 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; 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, iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below; subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work; 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 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 European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation protection of databases, and under any national implementation thereof,
thereof, including any amended or successor version of such including any amended or successor version of such directive); and
directive); and
vii. other similar, equivalent or corresponding rights throughout the vii. other similar, equivalent or corresponding rights throughout the world
world based on applicable law or treaty, and any national based on applicable law or treaty, and any national implementations thereof.
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
2. Waiver. To the greatest extent permitted by, but not in contravention applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
of, applicable law, Affirmer hereby overtly, fully, permanently, unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
irrevocably and unconditionally waives, abandons, and surrenders all of and Related Rights and associated claims and causes of action, whether now
Affirmer's Copyright and Related Rights and associated claims and causes known or unknown (including existing as well as future claims and causes of
of action, whether now known or unknown (including existing as well as action), in the Work (i) in all territories worldwide, (ii) for the maximum
future claims and causes of action), in the Work (i) in all territories duration provided by applicable law or treaty (including future time
worldwide, (ii) for the maximum duration provided by applicable law or extensions), (iii) in any current or future medium and for any number of
treaty (including future time extensions), (iii) in any current or future copies, and (iv) for any purpose whatsoever, including without limitation
medium and for any number of copies, and (iv) for any purpose whatsoever, commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
including without limitation commercial, advertising or promotional the Waiver for the benefit of each member of the public at large and to the
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each detriment of Affirmer's heirs and successors, fully intending that such Waiver
member of the public at large and to the detriment of Affirmer's heirs and shall not be subject to revocation, rescission, cancellation, termination, or
successors, fully intending that such Waiver shall not be subject to any other legal or equitable action to disrupt the quiet enjoyment of the Work
revocation, rescission, cancellation, termination, or any other legal or by the public as contemplated by Affirmer's express Statement of Purpose.
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
3. Public License Fallback. Should any part of the Waiver for any reason shall be preserved to the maximum extent permitted taking into account
be judged legally invalid or ineffective under applicable law, then the Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
Waiver shall be preserved to the maximum extent permitted taking into is so judged Affirmer hereby grants to each affected person a royalty-free,
account Affirmer's express Statement of Purpose. In addition, to the non transferable, non sublicensable, non exclusive, irrevocable and
extent the Waiver is so judged Affirmer hereby grants to each affected unconditional license to exercise Affirmer's Copyright and Related Rights in
person a royalty-free, non transferable, non sublicensable, non exclusive, the Work (i) in all territories worldwide, (ii) for the maximum duration
irrevocable and unconditional license to exercise Affirmer's Copyright and provided by applicable law or treaty (including future time extensions), (iii)
Related Rights in the Work (i) in all territories worldwide, (ii) for the in any current or future medium and for any number of copies, and (iv) for any
maximum duration provided by applicable law or treaty (including future purpose whatsoever, including without limitation commercial, advertising or
time extensions), (iii) in any current or future medium and for any number promotional purposes (the "License"). The License shall be deemed effective as
of copies, and (iv) for any purpose whatsoever, including without of the date CC0 was applied by Affirmer to the Work. Should any part of the
limitation commercial, advertising or promotional purposes (the License for any reason be judged legally invalid or ineffective under
"License"). The License shall be deemed effective as of the date CC0 was applicable law, such partial invalidity or ineffectiveness shall not
applied by Affirmer to the Work. Should any part of the License for any invalidate the remainder of the License, and in such case Affirmer hereby
reason be judged legally invalid or ineffective under applicable law, such affirms that he or she will not (i) exercise any of his or her remaining
partial invalidity or ineffectiveness shall not invalidate the remainder Copyright and Related Rights in the Work or (ii) assert any associated claims
of the License, and in such case Affirmer hereby affirms that he or she and causes of action with respect to the Work, in either case contrary to
will not (i) exercise any of his or her remaining Copyright and Related Affirmer's express Statement of Purpose.
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. 4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned, a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document. 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, b. Affirmer offers the Work as-is and makes no representations or warranties
statutory or otherwise, including without limitation warranties of of any kind concerning the Work, express, implied, statutory or otherwise,
title, merchantability, fitness for a particular purpose, non including without limitation warranties of title, merchantability, fitness
infringement, or the absence of latent or other defects, accuracy, or for a particular purpose, non infringement, or the absence of latent or
the present or absence of errors, whether or not discoverable, all to other defects, accuracy, or the present or absence of errors, whether or not
the greatest extent permissible under applicable law. discoverable, all to the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without that may apply to the Work or any use thereof, including without limitation
limitation any person's Copyright and Related Rights in the Work. any person's Copyright and Related Rights in the Work. Further, Affirmer
Further, Affirmer disclaims responsibility for obtaining any necessary disclaims responsibility for obtaining any necessary consents, permissions
consents, permissions or other rights required for any use of the or other rights required for any use of the Work.
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to party to this document and has no duty or obligation with respect to this
this CC0 or use of the Work. CC0 or use of the Work.
For more information, please see
<http://creativecommons.org/publicdomain/zero/1.0/>
...@@ -7,6 +7,7 @@ app/storage/ ...@@ -7,6 +7,7 @@ app/storage/
# Laravel 5 & Lumen specific # Laravel 5 & Lumen specific
bootstrap/cache/ bootstrap/cache/
public/storage
.env.*.php .env.*.php
.env.php .env.php
.env .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/ output/
# Temporary file directory # Temporary file directory
......
...@@ -11,3 +11,10 @@ system/cache/ ...@@ -11,3 +11,10 @@ system/cache/
system/logs/ system/logs/
system/storage/ system/storage/
# vQmod log files
vqmod/logs/*
# vQmod cache files
vqmod/vqcache/*
vqmod/checked.cache
vqmod/mods.cache
...@@ -66,7 +66,7 @@ docs/_build/ ...@@ -66,7 +66,7 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
# IPython Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
# pyenv # pyenv
......
...@@ -5,3 +5,6 @@ ...@@ -5,3 +5,6 @@
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # 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 # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
...@@ -61,6 +61,15 @@ acs-*.bib ...@@ -61,6 +61,15 @@ acs-*.bib
# fixme # fixme
*.lox *.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 #(r)(e)ledmac/(r)(e)ledpar
*.end *.end
*.?end *.?end
......
# Visual Studio 2015 user specific files # Visual Studio 2015 user specific files
.vs/ .vs/
# Visual Studio 2015 database file
*.VC.db
# Compiled Object files # Compiled Object files
*.slo *.slo
*.lo *.lo
......
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files # User-specific files
*.suo *.suo
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
*.vcxproj.filters
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
...@@ -44,6 +47,7 @@ dlldata.c ...@@ -44,6 +47,7 @@ dlldata.c
project.lock.json project.lock.json
project.fragment.lock.json project.fragment.lock.json
artifacts/ artifacts/
Properties/launchSettings.json
*_i.c *_i.c
*_p.c *_p.c
...@@ -238,6 +242,9 @@ FakesAssemblies/ ...@@ -238,6 +242,9 @@ FakesAssemblies/
# Visual Studio 6 workspace options file # Visual Studio 6 workspace options file
*.opt *.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output # Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts **/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/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