Commit 37ca458f authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'nt/ce-to-ee-thursday' into 'master'

CE upstream: Thursday

Closes #2246

See merge request !1749
parents 862d6f49 10d8f891
...@@ -12,6 +12,7 @@ variables: ...@@ -12,6 +12,7 @@ variables:
NODE_ENV: "test" NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
GIT_DEPTH: "20" GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
......
...@@ -26,6 +26,7 @@ logs, and code as it's very hard to read otherwise.) ...@@ -26,6 +26,7 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info #### Results of GitLab environment info
<details> <details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`) `sudo gitlab-rake gitlab:env:info`)
...@@ -33,11 +34,13 @@ logs, and code as it's very hard to read otherwise.) ...@@ -33,11 +34,13 @@ logs, and code as it's very hard to read otherwise.)
(For installations from source run and paste the output of: (For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
</pre>
</details> </details>
#### Results of GitLab application Check #### Results of GitLab application Check
<details> <details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`) `sudo gitlab-rake gitlab:check SANITIZE=true`)
...@@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.) ...@@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing) (we will only investigate if the tests are passing)
</pre>
</details> </details>
### Possible fixes ### Possible fixes
......
...@@ -154,7 +154,7 @@ gem 'after_commit_queue', '~> 1.3.0' ...@@ -154,7 +154,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 4.2.7' gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.4.4' gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
...@@ -304,6 +304,7 @@ group :development, :test do ...@@ -304,6 +304,7 @@ group :development, :test do
gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -465,7 +465,7 @@ GEM ...@@ -465,7 +465,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.4) mail (2.6.5)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.15.0)
...@@ -639,7 +639,7 @@ GEM ...@@ -639,7 +639,7 @@ GEM
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.2.2) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
...@@ -695,6 +695,7 @@ GEM ...@@ -695,6 +695,7 @@ GEM
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-set (0.1.3)
rspec-support (3.5.0) rspec-support (3.5.0)
rspec_profiling (0.0.5) rspec_profiling (0.0.5)
activerecord activerecord
...@@ -752,11 +753,11 @@ GEM ...@@ -752,11 +753,11 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (4.2.10) sidekiq (5.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.3, >= 3.3.3)
sidekiq-cron (0.4.4) sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24) rufus-scheduler (>= 2.0.24)
...@@ -1034,6 +1035,7 @@ DEPENDENCIES ...@@ -1034,6 +1035,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.47.1) rubocop (~> 0.47.1)
rubocop-rspec (~> 1.15.0) rubocop-rspec (~> 1.15.0)
...@@ -1050,7 +1052,7 @@ DEPENDENCIES ...@@ -1050,7 +1052,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7) sidekiq (~> 5.0)
sidekiq-cron (~> 0.4.4) sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4) sidekiq-limit_fetch (~> 3.4)
simplecov (~> 0.14.0) simplecov (~> 0.14.0)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ee/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ee/pipelines) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ee/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ee/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Test coverage ## Test coverage
......
...@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', { ...@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
...@@ -81,13 +81,14 @@ class FilteredSearchManager { ...@@ -81,13 +81,14 @@ class FilteredSearchManager {
this.checkForEnterWrapper = this.checkForEnter.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this); this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.removeTokenWrapper = this.removeToken.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
...@@ -100,12 +101,13 @@ class FilteredSearchManager { ...@@ -100,12 +101,13 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
...@@ -121,12 +123,13 @@ class FilteredSearchManager { ...@@ -121,12 +123,13 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
...@@ -201,14 +204,28 @@ class FilteredSearchManager { ...@@ -201,14 +204,28 @@ class FilteredSearchManager {
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
const removeButtonSelected = e.target.closest('.remove-token');
if (button) { if (!removeButtonSelected && button) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button); gl.FilteredSearchVisualTokens.selectToken(button);
} }
} }
removeToken(e) {
const removeButtonSelected = e.target.closest('.remove-token');
if (removeButtonSelected) {
e.preventDefault();
e.stopPropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
this.removeSelectedToken();
}
}
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = this.container.querySelector('.filtered-search-box'); const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
...@@ -256,14 +273,19 @@ class FilteredSearchManager { ...@@ -256,14 +273,19 @@ class FilteredSearchManager {
} }
} }
removeSelectedToken(e) { removeSelectedTokenKeydown(e) {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
this.removeSelectedToken();
}
}
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken(); gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder(); this.handleInputPlaceholder();
this.toggleClearSearchButton(); this.toggleClearSearchButton();
} this.dropdownManager.updateCurrentDropdownOffset();
} }
onClearSearch(e) { onClearSearch(e) {
......
...@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens { ...@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
} }
static selectToken(tokenButton) { static selectToken(tokenButton, forceSelection = false) {
const selected = tokenButton.classList.contains('selected'); const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens(); FilteredSearchVisualTokens.unselectTokens();
if (!selected) { if (!selected || forceSelection) {
tokenButton.classList.add('selected'); tokenButton.classList.add('selected');
} }
} }
...@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens { ...@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
return ` return `
<div class="selectable" role="button"> <div class="selectable" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value-container">
<div class="value"></div> <div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div> </div>
`; `;
} }
...@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens { ...@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
if (value) { if (value) {
const button = lastVisualToken.querySelector('.selectable'); const button = lastVisualToken.querySelector('.selectable');
button.removeChild(value); const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer);
lastVisualToken.innerHTML = button.innerHTML; lastVisualToken.innerHTML = button.innerHTML;
} else { } else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji'; import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
// Creates the variables for setting up GFM auto-completion // Creates the variables for setting up GFM auto-completion
window.gl = window.gl || {}; window.gl = window.gl || {};
...@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = { ...@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = {
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert, beforeInsert: this.DefaultOptions.beforeInsert,
filter: this.DefaultOptions.filter filter: this.DefaultOptions.filter,
matcher: (flag, subtext) => {
const relevantText = subtext.trim().split(/\s/).pop();
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
const match = regexp.exec(relevantText);
return match && match.length ? match[1] : null;
}
} }
}); });
// Team Members // Team Members
......
/**
* Regexp utility for the convenience of working with regular expressions.
*
*/
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
// Unicode 6.1
const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
export default { unicodeLetters };
...@@ -28,7 +28,9 @@ export default class MiniPipelineGraph { ...@@ -28,7 +28,9 @@ export default class MiniPipelineGraph {
* All dropdown events are fired at the .dropdown-menu's parent element. * All dropdown events are fired at the .dropdown-menu's parent element.
*/ */
bindEvents() { bindEvents() {
$(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); $(document)
.off('shown.bs.dropdown', this.container)
.on('shown.bs.dropdown', this.container, this.getBuildsList);
} }
/** /**
...@@ -91,6 +93,9 @@ export default class MiniPipelineGraph { ...@@ -91,6 +93,9 @@ export default class MiniPipelineGraph {
}, },
error: () => { error: () => {
this.toggleLoading(button); this.toggleLoading(button);
if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle');
}
new Flash('An error occurred while fetching the builds.', 'alert'); new Flash('An error occurred while fetching the builds.', 'alert');
}, },
}); });
......
...@@ -2,13 +2,6 @@ ...@@ -2,13 +2,6 @@
import StatusIconEntityMap from '../../ci_status_icons'; import StatusIconEntityMap from '../../ci_status_icons';
export default { export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: { props: {
stage: { stage: {
type: Object, type: Object,
...@@ -16,6 +9,13 @@ export default { ...@@ -16,6 +9,13 @@ export default {
}, },
}, },
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
updated() { updated() {
if (this.builds) { if (this.builds) {
this.stopDropdownClickPropagation(); this.stopDropdownClickPropagation();
...@@ -31,7 +31,13 @@ export default { ...@@ -31,7 +31,13 @@ export default {
return this.$http.get(this.stage.dropdown_path) return this.$http.get(this.stage.dropdown_path)
.then((response) => { .then((response) => {
this.builds = JSON.parse(response.body).html; this.builds = JSON.parse(response.body).html;
}, () => { })
.catch(() => {
// If dropdown is opened we'll close it.
if (this.$el.classList.contains('open')) {
$(this.$refs.dropdown).dropdown('toggle');
}
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
return flash; return flash;
}); });
...@@ -46,7 +52,8 @@ export default { ...@@ -46,7 +52,8 @@ export default {
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
...@@ -81,12 +88,22 @@ export default { ...@@ -81,12 +88,22 @@ export default {
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
type="button" type="button"
:aria-label="stage.title"> :aria-label="stage.title"
<span v-html="svgHTML" aria-hidden="true"></span> ref="dropdown">
<i class="fa fa-caret-down" aria-hidden="true"></i> <span
v-html="svgHTML"
aria-hidden="true">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true" />
</button> </button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <ul
<div class="arrow-up" aria-hidden="true"></div> ref="dropdown-content"
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div
class="arrow-up"
aria-hidden="true"></div>
<div <div
:class="dropdownClass" :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
......
...@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg'; ...@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility'; import '../../lib/utils/datetime_utility';
export default { export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
data() { data() {
return { return {
currentTime: new Date(),
iconTimerSvg, iconTimerSvg,
}; };
}, },
props: ['pipeline'],
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
computed: { computed: {
timeAgo() { hasDuration() {
return gl.utils.getTimeago(); return this.duration > 0;
}, },
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at); hasFinishedTime() {
return this.finishedTime !== '';
}, },
timeStopped() {
const changeTime = this.currentTime; localTimeFinished() {
const options = { return gl.utils.formatDate(this.finishedTime);
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
}, },
duration() {
const { duration } = this.pipeline.details; durationFormated() {
const date = new Date(duration * 1000); const date = new Date(this.duration * 1000);
let hh = date.getUTCHours(); let hh = date.getUTCHours();
let mm = date.getUTCMinutes(); let mm = date.getUTCMinutes();
let ss = date.getSeconds(); let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`; // left pad
if (mm < 10) mm = `0${mm}`; if (hh < 10) {
if (ss < 10) ss = `0${ss}`; hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
if (duration !== null) return `${hh}:${mm}:${ss}`; return `${hh}:${mm}:${ss}`;
return false;
}, },
},
methods: { finishedTimeFormated() {
changeTime() { const timeAgo = gl.utils.getTimeago();
this.currentTime = new Date();
return timeAgo.format(this.finishedTime);
}, },
}, },
template: ` template: `
<td class="pipelines-time-ago"> <td class="pipelines-time-ago">
<p class="duration" v-if='duration'> <p
<span v-html="iconTimerSvg"></span> class="duration"
{{duration}} v-if="hasDuration">
<span
v-html="iconTimerSvg">
</span>
{{durationFormated}}
</p> </p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i> <p
class="finished-at"
v-if="hasFinishedTime">
<i
class="fa fa-calendar"
aria-hidden="true" />
<time <time
ref="tooltip"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:data-original-title='localTimeFinished'> :title="localTimeFinished">
{{timeStopped.words}} {{finishedTimeFormated}}
</time> </time>
</p> </p>
</td> </td>
......
import Vue from 'vue';
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
...@@ -161,15 +160,6 @@ export default { ...@@ -161,15 +160,6 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
/* eslint-disable no-underscore-dangle*/
import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore { export default class PipelinesStore {
constructor() { constructor() {
this.state = {}; this.state = {};
...@@ -30,32 +27,4 @@ export default class PipelinesStore { ...@@ -30,32 +27,4 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo; this.state.pageInfo = paginationInfo;
} }
/**
* FIXME: Move this inside the component.
*
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
VueRealtimeListener(removeIntervals, startIntervals);
}
} }
export default (removeIntervals, startIntervals) => {
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
window.removeEventListener('onbeforeload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
window.addEventListener('onbeforeload', removeIntervals);
};
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
...@@ -166,6 +165,32 @@ export default { ...@@ -166,6 +165,32 @@ export default {
} }
return undefined; return undefined;
}, },
/**
* Timeago components expects a number
*
* @return {type} description
*/
pipelineDuration() {
if (this.pipeline.details && this.pipeline.details.duration) {
return this.pipeline.details.duration;
}
return 0;
},
/**
* Timeago component expects a String.
*
* @return {String}
*/
pipelineFinishedAt() {
if (this.pipeline.details && this.pipeline.details.finished_at) {
return this.pipeline.details.finished_at;
}
return '';
},
}, },
template: ` template: `
...@@ -192,7 +217,9 @@ export default { ...@@ -192,7 +217,9 @@ export default {
</div> </div>
</td> </td>
<time-ago :pipeline="pipeline"/> <time-ago
:duration="pipelineDuration"
:finished-time="pipelineFinishedAt" />
<td class="pipeline-actions"> <td class="pipeline-actions">
<div class="pull-right btn-group"> <div class="pull-right btn-group">
......
...@@ -195,7 +195,6 @@ ...@@ -195,7 +195,6 @@
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
overflow: hidden;
@include set-invisible; @include set-invisible;
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
......
...@@ -104,6 +104,24 @@ ...@@ -104,6 +104,24 @@
padding: 2px 7px; padding: 2px 7px;
} }
.value {
padding-right: 0;
}
.remove-token {
display: inline-block;
padding-left: 4px;
padding-right: 8px;
.fa-close {
color: $gl-text-color-disabled;
}
&:hover .fa-close {
color: $gl-text-color-secondary;
}
}
.name { .name {
background-color: $filter-name-resting-color; background-color: $filter-name-resting-color;
color: $filter-name-text-color; color: $filter-name-text-color;
...@@ -112,7 +130,7 @@ ...@@ -112,7 +130,7 @@
text-transform: capitalize; text-transform: capitalize;
} }
.value { .value-container {
background-color: $white-normal; background-color: $white-normal;
color: $filter-value-text-color; color: $filter-value-text-color;
border-radius: 0 2px 2px 0; border-radius: 0 2px 2px 0;
...@@ -124,7 +142,7 @@ ...@@ -124,7 +142,7 @@
background-color: $filter-name-selected-color; background-color: $filter-name-selected-color;
} }
.value { .value-container {
background-color: $filter-value-selected-color; background-color: $filter-value-selected-color;
} }
} }
......
...@@ -622,6 +622,7 @@ pre.light-well { ...@@ -622,6 +622,7 @@ pre.light-well {
.controls { .controls {
margin-left: auto; margin-left: auto;
text-align: right;
} }
.ci-status-link { .ci-status-link {
......
...@@ -160,7 +160,6 @@ ...@@ -160,7 +160,6 @@
.tree-controls { .tree-controls {
float: right; float: right;
margin-top: 11px;
position: relative; position: relative;
z-index: 2; z-index: 2;
......
module MarkdownPreview
private
def render_markdown_preview(text, markdown_context = {})
render json: {
body: view_context.markdown(text, markdown_context),
references: {
users: preview_referenced_users(text)
}
}
end
def preview_referenced_users(text)
extractor = Gitlab::ReferenceExtractor.new(@project, current_user)
extractor.analyze(text, author: current_user)
extractor.users.map(&:username)
end
end
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include MarkdownPreview
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
...@@ -97,21 +99,13 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -97,21 +99,13 @@ class Projects::WikisController < Projects::ApplicationController
) )
end end
def preview_markdown def git_access
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: ext.users.map(&:username)
}
}
end end
def git_access def preview_markdown
context = { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
render_markdown_preview(params[:text], context)
end end
private private
...@@ -121,7 +115,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -121,7 +115,6 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include ExtractsPath include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -217,20 +218,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -217,20 +218,6 @@ class ProjectsController < Projects::ApplicationController
} }
end end
def preview_markdown
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
ext.analyze(text, author: current_user)
render json: {
body: view_context.markdown(text),
references: {
users: ext.users.map(&:username)
}
}
end
def refs def refs
branches = BranchesFinder.new(@repository, params).execute.map(&:name) branches = BranchesFinder.new(@repository, params).execute.map(&:name)
...@@ -253,6 +240,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -253,6 +240,10 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json render json: options.to_json
end end
def preview_markdown
render_markdown_preview(params[:text])
end
private private
# Render project landing depending of which features are available # Render project landing depending of which features are available
......
...@@ -2,6 +2,7 @@ class SnippetsController < ApplicationController ...@@ -2,6 +2,7 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
...@@ -77,6 +78,10 @@ class SnippetsController < ApplicationController ...@@ -77,6 +78,10 @@ class SnippetsController < ApplicationController
) )
end end
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
protected protected
def snippet def snippet
......
...@@ -199,38 +199,6 @@ module ApplicationHelper ...@@ -199,38 +199,6 @@ module ApplicationHelper
end end
end end
def render_markup(file_name, file_content)
if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown(file_content))
elsif asciidoc?(file_name)
asciidoc(file_content)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
file_content
end
else
other_markup(file_name, file_content)
end
rescue RuntimeError
simple_format(file_content)
end
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
def markup?(filename)
Gitlab::MarkupHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkupHelper.gitlab_markdown?(filename)
end
def asciidoc?(filename)
Gitlab::MarkupHelper.asciidoc?(filename)
end
def promo_host def promo_host
'about.gitlab.com' 'about.gitlab.com'
end end
......
require 'nokogiri' require 'nokogiri'
module GitlabMarkdownHelper module MarkupHelper
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
end
def markup?(filename)
Gitlab::MarkupHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkupHelper.gitlab_markdown?(filename)
end
def asciidoc?(filename)
Gitlab::MarkupHelper.asciidoc?(filename)
end
# Use this in places where you would normally use link_to(gfm(...), ...). # Use this in places where you would normally use link_to(gfm(...), ...).
# #
# It solves a problem occurring with nested links (i.e. # It solves a problem occurring with nested links (i.e.
...@@ -11,7 +27,7 @@ module GitlabMarkdownHelper ...@@ -11,7 +27,7 @@ module GitlabMarkdownHelper
# explicitly produce the correct linking behavior (i.e. # explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
def link_to_gfm(body, url, html_options = {}) def link_to_gfm(body, url, html_options = {})
return "" if body.blank? return '' if body.blank?
context = { context = {
project: @project, project: @project,
...@@ -43,71 +59,73 @@ module GitlabMarkdownHelper ...@@ -43,71 +59,73 @@ module GitlabMarkdownHelper
fragment.to_html.html_safe fragment.to_html.html_safe
end end
# Return the first line of +text+, up to +max_chars+, after parsing the line
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
truncate_visible(md, max_chars || md.length) if md.present?
end
def markdown(text, context = {}) def markdown(text, context = {})
return "" unless text.present? return '' unless text.present?
context[:project] ||= @project context[:project] ||= @project
html = markdown_unsafe(text, context)
html = Banzai.render(text, context)
banzai_postprocess(html, context) banzai_postprocess(html, context)
end end
def markdown_field(object, field) def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display) object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present? return '' unless object.present?
html = Banzai.render_field(object, field) html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field)) banzai_postprocess(html, object.banzai_render_context(field))
end end
def asciidoc(text) def markup(file_name, text, context = {})
Gitlab::Asciidoc.render( context[:project] ||= @project
text, html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
project: @project, banzai_postprocess(html, context)
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
end
def other_markup(file_name, text)
Gitlab::OtherMarkup.render(
file_name,
text,
project: @project,
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
end end
# Return the first line of +text+, up to +max_chars+, after parsing the line def render_wiki_content(wiki_page)
# as Markdown. HTML tags in the parsed output are not counted toward the text = wiki_page.content
# +max_chars+ limit. If the length limit falls within a tag's contents, then return '' unless text.present?
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
truncate_visible(md, max_chars || md.length) if md.present? context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
end
def render_wiki_content(wiki_page) html =
case wiki_page.format case wiki_page.format
when :markdown when :markdown
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) markdown_unsafe(text, context)
when :asciidoc when :asciidoc
asciidoc(wiki_page.content) asciidoc_unsafe(text)
else else
wiki_page.formatted_content.html_safe wiki_page.formatted_content.html_safe
end end
banzai_postprocess(html, context)
end
def markup_unsafe(file_name, text, context = {})
return '' unless text.present?
if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context))
elsif asciidoc?(file_name)
asciidoc_unsafe(text)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
text
end
else
other_markup_unsafe(file_name, text)
end
rescue RuntimeError
simple_format(text)
end end
# Returns the text necessary to reference `entity` across projects # Returns the text necessary to reference `entity` across projects
...@@ -183,10 +201,10 @@ module GitlabMarkdownHelper ...@@ -183,10 +201,10 @@ module GitlabMarkdownHelper
end end
def markdown_toolbar_button(options = {}) def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" }) data = options[:data].merge({ container: 'body' })
content_tag :button, content_tag :button,
type: "button", type: 'button',
class: "toolbar-btn js-md has-tooltip hidden-xs", class: 'toolbar-btn js-md has-tooltip hidden-xs',
tabindex: -1, tabindex: -1,
data: data, data: data,
title: options[:title], title: options[:title],
...@@ -195,17 +213,34 @@ module GitlabMarkdownHelper ...@@ -195,17 +213,34 @@ module GitlabMarkdownHelper
end end
end end
def markdown_unsafe(text, context = {})
Banzai.render(text, context)
end
def asciidoc_unsafe(text)
Gitlab::Asciidoc.render(text)
end
def other_markup_unsafe(file_name, text)
Gitlab::OtherMarkup.render(file_name, text)
end
# Calls Banzai.post_process with some common context options # Calls Banzai.post_process with some common context options
def banzai_postprocess(html, context) def banzai_postprocess(html, context = {})
return '' unless html.present?
context.merge!( context.merge!(
current_user: (current_user if defined?(current_user)), current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter # RelativeLinkFilter
requested_path: @path, commit: @commit,
project_wiki: @project_wiki, project_wiki: @project_wiki,
ref: @ref ref: @ref,
requested_path: @path
) )
Banzai.post_process(html, context) Banzai.post_process(html, context)
end end
extend self
end end
...@@ -160,7 +160,7 @@ module ProjectsHelper ...@@ -160,7 +160,7 @@ module ProjectsHelper
end end
def project_list_cache_key(project) def project_list_cache_key(project)
key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3'] key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.4']
key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
key key
......
...@@ -13,8 +13,8 @@ module ServicesHelper ...@@ -13,8 +13,8 @@ module ServicesHelper
"Event will be triggered when a confidential issue is created/updated/closed" "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events" when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged" "Event will be triggered when a merge request is created/updated/merged"
when "build", "build_events" when "pipeline", "pipeline_events"
"Event will be triggered when a build status changes" "Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events" when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events" when "commit", "commit_events"
......
...@@ -12,10 +12,6 @@ module TreeHelper ...@@ -12,10 +12,6 @@ module TreeHelper
tree.html_safe tree.html_safe
end end
def render_readme(readme)
render_markup(readme.name, readme.data)
end
# Return an image icon depending on the file type and mode # Return an image icon depending on the file type and mode
# #
# type - String type of the tree item; either 'folder' or 'file' # type - String type of the tree item; either 'folder' or 'file'
......
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
helper ApplicationHelper helper ApplicationHelper
helper GitlabMarkdownHelper helper MarkupHelper
attr_accessor :current_user attr_accessor :current_user
helper_method :current_user, :can? helper_method :current_user, :can?
......
...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true
......
...@@ -107,7 +107,8 @@ module Network ...@@ -107,7 +107,8 @@ module Network
def find_commits(skip = 0) def find_commits(skip = 0)
opts = { opts = {
max_count: self.class.max_count, max_count: self.class.max_count,
skip: skip skip: skip,
order: :date
} }
opts[:ref] = @commit.id if @filter_ref opts[:ref] = @commit.id if @filter_ref
......
...@@ -24,9 +24,9 @@ class Repository ...@@ -24,9 +24,9 @@ class Repository
# same name. The cache key used by those methods must also match method's # same name. The cache key used by those methods must also match method's
# name. # name.
# #
# For example, for entry `:readme` there's a method called `readme` which # For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `readme` cache key. # stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count readme contribution_guide CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze tag_count avatar exists? empty? root_ref).freeze
...@@ -35,7 +35,7 @@ class Repository ...@@ -35,7 +35,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches. # the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = { METHOD_CACHES_FOR_FILE_TYPES = {
readme: :readme, readme: :rendered_readme,
changelog: :changelog, changelog: :changelog,
license: %i(license_blob license_key), license: %i(license_blob license_key),
contributing: :contribution_guide, contributing: :contribution_guide,
...@@ -534,7 +534,11 @@ class Repository ...@@ -534,7 +534,11 @@ class Repository
head.readme head.readme
end end
end end
cache_method :readme
def rendered_readme
MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
end
cache_method :rendered_readme
def contribution_guide def contribution_guide
file_on_head(:contributing) file_on_head(:contributing)
......
...@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base ...@@ -26,6 +26,7 @@ class Service < ActiveRecord::Base
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: proc { |service| service.template? } validates :project_id, presence: true, unless: proc { |service| service.template? }
validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
......
...@@ -100,7 +100,8 @@ module Projects ...@@ -100,7 +100,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create) system_hook_service.execute_hooks_for(@project, :create)
unless @project.group || @project.gitlab_project_import? unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user] owners = [current_user, @project.namespace.owner].compact.uniq
@project.add_master(owners, current_user: current_user)
end end
predefined_push_rule = PushRule.find_by(is_sample: true) predefined_push_rule = PushRule.find_by(is_sample: true)
......
...@@ -8,6 +8,7 @@ xml.entry do ...@@ -8,6 +8,7 @@ xml.entry do
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do xml.author do
xml.username event.author_username
xml.name event.author_name xml.name event.author_name
xml.email event.author_public_email xml.email event.author_public_email
end end
......
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme' = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
.file-content.wiki .file-content.wiki
= cache(readme_cache_key) do = markup(readme.name, readme.data, rendered: @repository.rendered_readme)
= render_readme(readme)
- else - else
.row-content-block.second-block.center .row-content-block.second-block.center
%h3.page-title %h3.page-title
......
- blob.load_all_data!(@repository) - blob.load_all_data!(@repository)
.file-content.wiki .file-content.wiki
= render_markup(blob.name, blob.data) = markup(blob.name, blob.data)
.diff-file .diff-file
.diff-content .diff-content
- if gitlab_markdown?(@blob.name) - if markup?(@blob.name)
.file-content.wiki .file-content.wiki
= preserve do = markup(@blob.name, @content)
= markdown(@content)
- elsif markup?(@blob.name)
.file-content.wiki
= raw render_markup(@blob.name, @content)
- else - else
.file-content.code.js-syntax-highlight .file-content.code.js-syntax-highlight
- unless @diff_lines.empty? - unless @diff_lines.empty?
......
- blob = viewer.blob - blob = viewer.blob
.file-content.wiki .file-content.wiki
= render_markup(blob.name, blob.data) = markup(blob.name, blob.data)
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
Milestone Milestone
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
= dropdown_title("Assignee milestone") = dropdown_title("Assign milestone")
= dropdown_filter("Search milestones") = dropdown_filter("Search milestones")
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
%strong %strong
= readme.name = readme.name
.file-content.wiki .file-content.wiki
= render_readme(readme) = markup(readme.name, readme.data)
.tree-controls
= render 'projects/find_file_link'
= render 'projects/buttons/download', project: @project, ref: @ref
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
......
...@@ -7,12 +7,4 @@ ...@@ -7,12 +7,4 @@
= render 'projects/last_push' = render 'projects/last_push'
%div{ class: container_class } %div{ class: container_class }
.tree-controls = render 'projects/files'
= render 'projects/find_file_link'
= render 'projects/buttons/download', project: @project, ref: @ref
#tree-holder.tree-holder.clearfix
.nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.file-content.wiki .file-content.wiki
- snippet_chunks.each do |chunk| - snippet_chunks.each do |chunk|
- unless chunk[:data].empty? - unless chunk[:data].empty?
= render_markup(snippet.file_name, chunk[:data]) = markup(snippet.file_name, chunk[:data])
- else - else
.file-content.code .file-content.code
.nothing-here-block Empty file .nothing-here-block Empty file
......
<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
.sidebar-collapsed-icon .sidebar-collapsed-icon
%strong %strong
= icon('exclamation', 'aria-hidden': 'true') = icon('exclamation', 'aria-hidden': 'true')
%span= milestone.issues_visible_to_user(current_user).count %span= milestone.merge_requests.count
.title.hide-collapsed .title.hide-collapsed
Merge requests Merge requests
%span.badge= milestone.merge_requests.count %span.badge= milestone.merge_requests.count
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project) - cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.updated_at)
%li.project-row{ class: css_class } %li.project-row{ class: css_class }
= cache(cache_key) do = cache(cache_key) do
...@@ -37,6 +38,7 @@ ...@@ -37,6 +38,7 @@
= markdown_field(project, :description) = markdown_field(project, :description)
.controls .controls
.prepend-top-0
- if project.archived - if project.archived
%span.prepend-left-10.label.label-warning archived %span.prepend-left-10.label.label-warning archived
- if project.pipeline_status.has_status? - if project.pipeline_status.has_status?
...@@ -52,3 +54,5 @@ ...@@ -52,3 +54,5 @@
= number_with_delimiter(project.star_count) = number_with_delimiter(project.star_count)
%span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) }
= visibility_level_icon(project.visibility_level, fw: true) = visibility_level_icon(project.visibility_level, fw: true)
.prepend-top-5
updated #{updated_tooltip}
...@@ -24,6 +24,6 @@ ...@@ -24,6 +24,6 @@
- if gitlab_markdown?(@snippet.file_name) - if gitlab_markdown?(@snippet.file_name)
= preserve(markdown_field(@snippet, :content)) = preserve(markdown_field(@snippet, :content))
- else - else
= render_markup(@snippet.file_name, @snippet.content) = markup(@snippet.file_name, @snippet.content)
- else - else
= render 'shared/file_highlight', blob: @snippet = render 'shared/file_highlight', blob: @snippet
---
title: Support Markdown previews for personal snippets
merge_request: 10810
author:
---
title: Fix UI inconsistency different files view (find file button missing)
merge_request: 9847
author: TM Lee
---
title: Add update time to project lists.
merge_request: 8514
author: Jeff Stubler
---
title: 'Remove view fragment caching for project READMEs'
merge_request: 8838
author:
---
title: Allow admins to sudo to blocked users via the API
merge_request: 10842
author:
---
title: Add button to delete filters from filtered search bar
merge_request:
author:
---
title: Fix pipeline events description for Slack and Mattermost integration
merge_request: 10908
author:
---
title: Fix ordering of commits in the network graph
merge_request: 10936
author:
---
title: Fixed milestone sidebar showing incorrect number of MRs when collapsed
merge_request: 10933
author:
---
title: Add username to activity atom feed
merge_request: 10802
author: winniehell
---
title: Bump Sidekiq to 5.0.0
merge_request:
author:
---
title: Fixed wrong method call on notify_post_receive
merge_request:
author: Luigi Leoni
---
title: Fix rendering emoji inside a string
merge_request: 10647
author: blackst0ne
---
title: Refactor backup/restore docs
merge_request:
author:
---
title: Replace header merge request icon
merge_request: 10932
author: blackst0ne
---
title: Ensure namespace owner is Master of project upon creation
merge_request: 10910
author:
---
title: Dockerfiles templates are imported from gitlab.com/gitlab-org/Dockerfile
merge_request: 10663
author:
...@@ -3,6 +3,7 @@ resources :snippets, concerns: :awardable do ...@@ -3,6 +3,7 @@ resources :snippets, concerns: :awardable do
get 'raw' get 'raw'
get 'download' get 'download'
post :mark_as_spam post :mark_as_spam
post :preview_markdown
end end
end end
......
class RemoveNilTypeServices < ActiveRecord::Migration
DOWNTIME = false
def up
execute <<-SQL
DELETE FROM services WHERE type IS NULL OR type = '';
SQL
end
def down
end
end
# GitLab Enterprise Edition # GitLab Enterprise Edition
[GitLab](https://about.gitlab.com/) is a Git-based fully featured platform for software development. [GitLab](https://about.gitlab.com/) is a Git-based fully featured platform
for software development.
**[GitLab Enterprise Edition (EE)](https://about.gitlab.com/gitlab-ee/)** is an opencore product, self-hosted, available under distinct [subscriptions](https://about.gitlab.com/products/). **GitLab Enterprise Edition (EE)** is an opencore product, self-hosted,
available under distinct [subscriptions](https://about.gitlab.com/products/).
GitLab EE contains all the features available in [GitLab Community Edition (CE)](https://docs.gitlab.com/ce/), plus premium features available in each version: **Enterprise Edition Starter** (**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in **EES** is also available in **EEP**. GitLab EE contains all the features available in
[GitLab Community Edition (CE)](https://docs.gitlab.com/ce/),
plus premium features available in each version: **Enterprise Edition Starter**
(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in
**EES** is also available in **EEP**.
---- ----
...@@ -22,7 +28,7 @@ Shortcuts to GitLab's most visited docs: ...@@ -22,7 +28,7 @@ Shortcuts to GitLab's most visited docs:
- [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow. - [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow.
- See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/). - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown). - [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
- [GitLab Slash Commands](user/project/slash_commands.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI - [GitLab Slash Commands](user/project/slash_commands.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
### User account ### User account
...@@ -69,7 +75,7 @@ Manage files and branches from the UI (user interface): ...@@ -69,7 +75,7 @@ Manage files and branches from the UI (user interface):
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests. - [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles. - [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
- [Merge Requests](user/project/merge_requests/index.md) - [Merge Requests](user/project/merge_requests/index.md)
- [WIP Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md) - [Work In Progress Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
- [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved. - [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
- **(EES/EEP)** [Merge Request approval](user/project/merge_requests/merge_request_approvals.md): Make sure every merge request is approved by one or more people before getting merged. - **(EES/EEP)** [Merge Request approval](user/project/merge_requests/merge_request_approvals.md): Make sure every merge request is approved by one or more people before getting merged.
- [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally) - [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
...@@ -118,7 +124,7 @@ Take a step ahead and dive into GitLab's advanced features. ...@@ -118,7 +124,7 @@ Take a step ahead and dive into GitLab's advanced features.
### Integrations ### Integrations
- [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat. - [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat.
- [GitLab Integrations](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication. - [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication.
---- ----
...@@ -145,12 +151,12 @@ have access to GitLab administration tools and settings. ...@@ -145,12 +151,12 @@ have access to GitLab administration tools and settings.
### GitLab admins' superpowers ### GitLab admins' superpowers
- [Container Registry](administration/container_registry.md): Configure Container Registry with GitLab. - [Container Registry](administration/container_registry.md): Configure Docker Registry with GitLab.
- [Custom Git hooks](administration/custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough. - [Custom Git hooks](administration/custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
- [Git LFS configuration](workflow/lfs/lfs_administration.md): Enable/disable Git LFS, change the location of LFS object storage. - [Git LFS configuration](workflow/lfs/lfs_administration.md): Learn how to use LFS under GitLab.
- [GitLab Pages configuration](administration/pages/index.md): Configure GitLab Pages. - [GitLab Pages configuration](administration/pages/index.md): Configure GitLab Pages.
- [High Availability](administration/high_availability/README.md): Configure multiple servers for scaling or high availability. - [High Availability](administration/high_availability/README.md): Configure multiple servers for scaling or high availability.
- [User cohorts](user/admin_area/user_cohorts.md): View user activity over time. - [User cohorts](user/admin_area/user_cohorts.md) View user activity over time.
- [Web terminals](administration/integration/terminal.md): Provide terminal access to environments from within GitLab. - [Web terminals](administration/integration/terminal.md): Provide terminal access to environments from within GitLab.
- **(EES/EEP)** [Audit logs and events](administration/audit_events.md): View the changes made within the GitLab server. - **(EES/EEP)** [Audit logs and events](administration/audit_events.md): View the changes made within the GitLab server.
- **(EES/EEP)** [Elasticsearch](integration/elasticsearch.md): A flexible, scalable and powerful search service to keep GitLab's search fast when dealing with huge amount of data. - **(EES/EEP)** [Elasticsearch](integration/elasticsearch.md): A flexible, scalable and powerful search service to keep GitLab's search fast when dealing with huge amount of data.
...@@ -163,7 +169,7 @@ have access to GitLab administration tools and settings. ...@@ -163,7 +169,7 @@ have access to GitLab administration tools and settings.
- **(EES/EEP)** [Omnibus support for external MySQL DB](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only): Omnibus package supports configuring an external MySQL database. - **(EES/EEP)** [Omnibus support for external MySQL DB](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only): Omnibus package supports configuring an external MySQL database.
- **(EES/EEP)** [Omnibus support for log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only) - **(EES/EEP)** [Omnibus support for log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only)
- GitLab CI - GitLab CI
- [CI admin settings](user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration. - [CI admin settings](user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
### Integrations ### Integrations
...@@ -172,6 +178,7 @@ have access to GitLab administration tools and settings. ...@@ -172,6 +178,7 @@ have access to GitLab administration tools and settings.
- [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost. - [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost.
- **(EES/EEP)** [Jenkins](integration/jenkins.md): Set up GitLab with Jenkins. - **(EES/EEP)** [Jenkins](integration/jenkins.md): Set up GitLab with Jenkins.
### Monitoring ### Monitoring
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics. - [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
...@@ -182,7 +189,7 @@ have access to GitLab administration tools and settings. ...@@ -182,7 +189,7 @@ have access to GitLab administration tools and settings.
- [Housekeeping](administration/housekeeping.md): Keep your Git repository tidy and fast. - [Housekeeping](administration/housekeeping.md): Keep your Git repository tidy and fast.
- [Operations](administration/operations.md): Keeping GitLab up and running. - [Operations](administration/operations.md): Keeping GitLab up and running.
- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates - [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates.
- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests. - [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
### Customization ### Customization
......
...@@ -18,7 +18,8 @@ you need to use with GitLab. ...@@ -18,7 +18,8 @@ you need to use with GitLab.
## GitLab Pages Ports ## GitLab Pages Ports
If you're using GitLab Pages you will need some additional port configurations. If you're using GitLab Pages with custom domain support you will need some
additional port configurations.
GitLab Pages requires a separate virtual IP address. Configure DNS to point the GitLab Pages requires a separate virtual IP address. Configure DNS to point the
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the `pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
[GitLab Pages documentation][gitlab-pages] for more information. [GitLab Pages documentation][gitlab-pages] for more information.
......
...@@ -7,21 +7,20 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as ...@@ -7,21 +7,20 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
specifically test NFSv3. specifically test NFSv3.
**no_root_squash**: NFS normally changes the `root` user to `nobody`. This is
a good security measure when NFS shares will be accessed by many different
users. However, in this case only GitLab will use the NFS share so it
is safe. GitLab requires the `no_root_squash` setting because we need to
manage file permissions automatically. Without the setting you will receive
errors when the Omnibus package tries to alter permissions. Note that GitLab
and other bundled components do **not** run as `root` but as non-privileged
users. The requirement for `no_root_squash` is to allow the Omnibus package to
set ownership and permissions on files, as needed.
### Recommended options ### Recommended options
When you define your NFS exports, we recommend you also add the following When you define your NFS exports, we recommend you also add the following
options: options:
- `no_root_squash` - NFS normally changes the `root` user to `nobody`. This is
a good security measure when NFS shares will be accessed by many different
users. However, in this case only GitLab will use the NFS share so it
is safe. GitLab recommends the `no_root_squash` setting because we need to
manage file permissions automatically. Without the setting you may receive
errors when the Omnibus package tries to alter permissions. Note that GitLab
and other bundled components do **not** run as `root` but as non-privileged
users. The recommendation for `no_root_squash` is to allow the Omnibus package
to set ownership and permissions on files, as needed.
- `sync` - Force synchronous behavior. Default is asynchronous and under certain - `sync` - Force synchronous behavior. Default is asynchronous and under certain
circumstances it could lead to data loss if a failure occurs before data has circumstances it could lead to data loss if a failure occurs before data has
synced. synced.
......
...@@ -202,6 +202,7 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). ...@@ -202,6 +202,7 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases. to separate phases.
- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` - Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test [four-phase-test]: https://robots.thoughtbot.com/four-phase-test
...@@ -225,6 +226,20 @@ so we need to set some guidelines for their use going forward: ...@@ -225,6 +226,20 @@ so we need to set some guidelines for their use going forward:
[lets-not]: https://robots.thoughtbot.com/lets-not [lets-not]: https://robots.thoughtbot.com/lets-not
#### `set` variables
In some cases there is no need to recreate the same object for tests again for
each example. For example, a project is needed to test issues on the same
project, one project will do for the entire file. This can be achieved by using
`set` in the same way you would use `let`.
`rspec-set` only works on ActiveRecord objects, and before new examples it
reloads or recreates the model, _only_ if needed. That is, when you changed
properties or destroyed the object.
There is one gotcha; you can't reference a model defined in a `let` block in a
`set` block.
### Time-sensitive tests ### Time-sensitive tests
[Timecop](https://github.com/travisjeffery/timecop) is available in our [Timecop](https://github.com/travisjeffery/timecop) is available in our
......
# Backup restore # Backing up and restoring GitLab
![backup banner](backup_hrz.png) ![backup banner](backup_hrz.png)
An application data backup creates an archive file that contains the database, An application data backup creates an archive file that contains the database,
all repositories and all attachments. all repositories and all attachments.
This archive will be saved in `backup_path`, which is specified in the
`config/gitlab.yml` file.
The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
identifies the time at which each backup was created.
> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`)
> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`)
You can only restore a backup to exactly the same version of GitLab on which it You can only restore a backup to **exactly the same version** of GitLab on which
was created. The best way to migrate your repositories from one server to it was created. The best way to migrate your repositories from one server to
another is through backup restore. another is through backup restore.
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` ## Backup
(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key, GitLab provides a simple command line interface to backup your whole installation,
[CI secret variables](../ci/variables/README.md#secret-variables), and and is flexible enough to fit your needs.
secret variables used for [two-factor authentication](../security/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server.
## Create a backup of the GitLab system ### Backup timestamp
>**Note:**
In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to
`EPOCH_YYYY_MM_DD_GitLab version`, for example `1493107454_2017_04_25`
would become `1493107454_2017_04_25_9.1.0`.
The backup archive will be saved in `backup_path`, which is specified in the
`config/gitlab.yml` file.
The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
identifies the time at which each backup was created, plus the GitLab version.
The timestamp is needed if you need to restore GitLab and multiple backups are
available.
For example, if the backup name is `1493107454_2017_04_25_9.1.0_gitlab_backup.tar`,
then the timestamp is `1493107454_2017_04_25_9.1.0`.
### Creating a backup of the GitLab system
Use this command if you've installed GitLab with the Omnibus package: Use this command if you've installed GitLab with the Omnibus package:
``` ```
sudo gitlab-rake gitlab:backup:create sudo gitlab-rake gitlab:backup:create
``` ```
Use this if you've installed GitLab from source: Use this if you've installed GitLab from source:
``` ```
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
``` ```
If you are running GitLab within a Docker container, you can run the backup from the host: If you are running GitLab within a Docker container, you can run the backup from the host:
``` ```
docker exec -t <container name> gitlab-rake gitlab:backup:create docker exec -t <container name> gitlab-rake gitlab:backup:create
``` ```
...@@ -69,9 +80,9 @@ Deleting tmp directories...[DONE] ...@@ -69,9 +80,9 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING] Deleting old backups... [SKIPPING]
``` ```
## Backup Strategy Option ### Backup strategy option
> **Note:** Introduced as an option in 8.17 > **Note:** Introduced as an option in GitLab 8.17.
The default backup strategy is to essentially stream data from the respective The default backup strategy is to essentially stream data from the respective
data locations to the backup using the Linux command `tar` and `gzip`. This works data locations to the backup using the Linux command `tar` and `gzip`. This works
...@@ -91,7 +102,7 @@ To use the `copy` strategy instead of the default streaming strategy, specify ...@@ -91,7 +102,7 @@ To use the `copy` strategy instead of the default streaming strategy, specify
`STRATEGY=copy` in the Rake task command. For example, `STRATEGY=copy` in the Rake task command. For example,
`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`. `sudo gitlab-rake gitlab:backup:create STRATEGY=copy`.
## Exclude specific directories from the backup ### Excluding specific directories from the backup
You can choose what should be backed up by adding the environment variable `SKIP`. You can choose what should be backed up by adding the environment variable `SKIP`.
The available options are: The available options are:
...@@ -115,7 +126,7 @@ sudo gitlab-rake gitlab:backup:create SKIP=db,uploads ...@@ -115,7 +126,7 @@ sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
``` ```
## Upload backups to remote (cloud) storage ### Uploading backups to a remote (cloud) storage
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates. Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload. It uses the [Fog library](http://fog.io/) to perform the upload.
...@@ -259,7 +270,7 @@ For installations from source: ...@@ -259,7 +270,7 @@ For installations from source:
remote_directory: 'gitlab_backups' remote_directory: 'gitlab_backups'
``` ```
## Backup archive permissions ### Backup archive permissions
The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`) The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
will have owner/group git:git and 0600 permissions by default. will have owner/group git:git and 0600 permissions by default.
...@@ -277,7 +288,7 @@ gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives wo ...@@ -277,7 +288,7 @@ gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives wo
archive_permissions: 0644 # Makes the backup archives world-readable archive_permissions: 0644 # Makes the backup archives world-readable
``` ```
## Storing configuration files ### Storing configuration files
Please be informed that a backup does not store your configuration Please be informed that a backup does not store your configuration
files. One reason for this is that your database contains encrypted files. One reason for this is that your database contains encrypted
...@@ -294,11 +305,74 @@ At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and ...@@ -294,11 +305,74 @@ At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
`/home/git/gitlab/config/secrets.yml` (source) to preserve your database `/home/git/gitlab/config/secrets.yml` (source) to preserve your database
encryption key. encryption key.
## Restore a previously created backup ### Configuring cron to make daily backups
>**Note:**
The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files)
or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
**For Omnibus installations**
To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
```
sudo su -
crontab -e
```
There, add the following line to schedule the backup for everyday at 2 AM:
```
0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1
```
You may also want to set a limited lifetime for backups to prevent regular
backups using all your disk space. To do this add the following lines to
`/etc/gitlab/gitlab.rb` and reconfigure:
You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. ```
# limit backup lifetime to 7 days - 604800 seconds
gitlab_rails['backup_keep_time'] = 604800
```
### Prerequisites Note that the `backup_keep_time` configuration option only manages local
files. GitLab does not automatically prune old files stored in a third-party
object storage (e.g., AWS S3) because the user may not have permission to list
and delete files. We recommend that you configure the appropriate retention
policy for your object storage. For example, you can configure [the S3 backup
policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
**For installation from source**
```
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
sudo -u git crontab -e # Edit the crontab for the git user
```
Add the following lines at the bottom:
```
# Create a full backup of the GitLab repositories and SQL database every day at 4am
0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1
```
The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors.
This is recommended to reduce cron spam.
## Restore
GitLab provides a simple command line interface to backup your whole installation,
and is flexible enough to fit your needs.
The [restore prerequisites section](#restore-prerequisites) includes crucial
information. Make sure to read and test the whole restore process at least once
before attempting to perform it in a production environment.
You can only restore a backup to **exactly the same version** of GitLab that
you created it on, for example 9.1.0.
### Restore prerequisites
You need to have a working GitLab installation before you can perform You need to have a working GitLab installation before you can perform
a restore. This is mainly because the system user performing the a restore. This is mainly because the system user performing the
...@@ -307,13 +381,23 @@ the SQL database it needs to import data into ('gitlabhq_production'). ...@@ -307,13 +381,23 @@ the SQL database it needs to import data into ('gitlabhq_production').
All existing data will be either erased (SQL) or moved to a separate All existing data will be either erased (SQL) or moved to a separate
directory (repositories, uploads). directory (repositories, uploads).
If some or all of your GitLab users are using two-factor authentication (2FA) To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
then you must also make sure to restore `/etc/gitlab/gitlab.rb` and (for Omnibus packages) or `/home/git/gitlab/.secret` (for installations
`/etc/gitlab/gitlab-secrets.json` (Omnibus), or from source). This file contains the database encryption key,
`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you [CI secret variables](../ci/variables/README.md#secret-variables), and
need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`. secret variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server.
Depending on your case, you might want to run the restore command with one or
more of the following options:
- `BACKUP=timestamp_of_backup` - Required if more than one backup exists.
Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Do not ask if the authorized_keys file should get regenerated.
### Installation from source ### Restore for installation from source
``` ```
# Stop processes that are connected to the database # Stop processes that are connected to the database
...@@ -322,13 +406,6 @@ sudo service gitlab stop ...@@ -322,13 +406,6 @@ sudo service gitlab stop
bundle exec rake gitlab:backup:restore RAILS_ENV=production bundle exec rake gitlab:backup:restore RAILS_ENV=production
``` ```
Options:
```
BACKUP=timestamp_of_backup (required if more than one backup exists)
force=yes (do not ask if the authorized_keys file should get regenerated)
```
Example output: Example output:
``` ```
...@@ -360,13 +437,13 @@ Restoring repositories: ...@@ -360,13 +437,13 @@ Restoring repositories:
Deleting tmp directories...[DONE] Deleting tmp directories...[DONE]
``` ```
### Omnibus installations ### Restore for Omnibus installations
This procedure assumes that: This procedure assumes that:
- You have installed the exact same version of GitLab Omnibus with which the - You have installed the **exact same version** of GitLab Omnibus with which the
backup was created backup was created.
- You have run `sudo gitlab-ctl reconfigure` at least once - You have run `sudo gitlab-ctl reconfigure` at least once.
- GitLab is running. If not, start it using `sudo gitlab-ctl start`. - GitLab is running. If not, start it using `sudo gitlab-ctl start`.
First make sure your backup tar file is in the backup directory described in the First make sure your backup tar file is in the backup directory described in the
...@@ -374,7 +451,7 @@ First make sure your backup tar file is in the backup directory described in the ...@@ -374,7 +451,7 @@ First make sure your backup tar file is in the backup directory described in the
`/var/opt/gitlab/backups`. `/var/opt/gitlab/backups`.
```shell ```shell
sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/ sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/
``` ```
Stop the processes that are connected to the database. Leave the rest of GitLab Stop the processes that are connected to the database. Leave the rest of GitLab
...@@ -392,7 +469,7 @@ restore: ...@@ -392,7 +469,7 @@ restore:
```shell ```shell
# This command will overwrite the contents of your GitLab database! # This command will overwrite the contents of your GitLab database!
sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186_2014_02_27 sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0
``` ```
Restart and check GitLab: Restart and check GitLab:
...@@ -404,59 +481,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true ...@@ -404,59 +481,7 @@ sudo gitlab-rake gitlab:check SANITIZE=true
If there is a GitLab version mismatch between your backup tar file and the installed If there is a GitLab version mismatch between your backup tar file and the installed
version of GitLab, the restore command will abort with an error. Install the version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://about.gitlab.com/downloads/archives/) and try again. [correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
## Configure cron to make daily backups
### For installation from source:
```
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
sudo -u git crontab -e # Edit the crontab for the git user
```
Add the following lines at the bottom:
```
# Create a full backup of the GitLab repositories and SQL database every day at 4am
0 4 * * * cd /home/git/gitlab && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake gitlab:backup:create RAILS_ENV=production CRON=1
```
The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors.
This is recommended to reduce cron spam.
### For omnibus installations
To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
```
sudo su -
crontab -e
```
There, add the following line to schedule the backup for everyday at 2 AM:
```
0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1
```
You may also want to set a limited lifetime for backups to prevent regular
backups using all your disk space. To do this add the following lines to
`/etc/gitlab/gitlab.rb` and reconfigure:
```
# limit backup lifetime to 7 days - 604800 seconds
gitlab_rails['backup_keep_time'] = 604800
```
Note that the `backup_keep_time` configuration option only manages local
files. GitLab does not automatically prune old files stored in a third-party
object storage (e.g. AWS S3) because the user may not have permission to list
and delete files. We recommend that you configure the appropriate retention
policy for your object storage. For example, you can configure [the S3 backup
policy here as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
## Alternative backup strategies ## Alternative backup strategies
...@@ -481,6 +506,19 @@ Example: LVM snapshots + rsync ...@@ -481,6 +506,19 @@ Example: LVM snapshots + rsync
If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server. If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server.
It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use. It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use.
## Additional notes
This documentation is for GitLab Community and Enterprise Edition. We backup
GitLab.com and make sure your data is secure, but you can't use these methods
to export / backup your data yourself from GitLab.com.
Issues are stored in the database. They can't be stored in Git itself.
To migrate your repositories from one server to another with an up-to-date version of
GitLab, you can use the [import rake task](import.md) to do a mass import of the
repository. Note that if you do an import rake task, rather than a backup restore, you
will have all your repositories, but not any other data.
## Troubleshooting ## Troubleshooting
### Restoring database backup using omnibus packages outputs warnings ### Restoring database backup using omnibus packages outputs warnings
...@@ -490,7 +528,6 @@ If you are using backup restore procedures you might encounter the following war ...@@ -490,7 +528,6 @@ If you are using backup restore procedures you might encounter the following war
psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql
psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurrences) psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurrences)
psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurrences) psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurrences)
``` ```
Be advised that, backup is successfully restored in spite of these warnings. Be advised that, backup is successfully restored in spite of these warnings.
...@@ -499,14 +536,3 @@ The rake task runs this as the `gitlab` user which does not have the superuser a ...@@ -499,14 +536,3 @@ The rake task runs this as the `gitlab` user which does not have the superuser a
Those objects have no influence on the database backup/restore but they give this annoying warning. Those objects have no influence on the database backup/restore but they give this annoying warning.
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql). For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
## Note
This documentation is for GitLab CE.
We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
Issues are stored in the database. They can't be stored in Git itself.
To migrate your repositories from one server to another with an up-to-date version of
GitLab, you can use the [import rake task](import.md) to do a mass import of the
repository. Note that if you do an import rake task, rather than a backup restore, you
will have all your repositories, but not any other data.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
GitLab Inc. will periodically collect information about your instance in order GitLab Inc. will periodically collect information about your instance in order
to perform various actions. to perform various actions.
All statistics are opt-in and you can always disable them from the admin panel. All statistics are opt-out, you can disable them from the admin panel.
## Version check ## Version check
......
...@@ -51,9 +51,9 @@ service in GitLab. ...@@ -51,9 +51,9 @@ service in GitLab.
## Troubleshooting ## Troubleshooting
If builds are not triggered, these are a couple of things to keep in mind. If builds are not triggered, ensure you entered the right GitLab IP address in
Bamboo under 'Trigger IP addresses'.
>**Note:**
- Starting with GitLab 8.14.0, builds are triggered on push events.
1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
IP addresses'.
1. Remember that GitLab only triggers builds on push events. A commit via the
web interface will not trigger CI currently.
...@@ -104,7 +104,7 @@ module API ...@@ -104,7 +104,7 @@ module API
end end
def authenticate! def authenticate!
unauthorized! unless current_user && can?(current_user, :access_api) unauthorized! unless current_user && can?(initial_current_user, :access_api)
end end
def authenticate_non_get! def authenticate_non_get!
......
...@@ -152,7 +152,7 @@ module API ...@@ -152,7 +152,7 @@ module API
begin begin
Gitlab::GitalyClient::Notifications.new(project.repository).post_receive Gitlab::GitalyClient::Notifications.new(project.repository).post_receive
rescue GRPC::Unavailable => e rescue GRPC::Unavailable => e
render_api_error(e, 500) render_api_error!(e, 500)
end end
end end
end end
......
...@@ -15,7 +15,7 @@ module Backup ...@@ -15,7 +15,7 @@ module Backup
s[:gitlab_version] = Gitlab::VERSION s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version s[:tar_version] = tar_version
s[:skipped] = ENV["SKIP"] s[:skipped] = ENV["SKIP"]
tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}" tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d_')}#{s[:gitlab_version]}#{FILE_NAME_SUFFIX}"
Dir.chdir(backup_path) do Dir.chdir(backup_path) do
File.open("#{backup_path}/backup_information.yml", "w+") do |file| File.open("#{backup_path}/backup_information.yml", "w+") do |file|
......
...@@ -53,7 +53,10 @@ module Banzai ...@@ -53,7 +53,10 @@ module Banzai
# Build a regexp that matches all valid :emoji: names. # Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ @emoji_pattern ||=
/(?<=[^[:alnum:]:]|\n|^)
:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):
(?=[^[:alnum:]:]|$)/x
end end
# Build a regexp that matches all valid unicode emojis names. # Build a regexp that matches all valid unicode emojis names.
......
...@@ -14,28 +14,16 @@ module Gitlab ...@@ -14,28 +14,16 @@ module Gitlab
# Public: Converts the provided Asciidoc markup into HTML. # Public: Converts the provided Asciidoc markup into HTML.
# #
# input - the source text in Asciidoc format # input - the source text in Asciidoc format
# context - a Hash with the template context:
# :commit
# :project
# :project_wiki
# :requested_path
# :ref
# asciidoc_opts - a Hash of options to pass to the Asciidoctor converter
# #
def self.render(input, context, asciidoc_opts = {}) def self.render(input)
asciidoc_opts.reverse_merge!( asciidoc_opts = { safe: :secure,
safe: :secure,
backend: :gitlab_html5, backend: :gitlab_html5,
attributes: [] attributes: DEFAULT_ADOC_ATTRS }
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
plantuml_setup plantuml_setup
html = ::Asciidoctor.convert(input, asciidoc_opts) html = ::Asciidoctor.convert(input, asciidoc_opts)
html = Banzai.post_process(html, context)
filter = Banzai::Filter::SanitizationFilter.new(html) filter = Banzai::Filter::SanitizationFilter.new(html)
html = filter.call.to_s html = filter.call.to_s
......
...@@ -494,7 +494,9 @@ module Gitlab ...@@ -494,7 +494,9 @@ module Gitlab
# :contains is the commit contained by the refs from which to begin (SHA1 or name) # :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch # :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip # :skip is the number of commits to skip
# :order is the commits order and allowed value is :date(default) or :topo # :order is the commits order and allowed value is :none (default), :date, or :topo
# commit ordering types are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
# #
def find_commits(options = {}) def find_commits(options = {})
actual_options = options.dup actual_options = options.dup
...@@ -522,11 +524,8 @@ module Gitlab ...@@ -522,11 +524,8 @@ module Gitlab
end end
end end
if actual_options[:order] == :topo sort_type = rugged_sort_type(actual_options[:order])
walker.sorting(Rugged::SORT_TOPO) walker.sorting(sort_type)
else
walker.sorting(Rugged::SORT_NONE)
end
commits = [] commits = []
offset = actual_options[:skip] offset = actual_options[:skip]
...@@ -1273,6 +1272,18 @@ module Gitlab ...@@ -1273,6 +1272,18 @@ module Gitlab
def gitaly_ref_client def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self) @gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self)
end end
# Returns the `Rugged` sorting type constant for a given
# sort type key. Valid keys are `:none`, `:topo`, and `:date`
def rugged_sort_type(key)
@rugged_sort_types ||= {
none: Rugged::SORT_NONE,
topo: Rugged::SORT_TOPO,
date: Rugged::SORT_DATE
}
@rugged_sort_types.fetch(key, Rugged::SORT_NONE)
end
end end
end end
end end
...@@ -4,19 +4,11 @@ module Gitlab ...@@ -4,19 +4,11 @@ module Gitlab
# Public: Converts the provided markup into HTML. # Public: Converts the provided markup into HTML.
# #
# input - the source text in a markup format # input - the source text in a markup format
# context - a Hash with the template context:
# :commit
# :project
# :project_wiki
# :requested_path
# :ref
# #
def self.render(file_name, input, context) def self.render(file_name, input)
html = GitHub::Markup.render(file_name, input). html = GitHub::Markup.render(file_name, input).
force_encoding(input.encoding) force_encoding(input.encoding)
html = Banzai.post_process(html, context)
filter = Banzai::Filter::SanitizationFilter.new(html) filter = Banzai::Filter::SanitizationFilter.new(html)
html = filter.call.to_s html = filter.call.to_s
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
class << self class << self
def extension def extension
'Dockerfile' '.Dockerfile'
end end
def categories def categories
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
end end
def base_dir def base_dir
Rails.root.join('vendor/dockerfile') Rails.root.join('vendor/Dockerfile')
end end
def finder(project = nil) def finder(project = nil)
......
...@@ -5,7 +5,7 @@ namespace :gitlab do ...@@ -5,7 +5,7 @@ namespace :gitlab do
end end
def update(template) def update(template)
sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1] sub_dir = template.repo_url.match(/([A-Za-z-]+)\.git\z/)[1]
dir = File.join(vendor_directory, sub_dir) dir = File.join(vendor_directory, sub_dir)
unless clone_repository(template.repo_url, dir) unless clone_repository(template.repo_url, dir)
...@@ -45,7 +45,11 @@ namespace :gitlab do ...@@ -45,7 +45,11 @@ namespace :gitlab do
Template.new( Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git", "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
/(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/ /(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/
) ),
Template.new(
"https://gitlab.com/gitlab-org/Dockerfile.git",
/(\.{1,2}|LICENSE|CONTRIBUTING.md|\.Dockerfile)\z/
),
].freeze ].freeze
def vendor_directory def vendor_directory
......
require 'spec_helper'
describe Projects::WikisController do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
describe 'POST #preview_markdown' do
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
end
end
...@@ -441,4 +441,14 @@ describe ProjectsController do ...@@ -441,4 +441,14 @@ describe ProjectsController do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
describe 'POST #preview_markdown' do
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
end
end end
...@@ -521,4 +521,16 @@ describe SnippetsController do ...@@ -521,4 +521,16 @@ describe SnippetsController do
end end
end end
end end
describe 'POST #preview_markdown' do
let(:snippet) { create(:personal_snippet, :public) }
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, id: snippet, text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
end
end end
FactoryGirl.define do FactoryGirl.define do
factory :service do factory :service do
project factory: :empty_project project factory: :empty_project
type 'Service'
end
factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
project factory: :empty_project
type 'CustomIssueTrackerService'
category 'issue_tracker'
active true
properties(
project_url: 'https://project.url.com',
issues_url: 'https://issues.url.com',
new_issue_url: 'https://newissue.url.com'
)
end end
factory :kubernetes_service do factory :kubernetes_service do
......
require 'rails_helper'
feature 'Admin cohorts page', feature: true do
before do
login_as :admin
end
scenario 'See users count per month' do
2.times { create(:user) }
visit admin_cohorts_path
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
end
end
require 'spec_helper' require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper include MarkupHelper
include RepoHelpers include RepoHelpers
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
......
...@@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do ...@@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) } let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
......
...@@ -45,6 +45,33 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -45,6 +45,33 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type') expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
end end
it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
note = find('#note_note')
# Number.
page.within '.timeline-content-form' do
note.native.send_keys('7:')
end
expect(page).not_to have_selector('.atwho-view')
# ASCII letter.
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('w:')
end
expect(page).not_to have_selector('.atwho-view')
# Non-ASCII letter.
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('Ё:')
end
expect(page).not_to have_selector('.atwho-view')
end
it 'selects the first item for assignee dropdowns' do it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
find('#note_note').native.send_keys('') find('#note_note').native.send_keys('')
......
...@@ -26,7 +26,7 @@ require 'erb' ...@@ -26,7 +26,7 @@ require 'erb'
describe 'GitLab Markdown', feature: true do describe 'GitLab Markdown', feature: true do
include Capybara::Node::Matchers include Capybara::Node::Matchers
include GitlabMarkdownHelper include MarkupHelper
include MarkdownMatchers include MarkdownMatchers
# Sometimes it can be useful to see the parsed output of the Markdown document # Sometimes it can be useful to see the parsed output of the Markdown document
......
require 'spec_helper' require 'spec_helper'
require 'fileutils'
feature 'User wants to add a Dockerfile file', feature: true do feature 'User wants to add a Dockerfile file', feature: true do
before do before do
user = create(:user) user = create(:user)
project = create(:project) project = create(:project)
project.team << [user, :master] project.team << [user, :master]
login_as user login_as user
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile') visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile')
end end
...@@ -15,11 +18,14 @@ feature 'User wants to add a Dockerfile file', feature: true do ...@@ -15,11 +18,14 @@ feature 'User wants to add a Dockerfile file', feature: true do
scenario 'user can pick a Dockerfile file from the dropdown', js: true do scenario 'user can pick a Dockerfile file from the dropdown', js: true do
find('.js-dockerfile-selector').click find('.js-dockerfile-selector').click
wait_for_ajax wait_for_ajax
within '.dockerfile-selector' do within '.dockerfile-selector' do
find('.dropdown-input-field').set('HTTPd') find('.dropdown-input-field').set('HTTPd')
find('.dropdown-content li', text: 'HTTPd').click find('.dropdown-content li', text: 'HTTPd').click
end end
wait_for_ajax wait_for_ajax
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd') expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
......
require 'spec_helper'
feature 'Find files button in the tree header', feature: true do
given(:user) { create(:user) }
given(:project) { create(:project) }
background do
login_as(user)
project.team << [user, :developer]
end
scenario 'project main screen' do
visit namespace_project_path(
project.namespace,
project
)
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
scenario 'project tree screen' do
visit namespace_project_tree_path(
project.namespace,
project,
project.default_branch
)
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
end
...@@ -93,4 +93,28 @@ feature 'Project milestone', :feature do ...@@ -93,4 +93,28 @@ feature 'Project milestone', :feature do
def milestone_path def milestone_path
namespace_project_milestone_path(project.namespace, project, milestone) namespace_project_milestone_path(project.namespace, project, milestone)
end end
context 'when project has an issue' do
before do
create(:issue, project: project, milestone: milestone)
visit namespace_project_milestone_path(project.namespace, project, milestone)
end
describe 'the collapsed sidebar' do
before do
find('.milestone-sidebar .gutter-toggle').click
end
it 'shows the total MR and issue counts' do
find('.milestone-sidebar .block', match: :first)
blocks = all('.milestone-sidebar .block')
aggregate_failures 'MR and issue blocks' do
expect(blocks[3]).to have_content 1
expect(blocks[5]).to have_content 0
end
end
end
end
end end
require 'spec_helper' require 'spec_helper'
describe IssuesFinder do describe IssuesFinder do
let(:user) { create(:user) } set(:user) { create(:user) }
let(:user2) { create(:user) } set(:user2) { create(:user) }
let(:project1) { create(:empty_project) } set(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) } set(:project2) { create(:empty_project) }
let(:milestone) { create(:milestone, project: project1) } set(:milestone) { create(:milestone, project: project1) }
let(:label) { create(:label, project: project2) } set(:label) { create(:label, project: project2) }
let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } set(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } set(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') } set(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') }
describe '#execute' do describe '#execute' do
let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } set(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
let!(:label_link) { create(:label_link, label: label, target: issue2) } set(:label_link) { create(:label_link, label: label, target: issue2) }
let(:search_user) { user } let(:search_user) { user }
let(:params) { {} } let(:params) { {} }
let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
before do before(:context) do
project1.team << [user, :master] project1.team << [user, :master]
project2.team << [user, :developer] project2.team << [user, :developer]
project2.team << [user2, :developer] project2.team << [user2, :developer]
......
...@@ -116,7 +116,7 @@ Linking to a file relative to this project's repository should work. ...@@ -116,7 +116,7 @@ Linking to a file relative to this project's repository should work.
Because life would be :zzz: without Emoji, right? :rocket: Because life would be :zzz: without Emoji, right? :rocket:
Get ready for the Emoji :bomb:: :+1::-1::ok_hand::wave::v::raised_hand::muscle: Get ready for the Emoji :bomb: : :+1: :-1: :ok_hand: :wave: :v: :raised_hand: :muscle:
### TableOfContentsFilter ### TableOfContentsFilter
......
...@@ -239,33 +239,6 @@ describe ApplicationHelper do ...@@ -239,33 +239,6 @@ describe ApplicationHelper do
end end
end end
describe 'render_markup' do
let(:content) { 'Noël' }
let(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
it 'preserves encoding' do
expect(content.encoding.name).to eq('UTF-8')
expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
it "delegates to #markdown when file name corresponds to Markdown" do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown).and_return('NOEL')
expect(helper.render_markup('foo.md', content)).to eq('NOEL')
end
it "delegates to #asciidoc when file name corresponds to AsciiDoc" do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc).and_return('NOEL')
expect(helper.render_markup('foo.adoc', content)).to eq('NOEL')
end
end
describe '#active_when' do describe '#active_when' do
it { expect(helper.active_when(true)).to eq('active') } it { expect(helper.active_when(true)).to eq('active') }
it { expect(helper.active_when(false)).to eq(nil) } it { expect(helper.active_when(false)).to eq(nil) }
......
require 'spec_helper' require 'spec_helper'
describe GitlabMarkdownHelper do describe MarkupHelper do
include ApplicationHelper
let!(:project) { create(:project, :repository) } let!(:project) { create(:project, :repository) }
let(:user) { create(:user, username: 'gfm') } let(:user) { create(:user, username: 'gfm') }
...@@ -111,9 +109,9 @@ describe GitlabMarkdownHelper do ...@@ -111,9 +109,9 @@ describe GitlabMarkdownHelper do
end end
it 'replaces commit message with emoji to link' do it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo') actual = link_to_gfm(':book: Book', '/foo')
expect(actual). expect(actual).
to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>' to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
end end
end end
...@@ -128,7 +126,7 @@ describe GitlabMarkdownHelper do ...@@ -128,7 +126,7 @@ describe GitlabMarkdownHelper do
it "uses Wiki pipeline for markdown files" do it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown) allow(@wiki).to receive(:format).and_return(:markdown)
expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page") expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page")
helper.render_wiki_content(@wiki) helper.render_wiki_content(@wiki)
end end
...@@ -136,7 +134,7 @@ describe GitlabMarkdownHelper do ...@@ -136,7 +134,7 @@ describe GitlabMarkdownHelper do
it "uses Asciidoctor for asciidoc files" do it "uses Asciidoctor for asciidoc files" do
allow(@wiki).to receive(:format).and_return(:asciidoc) allow(@wiki).to receive(:format).and_return(:asciidoc)
expect(helper).to receive(:asciidoc).with('wiki content') expect(helper).to receive(:asciidoc_unsafe).with('wiki content')
helper.render_wiki_content(@wiki) helper.render_wiki_content(@wiki)
end end
...@@ -151,6 +149,29 @@ describe GitlabMarkdownHelper do ...@@ -151,6 +149,29 @@ describe GitlabMarkdownHelper do
end end
end end
describe 'markup' do
let(:content) { 'Noël' }
it 'preserves encoding' do
expect(content.encoding.name).to eq('UTF-8')
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
it "delegates to #markdown_unsafe when file name corresponds to Markdown" do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
expect(helper.markup('foo.md', content)).to eq('NOEL')
end
it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
expect(helper.markup('foo.adoc', content)).to eq('NOEL')
end
end
describe '#first_line_in_markdown' do describe '#first_line_in_markdown' do
it 'truncates Markdown properly' do it 'truncates Markdown properly' do
text = "@#{user.username}, can you look at this?\nHello world\n" text = "@#{user.username}, can you look at this?\nHello world\n"
......
...@@ -93,7 +93,7 @@ describe ProjectsHelper do ...@@ -93,7 +93,7 @@ describe ProjectsHelper do
end end
it "includes a version" do it "includes a version" do
expect(helper.project_list_cache_key(project)).to include("v2.3") expect(helper.project_list_cache_key(project).last).to start_with('v')
end end
it "includes the pipeline status when there is a status" do it "includes the pipeline status when there is a status" do
......
...@@ -26,6 +26,10 @@ describe('Filtered Search Manager', () => { ...@@ -26,6 +26,10 @@ describe('Filtered Search Manager', () => {
element.dispatchEvent(event); element.dispatchEvent(event);
} }
function getVisualTokens() {
return tokensContainer.querySelectorAll('.js-visual-token');
}
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<div class="filtered-search-box"> <div class="filtered-search-box">
...@@ -170,11 +174,37 @@ describe('Filtered Search Manager', () => { ...@@ -170,11 +174,37 @@ describe('Filtered Search Manager', () => {
}); });
}); });
describe('removeSelectedToken', () => { describe('removeToken', () => {
function getVisualTokens() { it('removes token even when it is already selected', () => {
return tokensContainer.querySelectorAll('.js-visual-token'); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
} FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
tokensContainer.querySelector('.js-visual-token .remove-token').click();
expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
});
describe('unselected token', () => {
beforeEach(() => {
spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
);
tokensContainer.querySelector('.js-visual-token .remove-token').click();
});
it('removes token when remove button is selected', () => {
expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
});
it('calls removeSelectedToken', () => {
expect(manager.removeSelectedToken).toHaveBeenCalled();
});
});
});
describe('removeSelectedTokenKeydown', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
...@@ -224,6 +254,31 @@ describe('Filtered Search Manager', () => { ...@@ -224,6 +254,31 @@ describe('Filtered Search Manager', () => {
}); });
}); });
describe('removeSelectedToken', () => {
beforeEach(() => {
spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
manager.removeSelectedToken();
});
it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
});
it('calls handleInputPlaceholder', () => {
expect(manager.handleInputPlaceholder).toHaveBeenCalled();
});
it('calls toggleClearSearchButton', () => {
expect(manager.toggleClearSearchButton).toHaveBeenCalled();
});
it('calls update dropdown offset', () => {
expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
});
});
describe('unselects token', () => { describe('unselects token', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
......
...@@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
}); });
it('contains value container div', () => {
expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything());
});
it('contains value div', () => { it('contains value div', () => {
expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything()); expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything());
}); });
it('contains selectable class', () => { it('contains selectable class', () => {
...@@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => {
it('contains button role', () => { it('contains button role', () => {
expect(tokenElement.getAttribute('role')).toEqual('button'); expect(tokenElement.getAttribute('role')).toEqual('button');
}); });
describe('remove token', () => {
it('contains remove-token button', () => {
expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything());
});
it('contains fa-close icon', () => {
expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything());
});
});
}); });
describe('addVisualTokenElement', () => { describe('addVisualTokenElement', () => {
......
...@@ -10,7 +10,12 @@ class FilteredSearchSpecHelper { ...@@ -10,7 +10,12 @@ class FilteredSearchSpecHelper {
li.innerHTML = ` li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button"> <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
<div class="name">${name}</div> <div class="name">${name}</div>
<div class="value-container">
<div class="value">${value}</div> <div class="value">${value}</div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div> </div>
`; `;
......
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import '~/flash'; import '~/flash';
(() => { describe('Mini Pipeline Graph Dropdown', () => {
describe('Mini Pipeline Graph Dropdown', () => {
preloadFixtures('static/mini_dropdown_graph.html.raw'); preloadFixtures('static/mini_dropdown_graph.html.raw');
beforeEach(() => { beforeEach(() => {
...@@ -29,7 +28,10 @@ import '~/flash'; ...@@ -29,7 +28,10 @@ import '~/flash';
describe('When dropdown is clicked', () => { describe('When dropdown is clicked', () => {
it('should call getBuildsList', () => { it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); const getBuildsListSpy = spyOn(
MiniPipelineGraph.prototype,
'getBuildsList',
).and.callFake(function () {});
new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents(); new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
...@@ -68,5 +70,17 @@ import '~/flash'; ...@@ -68,5 +70,17 @@ import '~/flash';
expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true); expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
}); });
}); });
it('should close the dropdown when request returns an error', (done) => {
spyOn($, 'ajax').and.callFake(options => options.error());
new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
setTimeout(() => {
expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
done();
}, 0);
}); });
})(); });
...@@ -63,4 +63,19 @@ describe('Pipelines Stage', () => { ...@@ -63,4 +63,19 @@ describe('Pipelines Stage', () => {
expect(minifiedComponent).toContain(expectedSVG); expect(minifiedComponent).toContain(expectedSVG);
}); });
}); });
describe('when request fails', () => {
it('closes dropdown', () => {
spyOn($, 'ajax').and.callFake(options => options.error());
const StageComponent = Vue.extend(Stage);
const component = new StageComponent({
propsData: { stage: { status: { icon: 'foo' } } },
}).$mount();
expect(
component.$el.classList.contains('open'),
).toEqual(false);
});
});
}); });
import Vue from 'vue';
import timeAgo from '~/pipelines/components/time_ago';
describe('Timeago component', () => {
let TimeAgo;
beforeEach(() => {
TimeAgo = Vue.extend(timeAgo);
});
describe('with duration', () => {
it('should render duration and timer svg', () => {
const component = new TimeAgo({
propsData: {
duration: 10,
finishedTime: '',
},
}).$mount();
expect(component.$el.querySelector('.duration')).toBeDefined();
expect(component.$el.querySelector('.duration svg')).toBeDefined();
});
});
describe('without duration', () => {
it('should not render duration and timer svg', () => {
const component = new TimeAgo({
propsData: {
duration: 0,
finishedTime: '',
},
}).$mount();
expect(component.$el.querySelector('.duration')).toBe(null);
});
});
describe('with finishedTime', () => {
it('should render time and calendar icon', () => {
const component = new TimeAgo({
propsData: {
duration: 0,
finishedTime: '2017-04-26T12:40:23.277Z',
},
}).$mount();
expect(component.$el.querySelector('.finished-at')).toBeDefined();
expect(component.$el.querySelector('.finished-at i.fa-calendar')).toBeDefined();
expect(component.$el.querySelector('.finished-at time')).toBeDefined();
});
});
describe('without finishedTime', () => {
it('should not render time and calendar icon', () => {
const component = new TimeAgo({
propsData: {
duration: 0,
finishedTime: '',
},
}).$mount();
expect(component.$el.querySelector('.finished-at')).toBe(null);
});
});
});
...@@ -68,9 +68,9 @@ describe Banzai::Filter::EmojiFilter, lib: true do ...@@ -68,9 +68,9 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('gl-emoji').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'matches multiple emoji in a row' do it 'does not match multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
expect(doc.css('gl-emoji').size).to eq 3 expect(doc.css('gl-emoji').size).to eq 0
end end
it 'unicode matches multiple emoji in a row' do it 'unicode matches multiple emoji in a row' do
...@@ -83,6 +83,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do ...@@ -83,6 +83,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
expect(doc.css('gl-emoji').size).to eq 6 expect(doc.css('gl-emoji').size).to eq 6
end end
it 'does not match emoji in a string' do
doc = filter("'2a00:a4c0:100::1'")
expect(doc.css('gl-emoji').size).to eq 0
end
it 'has a data-name attribute' do it 'has a data-name attribute' do
doc = filter(':-1:') doc = filter(':-1:')
expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown' expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
......
...@@ -22,24 +22,7 @@ module Gitlab ...@@ -22,24 +22,7 @@ module Gitlab
expect(Asciidoctor).to receive(:convert) expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html) .with(input, expected_asciidoc_opts).and_return(html)
expect( render(input, context) ).to eql html expect(render(input)).to eq(html)
end
context "with asciidoc_opts" do
let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :safe,
backend: :gitlab_html5,
attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
}
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
render(input, context, asciidoc_opts)
end
end end
context "XSS" do context "XSS" do
...@@ -60,7 +43,7 @@ module Gitlab ...@@ -60,7 +43,7 @@ module Gitlab
links.each do |name, data| links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do it "does not convert dangerous #{name} into HTML" do
expect(render(data[:input], context)).to eql data[:output] expect(render(data[:input])).to eq(data[:output])
end end
end end
end end
......
...@@ -1031,6 +1031,35 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1031,6 +1031,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
describe '#find_commits' do
it 'should return a return a collection of commits' do
commits = repository.find_commits
expect(commits).not_to be_empty
expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) )
end
context 'while applying a sort order based on the `order` option' do
it "allows ordering topologically (no parents shown before their children)" do
expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO)
repository.find_commits(order: :topo)
end
it "allows ordering by date" do
expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE)
repository.find_commits(order: :date)
end
it "applies no sorting by default" do
expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE)
repository.find_commits
end
end
end
describe '#branches with deleted branch' do describe '#branches with deleted branch' do
before(:each) do before(:each) do
ref = double() ref = double()
......
...@@ -6980,28 +6980,6 @@ ...@@ -6980,28 +6980,6 @@
], ],
"services": [ "services": [
{
"id": 164,
"title": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:07.372Z",
"updated_at": "2016-06-14T15:02:07.372Z",
"active": false,
"properties": {
},
"template": false,
"push_events": true,
"issues_events": true,
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
"build_events": true,
"category": "issue_tracker",
"type": "CustomIssueTrackerService",
"default": true,
"wiki_page_events": true
},
{ {
"id": 100, "id": 100,
"title": "JetBrains TeamCity CI", "title": "JetBrains TeamCity CI",
...@@ -7019,6 +6997,7 @@ ...@@ -7019,6 +6997,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "TeamcityService",
"category": "ci", "category": "ci",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7040,6 +7019,7 @@ ...@@ -7040,6 +7019,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"pipeline_events": true, "pipeline_events": true,
"type": "SlackService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7061,6 +7041,7 @@ ...@@ -7061,6 +7041,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "RedmineService",
"category": "issue_tracker", "category": "issue_tracker",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7082,6 +7063,7 @@ ...@@ -7082,6 +7063,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "PushoverService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7103,6 +7085,7 @@ ...@@ -7103,6 +7085,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "PivotalTrackerService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7125,6 +7108,7 @@ ...@@ -7125,6 +7108,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "JiraService",
"category": "issue_tracker", "category": "issue_tracker",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7146,6 +7130,7 @@ ...@@ -7146,6 +7130,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "IrkerService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7167,6 +7152,7 @@ ...@@ -7167,6 +7152,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"pipeline_events": true, "pipeline_events": true,
"type": "HipchatService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7188,6 +7174,7 @@ ...@@ -7188,6 +7174,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "GemnasiumService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7209,6 +7196,7 @@ ...@@ -7209,6 +7196,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "FlowdockService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7230,6 +7218,7 @@ ...@@ -7230,6 +7218,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "ExternalWikiService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7251,6 +7240,7 @@ ...@@ -7251,6 +7240,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "EmailsOnPushService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7272,6 +7262,7 @@ ...@@ -7272,6 +7262,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "DroneCiService",
"category": "ci", "category": "ci",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7293,6 +7284,7 @@ ...@@ -7293,6 +7284,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "CustomIssueTrackerService",
"category": "issue_tracker", "category": "issue_tracker",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7314,6 +7306,7 @@ ...@@ -7314,6 +7306,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "CampfireService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7335,6 +7328,7 @@ ...@@ -7335,6 +7328,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "BuildkiteService",
"category": "ci", "category": "ci",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7356,6 +7350,7 @@ ...@@ -7356,6 +7350,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "BambooService",
"category": "ci", "category": "ci",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7377,6 +7372,7 @@ ...@@ -7377,6 +7372,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "AssemblaService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
...@@ -7398,6 +7394,7 @@ ...@@ -7398,6 +7394,7 @@
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
"build_events": true, "build_events": true,
"type": "AssemblaService",
"category": "common", "category": "common",
"default": false, "default": false,
"wiki_page_events": true "wiki_page_events": true
......
...@@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do ...@@ -60,7 +60,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
end end
context 'original service exists' do context 'original service exists' do
let(:service_id) { Service.create(project: project).id } let(:service_id) { create(:service, project: project).id }
it 'does not have the original service_id' do it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id) expect(created_object.service_id).not_to eq(service_id)
......
...@@ -13,7 +13,7 @@ describe Gitlab::OtherMarkup, lib: true do ...@@ -13,7 +13,7 @@ describe Gitlab::OtherMarkup, lib: true do
} }
links.each do |name, data| links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do it "does not convert dangerous #{name} into HTML" do
expect(render(data[:file], data[:input], context)).to eql data[:output] expect(render(data[:file], data[:input])).to eq(data[:output])
end end
end end
end end
......
...@@ -9,4 +9,25 @@ describe Network::Graph, models: true do ...@@ -9,4 +9,25 @@ describe Network::Graph, models: true do
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } ) expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end end
describe "#commits" do
let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) }
it "returns a list of commits" do
commits = graph.commits
expect(commits).not_to be_empty
expect(commits).to all( be_kind_of(Network::Commit) )
end
it "sorts the commits by commit date (descending)" do
# Remove duplicate timestamps because they make it harder to
# assert that the commits are sorted as expected.
commits = graph.commits.uniq(&:date)
sorted_commits = commits.sort_by(&:date).reverse
expect(commits).not_to be_empty
expect(commits.map(&:id)).to eq(sorted_commits.map(&:id))
end
end
end end
...@@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do ...@@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do
let(:service) { RedmineService.new(project: project, active: true) } let(:service) { RedmineService.new(project: project, active: true) }
before do before do
create(:service, project: project, active: true, category: 'issue_tracker') create(:custom_issue_tracker_service, project: project)
end end
context 'when service is changed manually by user' do context 'when service is changed manually by user' do
......
...@@ -1946,9 +1946,9 @@ describe Repository, models: true do ...@@ -1946,9 +1946,9 @@ describe Repository, models: true do
describe '#refresh_method_caches' do describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches). expect(repository).to receive(:expire_method_caches).
with(%i(readme license_blob license_key)) with(%i(rendered_readme license_blob license_key))
expect(repository).to receive(:readme) expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob) expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key) expect(repository).to receive(:license_key)
......
...@@ -6,6 +6,10 @@ describe Service, models: true do ...@@ -6,6 +6,10 @@ describe Service, models: true do
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
end end
describe 'Validations' do
it { is_expected.to validate_presence_of(:type) }
end
describe "Test Button" do describe "Test Button" do
before do before do
@service = Service.new @service = Service.new
......
...@@ -427,6 +427,7 @@ describe API::Helpers do ...@@ -427,6 +427,7 @@ describe API::Helpers do
context 'current_user is nil' do context 'current_user is nil' do
before do before do
expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil)
end end
it 'returns a 401 response' do it 'returns a 401 response' do
...@@ -435,13 +436,38 @@ describe API::Helpers do ...@@ -435,13 +436,38 @@ describe API::Helpers do
end end
context 'current_user is present' do context 'current_user is present' do
let(:user) { build(:user) }
before do before do
expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(User.new) expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
end end
it 'does not raise an error' do it 'does not raise an error' do
expect { authenticate! }.not_to raise_error expect { authenticate! }.not_to raise_error
end end
end end
context 'current_user is blocked' do
let(:user) { build(:user, :blocked) }
before do
expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
end
it 'raises an error' do
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
end
it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do
admin_user = build(:user, :admin)
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user)
expect { authenticate! }.not_to raise_error
end
end
end end
end end
...@@ -3,14 +3,17 @@ require 'spec_helper' ...@@ -3,14 +3,17 @@ require 'spec_helper'
describe API::Issues do describe API::Issues do
include EmailHelpers include EmailHelpers
let(:user) { create(:user) } set(:user) { create(:user) }
set(:project) do
create(:empty_project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
let(:guest) { create(:user) } set(:guest) { create(:user) }
let(:author) { create(:author) } set(:author) { create(:author) }
let(:assignee) { create(:assignee) } set(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) } let(:admin) { create(:user, :admin) }
let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let(:issue_title) { 'foo' } let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' } let(:issue_description) { 'closed' }
let!(:closed_issue) do let!(:closed_issue) do
...@@ -43,19 +46,19 @@ describe API::Issues do ...@@ -43,19 +46,19 @@ describe API::Issues do
title: issue_title, title: issue_title,
description: issue_description description: issue_description
end end
let!(:label) do set(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project) create(:label, title: 'label', color: '#FFAABB', project: project)
end end
let!(:label_link) { create(:label_link, label: label, target: issue) } let!(:label_link) { create(:label_link, label: label, target: issue) }
let!(:milestone) { create(:milestone, title: '1.0.0', project: project) } set(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let!(:empty_milestone) do set(:empty_milestone) do
create(:milestone, title: '2.0.0', project: project) create(:milestone, title: '2.0.0', project: project)
end end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
let(:no_milestone_title) { URI.escape(Milestone::None.title) } let(:no_milestone_title) { URI.escape(Milestone::None.title) }
before do before(:all) do
project.team << [user, :reporter] project.team << [user, :reporter]
project.team << [guest, :guest] project.team << [guest, :guest]
end end
...@@ -70,6 +73,8 @@ describe API::Issues do ...@@ -70,6 +73,8 @@ describe API::Issues do
end end
context "when authenticated" do context "when authenticated" do
let(:first_issue) { json_response.first }
it "returns an array of issues" do it "returns an array of issues" do
get api("/issues", user) get api("/issues", user)
...@@ -79,46 +84,46 @@ describe API::Issues do ...@@ -79,46 +84,46 @@ describe API::Issues do
end end
it 'returns an array of closed issues' do it 'returns an array of closed issues' do
get api('/issues?state=closed', user) get api('/issues', user), state: :closed
expect_paginated_array_response(size: 1) expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(closed_issue.id) expect(first_issue['id']).to eq(closed_issue.id)
end end
it 'returns an array of opened issues' do it 'returns an array of opened issues' do
get api('/issues?state=opened', user) get api('/issues', user), state: :opened
expect_paginated_array_response(size: 1) expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id) expect(first_issue['id']).to eq(issue.id)
end end
it 'returns an array of all issues' do it 'returns an array of all issues' do
get api('/issues?state=all', user) get api('/issues', user), state: :all
expect_paginated_array_response(size: 2) expect_paginated_array_response(size: 2)
expect(json_response.first['id']).to eq(issue.id) expect(first_issue['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id) expect(json_response.second['id']).to eq(closed_issue.id)
end end
it 'returns issues matching given search string for title' do it 'returns issues matching given search string for title' do
get api("/issues?search=#{issue.title}", user) get api("/issues", user), search: issue.title
expect_paginated_array_response(size: 1) expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id) expect(json_response.first['id']).to eq(issue.id)
end end
it 'returns issues matching given search string for description' do it 'returns issues matching given search string for description' do
get api("/issues?search=#{issue.description}", user) get api("/issues", user), search: issue.description
expect_paginated_array_response(size: 1) expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id) expect(first_issue['id']).to eq(issue.id)
end end
it 'returns an array of labeled issues' do it 'returns an array of labeled issues' do
get api("/issues?labels=#{label.title}", user) get api("/issues", user), labels: label.title
expect_paginated_array_response(size: 1) expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title]) expect(first_issue['labels']).to eq([label.title])
end end
it 'returns an array of labeled issues when all labels matches' do it 'returns an array of labeled issues when all labels matches' do
...@@ -135,13 +140,13 @@ describe API::Issues do ...@@ -135,13 +140,13 @@ describe API::Issues do
end end
it 'returns an empty array if no issue matches labels' do it 'returns an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user) get api('/issues', user), labels: 'foo,bar'
expect_paginated_array_response(size: 0) expect_paginated_array_response(size: 0)
end end
it 'returns an array of labeled issues matching given state' do it 'returns an array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user) get api("/issues", user), labels: label.title, state: :opened
expect_paginated_array_response(size: 1) expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title]) expect(json_response.first['labels']).to eq([label.title])
......
...@@ -26,6 +26,7 @@ describe API::MergeRequests do ...@@ -26,6 +26,7 @@ describe API::MergeRequests do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns authentication error" do
get api("/projects/#{project.id}/merge_requests") get api("/projects/#{project.id}/merge_requests")
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
end end
......
...@@ -27,6 +27,22 @@ describe Projects::CreateService, '#execute', services: true do ...@@ -27,6 +27,22 @@ describe Projects::CreateService, '#execute', services: true do
end end
end end
context "admin creates project with other user's namespace_id" do
it 'sets the correct permissions' do
admin = create(:admin)
opts = {
name: 'GitLab',
namespace_id: user.namespace.id
}
project = create_project(admin, opts)
expect(project).to be_persisted
expect(project.owner).to eq(user)
expect(project.team.masters).to include(user, admin)
expect(project.namespace).to eq(user.namespace)
end
end
context 'group namespace' do context 'group namespace' do
let(:group) do let(:group) do
create(:group).tap do |group| create(:group).tap do |group|
......
...@@ -602,7 +602,7 @@ describe SystemNoteService, services: true do ...@@ -602,7 +602,7 @@ describe SystemNoteService, services: true do
end end
shared_examples 'cross project mentionable' do shared_examples 'cross project mentionable' do
include GitlabMarkdownHelper include MarkupHelper
it 'contains cross reference to new noteable' do it 'contains cross reference to new noteable' do
expect(subject.note).to include cross_project_reference(new_project, new_noteable) expect(subject.note).to include cross_project_reference(new_project, new_noteable)
......
...@@ -350,7 +350,7 @@ describe 'gitlab:app namespace rake task' do ...@@ -350,7 +350,7 @@ describe 'gitlab:app namespace rake task' do
end end
it 'name has human readable time' do it 'name has human readable time' do
expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_gitlab_backup.tar$/) expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+(-pre)?_gitlab_backup.tar$/)
end end
end end
end # gitlab:app namespace end # gitlab:app namespace
The canonical repository for `Dockerfile` templates is
https://gitlab.com/gitlab-org/Dockerfile.
GitLab only mirrors the templates. Please submit your merge requests to
https://gitlab.com/gitlab-org/Dockerfile.
The MIT License (MIT)
Copyright (c) 2016-2017 GitLab.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
FROM php:7.0-apache
# Customize any core extensions here
#RUN apt-get update && apt-get install -y \
# libfreetype6-dev \
# libjpeg62-turbo-dev \
# libmcrypt-dev \
# libpng12-dev \
# && docker-php-ext-install -j$(nproc) iconv mcrypt \
# && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
# && docker-php-ext-install -j$(nproc) gd
COPY config/php.ini /usr/local/etc/php/
COPY src/ /var/www/html/
FROM python:2.7
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt
COPY . /usr/src/app
CMD ["python", "app.py"]
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