Commit 18455d74 authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge branch 'master' into balsalmiq-support

parents cc248f3e 4d4aefc5
......@@ -11,6 +11,7 @@ variables:
NODE_ENV: "test"
SIMPLECOV: "true"
GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
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.)
#### Results of GitLab environment info
<details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`)
......@@ -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:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
</pre>
</details>
#### Results of GitLab application Check
<details>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`)
......@@ -47,6 +50,7 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing)
</pre>
</details>
### Possible fixes
......
......@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.1.1 (2017-04-26)
- Add a transaction around move_issues_to_ghost_user. !10465
- Properly expire cache for all MRs of a pipeline. !10770
- Add sub-nav for Project Integration Services edit page. !10813
- Fix missing duration for blocked pipelines. !10856
- Fix lastest commit status text on main project page. !10863
- Add index on ci_builds.updated_at. !10870 (blackst0ne)
- Fix 500 error due to trying to show issues from pending deleting projects. !10906
- Ensures that OAuth/LDAP/SAML users don't need to be confirmed.
- Ensure replying to an individual note by email creates a note with its own discussion ID.
- Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled.
- Fix usage ping docs link from empty cohorts page.
- Eliminate N+1 queries in loading namespaces for every issuable in milestones.
## 9.1.0 (2017-04-22)
- Added merge requests empty state. !7342
......
......@@ -17,6 +17,8 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
......@@ -142,7 +144,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2.7'
gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
......@@ -186,7 +188,7 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
gem 'slack-notifier', '~> 1.5.1'
# Asana integration
gem 'asana', '~> 0.4.0'
gem 'asana', '~> 0.6.0'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
......@@ -291,6 +293,7 @@ group :development, :test do
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
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)
gem 'minitest', '~> 5.7.0'
......@@ -345,7 +348,7 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
# OAuth
gem 'oauth2', '~> 1.2.0'
gem 'oauth2', '~> 1.3.0'
# Soft deletion
gem 'paranoia', '~> 2.2'
......
......@@ -47,7 +47,7 @@ GEM
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
asana (0.4.0)
asana (0.6.0)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
......@@ -193,10 +193,10 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
faraday (0.9.2)
faraday (0.11.0)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0)
faraday (>= 0.7.4, < 0.10)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
faraday_middleware
multi_json
......@@ -429,7 +429,7 @@ GEM
multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mail (2.6.5)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
......@@ -454,15 +454,15 @@ GEM
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.2.0)
faraday (>= 0.8, < 0.10)
oauth2 (1.3.1)
faraday (>= 0.8, < 0.12)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
oj (2.17.5)
omniauth (1.4.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
......@@ -603,7 +603,7 @@ GEM
json
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.2.2)
redis (3.3.3)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
......@@ -659,6 +659,7 @@ GEM
rspec-support (~> 3.5.0)
rspec-retry (0.4.5)
rspec-core
rspec-set (0.1.3)
rspec-support (3.5.0)
rspec_profiling (0.0.5)
activerecord
......@@ -716,11 +717,11 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
sidekiq (4.2.10)
sidekiq (5.0.0)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
redis (~> 3.3, >= 3.3.3)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
......@@ -853,7 +854,7 @@ DEPENDENCIES
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.4.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0)
......@@ -891,6 +892,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
faraday (~> 0.11.0)
ffaker (~> 2.4)
flay (~> 2.8.0)
fog-aws (~> 0.9)
......@@ -943,7 +945,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0)
oauth2 (~> 1.3.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
......@@ -988,6 +990,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.47.1)
rubocop-rspec (~> 1.15.0)
......@@ -1004,7 +1007,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7)
sidekiq (~> 5.0)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (~> 0.14.0)
......
......@@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
......
<script>
/* eslint-disable no-new */
/* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......@@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
export default Vue.component('environment-component', {
export default {
components: {
'environment-table': EnvironmentTable,
......@@ -140,76 +141,90 @@ export default Vue.component('environment-component', {
});
},
},
template: `
<div :class="cssContainerClass">
<div class="top-area">
<ul v-if="!isLoading" class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-create">
New environment
};
</script>
<template>
<div :class="cssContainerClass">
<div class="top-area">
<ul
v-if="!isLoading"
class="nav-links">
<li :class="{ active: scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</div>
</li>
</ul>
<div
v-if="canCreateEnvironmentParsed && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
New environment
</a>
</div>
</div>
<div class="content-list environments-container">
<div
class="environments-list-loading text-center"
v-if="isLoading">
<div class="content-list environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</p>
<a v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</div>
<div
class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</a>
</div>
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation" />
</div>
`,
});
</div>
</template>
import EnvironmentsComponent from './components/environment';
import Vue from 'vue';
import EnvironmentsComponent from './components/environment.vue';
$(() => {
window.gl = window.gl || {};
if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true);
}
gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
});
});
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-list-view',
components: {
'environments-table-app': EnvironmentsComponent,
},
render: createElement => createElement('environments-table-app'),
}));
import EnvironmentsFolderComponent from './environments_folder_view';
import Vue from 'vue';
import EnvironmentsFolderComponent from './environments_folder_view.vue';
$(() => {
window.gl = window.gl || {};
if (gl.EnvironmentsListFolderApp) {
gl.EnvironmentsListFolderApp.$destroy(true);
}
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-folder-list-view',
components: {
'environments-folder-app': EnvironmentsFolderComponent,
},
render: createElement => createElement('environments-folder-app'),
}));
<script>
/* eslint-disable no-new */
/* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......@@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', {
export default {
components: {
'environment-table': EnvironmentTable,
'table-pagination': TablePaginationComponent,
......@@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', {
return param;
},
},
};
</script>
<template>
<div :class="cssContainerClass">
<div
class="top-area"
v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li :class="{ active: scope === null || scope === 'available' }">
<a
:href="availablePath"
class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a
:href="stoppedPath"
class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
template: `
<div :class="cssContainerClass">
<div class="top-area" v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div class="environments-container">
<div
class="environments-list-loading text-center"
v-if="isLoading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</div>
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"/>
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"/>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div>
</div>
`,
});
</div>
</template>
......@@ -77,13 +77,14 @@ class FilteredSearchManager {
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.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.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.removeTokenWrapper = this.removeToken.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
......@@ -96,12 +97,13 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
......@@ -117,12 +119,13 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
......@@ -195,14 +198,28 @@ class FilteredSearchManager {
static selectToken(e) {
const button = e.target.closest('.selectable');
const removeButtonSelected = e.target.closest('.remove-token');
if (button) {
if (!removeButtonSelected && button) {
e.preventDefault();
e.stopPropagation();
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) {
const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
......@@ -248,16 +265,21 @@ class FilteredSearchManager {
}
}
removeSelectedToken(e) {
removeSelectedTokenKeydown(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.removeSelectedToken();
}
}
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.dropdownManager.updateCurrentDropdownOffset();
}
onClearSearch(e) {
e.preventDefault();
this.clearSearch();
......
......@@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
}
static selectToken(tokenButton) {
static selectToken(tokenButton, forceSelection = false) {
const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens();
if (!selected) {
if (!selected || forceSelection) {
tokenButton.classList.add('selected');
}
}
......@@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
return `
<div class="selectable" role="button">
<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>
`;
}
......@@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
if (value) {
const button = lastVisualToken.querySelector('.selectable');
button.removeChild(value);
const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer);
lastVisualToken.innerHTML = button.innerHTML;
} else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
......
......@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
// Creates the variables for setting up GFM auto-completion
window.gl = window.gl || {};
......@@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = {
callbacks: {
sorter: this.DefaultOptions.sorter,
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
......
/**
* 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 {
* All dropdown events are fired at the .dropdown-menu's parent element.
*/
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 {
},
error: () => {
this.toggleLoading(button);
if ($(button).parent().hasClass('open')) {
$(button).dropdown('toggle');
}
new Flash('An error occurred while fetching the builds.', 'alert');
},
});
......
......@@ -2,13 +2,6 @@
import StatusIconEntityMap from '../../ci_status_icons';
export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: {
stage: {
type: Object,
......@@ -16,6 +9,13 @@ export default {
},
},
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
......@@ -31,7 +31,13 @@ export default {
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
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.');
return flash;
});
......@@ -46,9 +52,10 @@ export default {
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
},
computed: {
......@@ -81,12 +88,22 @@ export default {
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
<span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
:aria-label="stage.title"
ref="dropdown">
<span
v-html="svgHTML"
aria-hidden="true">
</span>
<i
class="fa fa-caret-down"
aria-hidden="true" />
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<ul
ref="dropdown-content"
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div
class="arrow-up"
aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
......
......@@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
export default {
props: {
finishedTime: {
type: String,
required: true,
},
duration: {
type: Number,
required: true,
},
},
data() {
return {
currentTime: new Date(),
iconTimerSvg,
};
},
props: ['pipeline'],
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
computed: {
timeAgo() {
return gl.utils.getTimeago();
hasDuration() {
return this.duration > 0;
},
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at);
hasFinishedTime() {
return this.finishedTime !== '';
},
timeStopped() {
const changeTime = this.currentTime;
const options = {
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) });
localTimeFinished() {
return gl.utils.formatDate(this.finishedTime);
},
duration() {
const { duration } = this.pipeline.details;
const date = new Date(duration * 1000);
durationFormated() {
const date = new Date(this.duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`;
if (mm < 10) mm = `0${mm}`;
if (ss < 10) ss = `0${ss}`;
// left pad
if (hh < 10) {
hh = `0${hh}`;
}
if (mm < 10) {
mm = `0${mm}`;
}
if (ss < 10) {
ss = `0${ss}`;
}
if (duration !== null) return `${hh}:${mm}:${ss}`;
return false;
return `${hh}:${mm}:${ss}`;
},
},
methods: {
changeTime() {
this.currentTime = new Date();
finishedTimeFormated() {
const timeAgo = gl.utils.getTimeago();
return timeAgo.format(this.finishedTime);
},
},
template: `
<td class="pipelines-time-ago">
<p class="duration" v-if='duration'>
<span v-html="iconTimerSvg"></span>
{{duration}}
<p
class="duration"
v-if="hasDuration">
<span
v-html="iconTimerSvg">
</span>
{{durationFormated}}
</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
ref="tooltip"
data-toggle="tooltip"
data-placement="top"
data-container="body"
:data-original-title='localTimeFinished'>
{{timeStopped.words}}
:title="localTimeFinished">
{{finishedTimeFormated}}
</time>
</p>
</td>
......
import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
......@@ -161,15 +160,6 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeUpdate() {
if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
......
/* eslint-disable no-underscore-dangle*/
import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore {
constructor() {
this.state = {};
......@@ -30,32 +27,4 @@ export default class PipelinesStore {
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 */
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
......@@ -166,6 +165,32 @@ export default {
}
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: `
......@@ -192,7 +217,9 @@ export default {
</div>
</td>
<time-ago :pipeline="pipeline"/>
<time-ago
:duration="pipelineDuration"
:finished-time="pipelineFinishedAt" />
<td class="pipeline-actions">
<div class="pull-right btn-group">
......
......@@ -195,7 +195,6 @@
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
overflow: hidden;
@include set-invisible;
@media (max-width: $screen-sm-min) {
......
......@@ -104,6 +104,24 @@
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 {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
......@@ -112,7 +130,7 @@
text-transform: capitalize;
}
.value {
.value-container {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
......@@ -124,7 +142,7 @@
background-color: $filter-name-selected-color;
}
.value {
.value-container {
background-color: $filter-value-selected-color;
}
}
......
......@@ -614,6 +614,7 @@ pre.light-well {
.controls {
margin-left: auto;
text-align: right;
}
.ci-status-link {
......
......@@ -160,7 +160,6 @@
.tree-controls {
float: right;
margin-top: 11px;
position: relative;
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
include MarkdownPreview
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
......@@ -91,21 +93,13 @@ class Projects::WikisController < Projects::ApplicationController
)
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, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: {
users: ext.users.map(&:username)
}
}
def git_access
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
private
......@@ -115,7 +109,6 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
......
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
include MarkdownPreview
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :project, except: [:index, :new, :create]
......@@ -216,20 +217,6 @@ class ProjectsController < Projects::ApplicationController
}
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
branches = BranchesFinder.new(@repository, params).execute.map(&:name)
......@@ -252,6 +239,10 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
def preview_markdown
render_markdown_preview(params[:text])
end
private
# Render project landing depending of which features are available
......
......@@ -2,6 +2,7 @@ class SnippetsController < ApplicationController
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
include MarkdownPreview
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
......@@ -77,6 +78,10 @@ class SnippetsController < ApplicationController
)
end
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
protected
def snippet
......
......@@ -196,38 +196,6 @@ module ApplicationHelper
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
'about.gitlab.com'
end
......
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(...), ...).
#
# It solves a problem occurring with nested links (i.e.
......@@ -11,7 +27,7 @@ module GitlabMarkdownHelper
# explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
return '' if body.blank?
context = {
project: @project,
......@@ -43,71 +59,73 @@ module GitlabMarkdownHelper
fragment.to_html.html_safe
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 = {})
return "" unless text.present?
return '' unless text.present?
context[:project] ||= @project
html = Banzai.render(text, context)
html = markdown_unsafe(text, context)
banzai_postprocess(html, context)
end
def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display)
return "" unless object.present?
return '' unless object.present?
html = Banzai.render_field(object, field)
banzai_postprocess(html, object.banzai_render_context(field))
end
def asciidoc(text)
Gitlab::Asciidoc.render(
text,
project: @project,
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
def markup(file_name, text, context = {})
context[:project] ||= @project
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
banzai_postprocess(html, context)
end
def other_markup(file_name, text)
Gitlab::OtherMarkup.render(
file_name,
text,
project: @project,
current_user: (current_user if defined?(current_user)),
def render_wiki_content(wiki_page)
text = wiki_page.content
return '' unless text.present?
context = { pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug }
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
project_wiki: @project_wiki,
requested_path: @path,
ref: @ref,
commit: @commit
)
banzai_postprocess(html, context)
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
def markup_unsafe(file_name, text, context = {})
return '' unless text.present?
truncate_visible(md, max_chars || md.length) if md.present?
end
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug)
when :asciidoc
asciidoc(wiki_page.content)
if gitlab_markdown?(file_name)
Hamlit::RailsHelpers.preserve(markdown_unsafe(text, context))
elsif asciidoc?(file_name)
asciidoc_unsafe(text)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
text
end
else
wiki_page.formatted_content.html_safe
other_markup_unsafe(file_name, text)
end
rescue RuntimeError
simple_format(text)
end
# Returns the text necessary to reference `entity` across projects
......@@ -183,10 +201,10 @@ module GitlabMarkdownHelper
end
def markdown_toolbar_button(options = {})
data = options[:data].merge({ container: "body" })
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: "button",
class: "toolbar-btn js-md has-tooltip hidden-xs",
type: 'button',
class: 'toolbar-btn js-md has-tooltip hidden-xs',
tabindex: -1,
data: data,
title: options[:title],
......@@ -195,17 +213,34 @@ module GitlabMarkdownHelper
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
def banzai_postprocess(html, context)
def banzai_postprocess(html, context = {})
return '' unless html.present?
context.merge!(
current_user: (current_user if defined?(current_user)),
# RelativeLinkFilter
requested_path: @path,
commit: @commit,
project_wiki: @project_wiki,
ref: @ref
ref: @ref,
requested_path: @path
)
Banzai.post_process(html, context)
end
extend self
end
......@@ -160,12 +160,17 @@ module ProjectsHelper
end
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
end
def load_pipeline_status(projects)
Gitlab::Cache::Ci::ProjectPipelineStatus.
load_in_batch_for_projects(projects)
end
private
def repo_children_classes(field)
......
......@@ -13,8 +13,8 @@ module ServicesHelper
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
when "build", "build_events"
"Event will be triggered when a build status changes"
when "pipeline", "pipeline_events"
"Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
......
......@@ -12,10 +12,6 @@ module TreeHelper
tree.html_safe
end
def render_readme(readme)
render_markup(readme.name, readme.data)
end
# Return an image icon depending on the file type and mode
#
# type - String type of the tree item; either 'folder' or 'file'
......
class BaseMailer < ActionMailer::Base
helper ApplicationHelper
helper GitlabMarkdownHelper
helper MarkupHelper
attr_accessor :current_user
helper_method :current_user, :can?
......
......@@ -28,6 +28,8 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :uuid, presence: true
validates :session_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
......@@ -159,6 +161,7 @@ class ApplicationSetting < ActiveRecord::Base
end
end
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
......@@ -344,6 +347,12 @@ class ApplicationSetting < ActiveRecord::Base
private
def ensure_uuid!
return if uuid?
self.uuid = SecureRandom.uuid
end
def check_repository_storages
invalid = repository_storages - Gitlab.config.repositories.storages.keys
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
......
......@@ -120,7 +120,9 @@ module CacheMarkdownField
attrs
end
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
# Using before_update here conflicts with elasticsearch-model somehow
before_create :refresh_markdown_cache!, if: :invalidated_markdown_cache?
before_update :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end
class_methods do
......
......@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
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: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true
......
......@@ -107,7 +107,8 @@ module Network
def find_commits(skip = 0)
opts = {
max_count: self.class.max_count,
skip: skip
skip: skip,
order: :date
}
opts[:ref] = @commit.id if @filter_ref
......
......@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_writer :pipeline_status
alias_attribute :title, :name
......@@ -1181,6 +1182,7 @@ class Project < ActiveRecord::Base
end
end
# Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
......
......@@ -17,9 +17,9 @@ class Repository
# same name. The cache key used by those methods must also match method's
# name.
#
# For example, for entry `:readme` there's a method called `readme` which
# stores its data in the `readme` cache key.
CACHED_METHODS = %i(size commit_count readme contribution_guide
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze
......@@ -28,7 +28,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
readme: :readme,
readme: :rendered_readme,
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
......@@ -527,7 +527,11 @@ class Repository
head.readme
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
file_on_head(:contributing)
......
......@@ -2,20 +2,13 @@ class ProjectPolicy < BasePolicy
def rules
team_access!(user)
owner = project.owner == user ||
(project.group && project.group.has_owner?(user))
owner_access! if user.admin? || owner
team_member_owner_access! if owner
owner_access! if user.admin? || owner?
team_member_owner_access! if owner?
if project.public? || (project.internal? && !user.external?)
guest_access!
public_access!
if project.request_access_enabled &&
!(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
can! :request_access if access_requestable?
end
archived_access! if project.archived?
......@@ -27,6 +20,13 @@ class ProjectPolicy < BasePolicy
@subject
end
def owner?
return @owner if defined?(@owner)
@owner = project.owner == user ||
(project.group && project.group.has_owner?(user))
end
def guest_access!
can! :read_project
can! :read_board
......@@ -226,14 +226,6 @@ class ProjectPolicy < BasePolicy
disabled_features!
end
def project_group_member?(user)
project.group &&
(
project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
def block_issues_abilities
unless project.feature_available?(:issues, user)
cannot! :read_issue if project.default_issues_tracker?
......@@ -254,6 +246,22 @@ class ProjectPolicy < BasePolicy
private
def project_group_member?(user)
project.group &&
(
project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id)
)
end
def access_requestable?
project.request_access_enabled &&
!owner? &&
!user.admin? &&
!project.team.member?(user) &&
!project_group_member?(user)
end
# A base set of abilities for read-only users, which
# is then augmented as necessary for anonymous and other
# read-only users.
......
......@@ -97,7 +97,8 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
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
@project.group&.refresh_members_authorized_projects
......
......@@ -8,6 +8,7 @@ xml.entry do
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
xml.author do
xml.username event.author_username
xml.name event.author_name
xml.email event.author_public_email
end
......
......@@ -4,8 +4,7 @@
- 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'
.file-content.wiki
= cache(readme_cache_key) do
= render_readme(readme)
= markup(readme.name, readme.data, rendered: @repository.rendered_readme)
- else
.row-content-block.second-block.center
%h3.page-title
......
- blob.load_all_data!(@repository)
.file-content.wiki
= render_markup(blob.name, blob.data)
= markup(blob.name, blob.data)
.diff-file
.diff-content
- if gitlab_markdown?(@blob.name)
- if markup?(@blob.name)
.file-content.wiki
= preserve do
= markdown(@content)
- elsif markup?(@blob.name)
.file-content.wiki
= raw render_markup(@blob.name, @content)
= markup(@blob.name, @content)
- else
.file-content.code.js-syntax-highlight
- unless @diff_lines.empty?
......
......@@ -22,7 +22,7 @@
Milestone
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable
= dropdown_title("Assignee milestone")
= dropdown_title("Assign milestone")
= dropdown_filter("Search milestones")
= dropdown_content
= dropdown_loading
......@@ -5,4 +5,4 @@
%strong
= readme.name
.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
= render 'shared/ref_switcher', destination: 'tree', path: @path
......
......@@ -7,12 +7,4 @@
= render 'projects/last_push'
%div{ class: container_class }
.tree-controls
= 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
= render 'projects/files'
......@@ -21,7 +21,7 @@
.file-content.wiki
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
= render_markup(snippet.file_name, chunk[:data])
= markup(snippet.file_name, chunk[:data])
- else
.file-content.code
.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>
......@@ -7,6 +7,7 @@
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
- load_pipeline_status(projects)
.js-projects-list-holder
- if projects.any?
......
......@@ -7,6 +7,7 @@
- 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
- cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.updated_at)
%li.project-row{ class: css_class }
= cache(cache_key) do
......@@ -37,18 +38,21 @@
= markdown_field(project, :description)
.controls
- if project.archived
%span.prepend-left-10.label.label-warning archived
- if project.pipeline_status.has_status?
%span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
- if forks
%span.prepend-left-10
= icon('code-fork')
= number_with_delimiter(project.forks_count)
- if stars
%span.prepend-left-10
= icon('star')
= number_with_delimiter(project.star_count)
%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-0
- if project.archived
%span.prepend-left-10.label.label-warning archived
- if project.pipeline_status.has_status?
%span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
- if forks
%span.prepend-left-10
= icon('code-fork')
= number_with_delimiter(project.forks_count)
- if stars
%span.prepend-left-10
= icon('star')
= number_with_delimiter(project.star_count)
%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 @@
- if gitlab_markdown?(@snippet.file_name)
= preserve(markdown_field(@snippet, :content))
- else
= render_markup(@snippet.file_name, @snippet.content)
= markup(@snippet.file_name, @snippet.content)
- else
= render 'shared/file_highlight', blob: @snippet
---
title: Support Markdown previews for personal snippets
merge_request: 10810
author:
---
title: Lazily sets UUID in ApplicationSetting for new installations
merge_request:
author:
---
title: Fix UI inconsistency different files view (find file button missing)
merge_request: 9847
author: TM Lee
---
title: Add issues/:iid/closed_by api endpoint
merge_request:
author: mhasbini
---
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: Fetch pipeline status in batch from redis
merge_request: 10785
author:
---
title: Allow admins to sudo to blocked users via the API
merge_request: 10842
author:
---
title: Add a transaction around move_issues_to_ghost_user
merge_request: 10465
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: Add sub-nav for Project Integration Services edit page
merge_request: 10813
author:
---
title: Fix 500 error due to trying to show issues from pending deleting projects
merge_request: 10906
author:
---
title: Add username to activity atom feed
merge_request: 10802
author: winniehell
---
title: Add index on ci_builds.updated_at
merge_request: 10870
author: blackst0ne
---
title: Ensure replying to an individual note by email creates a note with its own
discussion ID
merge_request:
author:
---
title: Fix OAuth, LDAP and SAML SSO when regular sign-ups are disabled
merge_request:
author:
---
title: Fix usage ping docs link from empty cohorts page
title: Bump Sidekiq to 5.0.0
merge_request:
author:
---
title: Fix missing duration for blocked pipelines
merge_request: 10856
author:
---
title: Fix lastest commit status text on main project page
merge_request: 10863
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: Eliminate N+1 queries in loading namespaces for every issuable in milestones
merge_request:
author:
---
title: Ensure namespace owner is Master of project upon creation
merge_request: 10910
author:
---
title: Properly expire cache for all MRs of a pipeline
merge_request: 10770
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
get 'raw'
get 'download'
post :mark_as_spam
post :preview_markdown
end
end
......
class FillMissingUuidOnApplicationSettings < ActiveRecord::Migration
DOWNTIME = false
def up
execute("UPDATE application_settings SET uuid = #{quote(SecureRandom.uuid)} WHERE uuid is NULL")
end
def down
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170424142900) do
ActiveRecord::Schema.define(version: 20170426175636) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
This diff is collapsed.
......@@ -18,7 +18,8 @@ you need to use with GitLab.
## 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
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
[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
Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not
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
When you define your NFS exports, we recommend you also add the following
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
circumstances it could lead to data loss if a failure occurs before data has
synced.
......
......@@ -823,6 +823,67 @@ Example response:
}
```
## List merge requests that will close issue on merge
Get all the merge requests that will close issue when merged.
```
GET /projects/:id/issues/:issue_iid/closed_by
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project issue |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/11/closed_by
```
Example response:
```json
[
{
"id": 6471,
"iid": 6432,
"project_id": 1,
"title": "add a test for cgi lexer options",
"description": "closes #11",
"state": "opened",
"created_at": "2017-04-06T18:33:34.168Z",
"updated_at": "2017-04-09T20:10:24.983Z",
"target_branch": "master",
"source_branch": "feature.custom-highlighting",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
},
"assignee": null,
"source_project_id": 1,
"target_project_id": 1,
"labels": [],
"work_in_progress": false,
"milestone": null,
"merge_when_pipeline_succeeds": false,
"merge_status": "unchecked",
"sha": "5a62481d563af92b8e32d735f2fa63b94e806835",
"merge_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": null,
"force_remove_source_branch": false,
"web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432"
}
]
```
## Comments on issues
Comments are done via the [notes](notes.md) resource.
......@@ -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
to separate phases.
- 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
......@@ -225,6 +226,20 @@ so we need to set some guidelines for their use going forward:
[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
[Timecop](https://github.com/travisjeffery/timecop) is available in our
......
## Migrate GitLab CI to GitLab CE or EE
# Migrate GitLab CI to GitLab CE or EE
Beginning with version 8.0 of GitLab Community Edition (CE) and Enterprise
Edition (EE), GitLab CI is no longer its own application, but is instead built
......@@ -12,7 +12,7 @@ is not possible.**
We recommend that you read through the entire migration process in this
document before beginning.
### Overview
## Overview
In this document we assume you have a GitLab server and a GitLab CI server. It
does not matter if these are the same machine.
......@@ -26,7 +26,7 @@ can be online for most of the procedure; the only GitLab downtime (if any) is
during the upgrade to 8.0. Your CI service will be offline from the moment you
upgrade to 8.0 until you finish the migration procedure.
### Before upgrading
## Before upgrading
If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**:
......@@ -38,12 +38,12 @@ run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci
If you want to migrate your existing data, continue reading.
#### 0. Updating Omnibus from versions prior to 7.13
### 0. Updating Omnibus from versions prior to 7.13
If you are updating from older versions you should first update to 7.14 and then to 8.0.
Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting).
#### 1. Verify that backups work
### 1. Verify that backups work
Make sure that the backup script on both servers can connect to the database.
......@@ -73,7 +73,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r
If this fails you need to fix it before upgrading to 8.0. Also see
https://about.gitlab.com/getting-help/
#### 2. Check source and target database types
### 2. Check source and target database types
Check what databases you use on your GitLab server and your CI server.
Look for the 'adapter:' line. If your CI server and your GitLab server use
......@@ -102,7 +102,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
#### 3. Storage planning
### 3. Storage planning
Decide where to store CI build traces on GitLab server. GitLab CI uses
files on disk to store CI build traces. The default path for these build
......@@ -111,34 +111,34 @@ traces is `/var/opt/gitlab/gitlab-ci/builds` (Omnibus) or
a special location, or if you are using NFS, you should make sure that you
store build traces on the same storage as your Git repositories.
### I. Upgrading
## I. Upgrading
From this point on, GitLab CI will be unavailable for your end users.
#### 1. Upgrade GitLab to 8.0
### 1. Upgrade GitLab to 8.0
First upgrade your GitLab server to version 8.0:
https://about.gitlab.com/update/
#### 2. Disable CI on the GitLab server during the migration
### 2. Disable CI on the GitLab server during the migration
After you update, go to the admin panel and temporarily disable CI. As
an administrator, go to **Admin Area** -> **Settings**, and under
**Continuous Integration** uncheck **Disable to prevent CI usage until rake
ci:migrate is run (8.0 only)**.
#### 3. CI settings are now in GitLab
### 3. CI settings are now in GitLab
If you want to use custom CI settings (e.g. change where builds are
stored), please update `/etc/gitlab/gitlab.rb` (Omnibus) or
`/home/git/gitlab/config/gitlab.yml` (Source).
#### 4. Upgrade GitLab CI to 8.0
### 4. Upgrade GitLab CI to 8.0
Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages,
this may have already happened when you upgraded GitLab to 8.0.
#### 5. Disable GitLab CI on the CI server
### 5. Disable GitLab CI on the CI server
Disable GitLab CI after upgrading to 8.0.
......@@ -154,9 +154,9 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production
```
### II. Moving data
## II. Moving data
#### 1. Database encryption key
### 1. Database encryption key
Move the database encryption key from your CI server to your GitLab
server. The command below will show you what you need to copy-paste to your
......@@ -174,7 +174,7 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rake backup:show_secrets RAILS_ENV=production
```
#### 2. SQL data and build traces
### 2. SQL data and build traces
Create your final CI data export. If you are converting from MySQL to
PostgreSQL, add ` MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When
......@@ -192,7 +192,7 @@ cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production
```
#### 3. Copy data to the GitLab server
### 3. Copy data to the GitLab server
If you were running GitLab and GitLab CI on the same server you can skip this
step.
......@@ -209,7 +209,7 @@ ssh -A ci_admin@ci_server.example
scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~
```
#### 4. Move data to the GitLab backups folder
### 4. Move data to the GitLab backups folder
Make the CI data archive discoverable for GitLab. We assume below that you
store backups in the default path, adjust the command if necessary.
......@@ -223,7 +223,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/
sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/
```
#### 5. Import the CI data into GitLab.
### 5. Import the CI data into GitLab.
This step will delete any existing CI data on your GitLab server. There should
be no CI data yet because you turned CI on the GitLab server off earlier.
......@@ -239,7 +239,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production
```
#### 6. Restart GitLab
### 6. Restart GitLab
```
# On your GitLab server:
......@@ -251,7 +251,7 @@ sudo gitlab-ctl restart sidekiq
sudo service gitlab reload
```
### III. Redirecting traffic
## III. Redirecting traffic
If you were running GitLab CI with Omnibus packages and you were using the
internal NGINX configuration your CI service should now be available both at
......@@ -261,7 +261,7 @@ If you installed GitLab CI from source we now need to configure a redirect in
NGINX so that existing CI runners can keep using the old CI server address, and
so that existing links to your CI server keep working.
#### 1. Update Nginx configuration
### 1. Update Nginx configuration
To ensure that your existing CI runners are able to communicate with the
migrated installation, and that existing build triggers still work, you'll need
......@@ -317,22 +317,22 @@ You should also make sure that you can:
1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server.
1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE (or EE) server.
#### 2. Check Nginx configuration
### 2. Check Nginx configuration
sudo nginx -t
#### 3. Restart Nginx
### 3. Restart Nginx
sudo /etc/init.d/nginx restart
#### Restore from backup
### Restore from backup
If something went wrong and you need to restore a backup, consult the [Backup
restoration](../raketasks/backup_restore.md) guide.
### Troubleshooting
## Troubleshooting
#### show:secrets problem (Omnibus-only)
### show:secrets problem (Omnibus-only)
If you see errors like this:
```
Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml`
......@@ -343,7 +343,7 @@ Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml
This can happen if you are updating from versions prior to 7.13 straight to 8.0.
The fix for this is to update to Omnibus 7.14 first and then update it to 8.0.
#### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
To fix that issue you have to change builds/ folder permission before doing final backup:
```
sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
......@@ -354,7 +354,7 @@ Then before executing `ci:migrate` you need to fix builds folder permission:
sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
```
#### Problems when importing CI database to GitLab
### Problems when importing CI database to GitLab
If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences:
```
ALTER SEQUENCE
......
This diff is collapsed.
......@@ -17,6 +17,10 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
- [Command Line basic commands](../../gitlab-basics/command-line-commands.md)
- [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf)
- Commits
- [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **Articles:**
- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
......
......@@ -3,7 +3,7 @@
GitLab Inc. will periodically collect information about your instance in order
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
......
......@@ -51,9 +51,9 @@ service in GitLab.
## 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.
module SharedActiveTab
include Spinach::DSL
include WaitForAjax
after do
wait_for_ajax if javascript_test?
end
def ensure_active_main_tab(content)
expect(find('.layout-nav li.active')).to have_content(content)
......
......@@ -102,7 +102,7 @@ module API
end
def authenticate!
unauthorized! unless current_user && can?(current_user, :access_api)
unauthorized! unless current_user && can?(initial_current_user, :access_api)
end
def authenticate_non_get!
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment