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,16 +273,21 @@ class FilteredSearchManager { ...@@ -256,16 +273,21 @@ 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) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); this.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
} }
} }
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.dropdownManager.updateCurrentDropdownOffset();
}
onClearSearch(e) { onClearSearch(e) {
e.preventDefault(); e.preventDefault();
this.clearSearch(); this.clearSearch();
......
...@@ -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"></div> <div class="value-container">
<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,9 +52,10 @@ export default { ...@@ -46,9 +52,10 @@ 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'))
e.stopPropagation(); .on('click', (e) => {
}); e.stopPropagation();
});
}, },
}, },
computed: { computed: {
...@@ -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 end
def other_markup(file_name, text) def render_wiki_content(wiki_page)
Gitlab::OtherMarkup.render( text = wiki_page.content
file_name, return '' unless text.present?
text,
project: @project, context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
current_user: (current_user if defined?(current_user)),
html =
case wiki_page.format
when :markdown
markdown_unsafe(text, context)
when :asciidoc
asciidoc_unsafe(text)
else
wiki_page.formatted_content.html_safe
end
# RelativeLinkFilter banzai_postprocess(html, context)
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 markup_unsafe(file_name, text, context = {})
# as Markdown. HTML tags in the parsed output are not counted toward the return '' unless text.present?
# +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? if gitlab_markdown?(file_name)
end Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context))
elsif asciidoc?(file_name)
def render_wiki_content(wiki_page) asciidoc_unsafe(text)
case wiki_page.format elsif plain?(file_name)
when :markdown content_tag :pre, class: 'plain-readme' do
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) text
when :asciidoc end
asciidoc(wiki_page.content)
else else
wiki_page.formatted_content.html_safe other_markup_unsafe(file_name, text)
end 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,18 +38,21 @@ ...@@ -37,18 +38,21 @@
= markdown_field(project, :description) = markdown_field(project, :description)
.controls .controls
- if project.archived .prepend-top-0
%span.prepend-left-10.label.label-warning archived - if project.archived
- if project.pipeline_status.has_status? %span.prepend-left-10.label.label-warning archived
%span.prepend-left-10 - if project.pipeline_status.has_status?
= render_project_pipeline_status(project.pipeline_status) %span.prepend-left-10
- if forks = render_project_pipeline_status(project.pipeline_status)
%span.prepend-left-10 - if forks
= icon('code-fork') %span.prepend-left-10
= number_with_delimiter(project.forks_count) = icon('code-fork')
- if stars = number_with_delimiter(project.forks_count)
%span.prepend-left-10 - if stars
= icon('star') %span.prepend-left-10
= number_with_delimiter(project.star_count) = icon('star')
%span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } = number_with_delimiter(project.star_count)
= visibility_level_icon(project.visibility_level, fw: true) %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)
.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
......
This diff is collapsed.
...@@ -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: DEFAULT_ADOC_ATTRS }
attributes: []
)
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) }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment