Merge branch 'master' into 'badge-color-on-white-bg'
# Conflicts: # app/assets/stylesheets/pages/pipelines.scss
This commit is contained in:
commit
9b4b7da2c9
546 changed files with 15363 additions and 4105 deletions
|
@ -15,6 +15,7 @@ variables:
|
|||
USE_BUNDLE_INSTALL: "true"
|
||||
GIT_DEPTH: "20"
|
||||
PHANTOMJS_VERSION: "2.1.1"
|
||||
GET_SOURCES_ATTEMPTS: "3"
|
||||
|
||||
before_script:
|
||||
- source ./scripts/prepare_build.sh
|
||||
|
|
|
@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout:
|
|||
|
||||
# Checks indentation of binary operations that span more than one line.
|
||||
Style/MultilineOperationIndentation:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
EnforcedStyle: indented
|
||||
|
||||
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
|
||||
Style/MultilineTernaryOperator:
|
||||
|
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -2,6 +2,21 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 8.14.5 (2016-12-14)
|
||||
|
||||
- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600
|
||||
- fix display hook error message. !7775 (basyura)
|
||||
- Remove wrong '.builds-feature' class from the MR settings fieldset. !7930
|
||||
- Avoid escaping relative links in Markdown twice. !7940 (winniehell)
|
||||
- API: Memoize the current_user so that sudo can work properly. !8017
|
||||
- Displays milestone remaining days only when it's present.
|
||||
- Allow branch names with dots on API endpoint.
|
||||
- Issue#visible_to_user moved to IssuesFinder to prevent accidental use.
|
||||
- Shows group members in project members list.
|
||||
- Encode input when migrating ProcessCommitWorker jobs to prevent migration errors.
|
||||
- Fixed timeago re-rendering every timeago.
|
||||
- Fix missing Note access checks by moving Note#search to updated NoteFinder.
|
||||
|
||||
## 8.14.4 (2016-12-08)
|
||||
|
||||
- Fix diff view permalink highlighting. !7090
|
||||
|
@ -264,6 +279,13 @@ entry.
|
|||
- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
|
||||
- Fix 404 when visit /projects page
|
||||
|
||||
## 8.13.10 (2016-12-14)
|
||||
|
||||
- API: Memoize the current_user so that sudo can work properly. !8017
|
||||
- Filter `authentication_token`, `incoming_email_token` and `runners_token` parameters.
|
||||
- Issue#visible_to_user moved to IssuesFinder to prevent accidental use.
|
||||
- Fix missing Note access checks by moving Note#search to updated NoteFinder.
|
||||
|
||||
## 8.13.9 (2016-12-08)
|
||||
|
||||
- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615
|
||||
|
|
|
@ -1 +1 @@
|
|||
4.0.3
|
||||
4.1.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.2.0
|
||||
1.2.1
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -22,7 +22,6 @@ gem 'doorkeeper', '~> 4.2.0'
|
|||
gem 'omniauth', '~> 1.3.1'
|
||||
gem 'omniauth-auth0', '~> 1.4.1'
|
||||
gem 'omniauth-azure-oauth2', '~> 0.0.6'
|
||||
gem 'omniauth-bitbucket', '~> 0.0.2'
|
||||
gem 'omniauth-cas3', '~> 1.1.2'
|
||||
gem 'omniauth-facebook', '~> 4.0.0'
|
||||
gem 'omniauth-github', '~> 1.1.1'
|
||||
|
@ -67,7 +66,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
|
|||
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
|
||||
|
||||
# API
|
||||
gem 'grape', '~> 0.15.0'
|
||||
gem 'grape', '~> 0.18.0'
|
||||
gem 'grape-entity', '~> 0.6.0'
|
||||
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
|
||||
|
||||
|
@ -170,7 +169,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
|
|||
gem 'gemnasium-gitlab-service', '~> 0.2'
|
||||
|
||||
# Slack integration
|
||||
gem 'slack-notifier', '~> 1.2.0'
|
||||
gem 'slack-notifier', '~> 1.5.1'
|
||||
|
||||
# Asana integration
|
||||
gem 'asana', '~> 0.4.0'
|
||||
|
|
24
Gemfile.lock
24
Gemfile.lock
|
@ -284,15 +284,15 @@ GEM
|
|||
json
|
||||
multi_json
|
||||
request_store (>= 1.0)
|
||||
grape (0.15.0)
|
||||
grape (0.18.0)
|
||||
activesupport
|
||||
builder
|
||||
hashie (>= 2.1.0)
|
||||
multi_json (>= 1.3.2)
|
||||
multi_xml (>= 0.5.2)
|
||||
mustermann-grape (~> 0.4.0)
|
||||
rack (>= 1.3.0)
|
||||
rack-accept
|
||||
rack-mount
|
||||
virtus (>= 1.0.0)
|
||||
grape-entity (0.6.0)
|
||||
activesupport
|
||||
|
@ -400,6 +400,10 @@ GEM
|
|||
multi_json (1.12.1)
|
||||
multi_xml (0.5.5)
|
||||
multipart-post (2.0.0)
|
||||
mustermann (0.4.0)
|
||||
tool (~> 0.2)
|
||||
mustermann-grape (0.4.0)
|
||||
mustermann (= 0.4.0)
|
||||
mysql2 (0.3.20)
|
||||
net-ldap (0.12.1)
|
||||
net-ssh (3.0.1)
|
||||
|
@ -428,10 +432,6 @@ GEM
|
|||
jwt (~> 1.0)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-bitbucket (0.0.2)
|
||||
multi_json (~> 1.7)
|
||||
omniauth (~> 1.1)
|
||||
omniauth-oauth (~> 1.0)
|
||||
omniauth-cas3 (1.1.3)
|
||||
addressable (~> 2.3)
|
||||
nokogiri (~> 1.6.6)
|
||||
|
@ -505,14 +505,12 @@ GEM
|
|||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
rack (1.6.4)
|
||||
rack (1.6.5)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (4.4.1)
|
||||
rack
|
||||
rack-cors (0.4.0)
|
||||
rack-mount (0.8.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-oauth2 (1.2.3)
|
||||
activesupport (>= 2.3)
|
||||
attr_required (>= 0.0.5)
|
||||
|
@ -685,7 +683,7 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
slack-notifier (1.2.1)
|
||||
slack-notifier (1.5.1)
|
||||
slop (3.6.0)
|
||||
spinach (0.8.10)
|
||||
colorize
|
||||
|
@ -743,6 +741,7 @@ GEM
|
|||
tilt (2.0.5)
|
||||
timecop (0.8.1)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
tool (0.2.3)
|
||||
truncato (0.7.8)
|
||||
htmlentities (~> 4.3.1)
|
||||
nokogiri (~> 1.6.1)
|
||||
|
@ -861,7 +860,7 @@ DEPENDENCIES
|
|||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.2)
|
||||
gon (~> 6.1.0)
|
||||
grape (~> 0.15.0)
|
||||
grape (~> 0.18.0)
|
||||
grape-entity (~> 0.6.0)
|
||||
haml_lint (~> 0.18.2)
|
||||
hamlit (~> 2.6.1)
|
||||
|
@ -899,7 +898,6 @@ DEPENDENCIES
|
|||
omniauth (~> 1.3.1)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth-azure-oauth2 (~> 0.0.6)
|
||||
omniauth-bitbucket (~> 0.0.2)
|
||||
omniauth-cas3 (~> 1.1.2)
|
||||
omniauth-facebook (~> 4.0.0)
|
||||
omniauth-github (~> 1.1.1)
|
||||
|
@ -954,7 +952,7 @@ DEPENDENCIES
|
|||
sidekiq-cron (~> 0.4.4)
|
||||
sidekiq-limit_fetch (~> 3.4)
|
||||
simplecov (= 0.12.0)
|
||||
slack-notifier (~> 1.2.0)
|
||||
slack-notifier (~> 1.5.1)
|
||||
spinach-rails (~> 0.2.1)
|
||||
spinach-rerun-reporter (~> 0.0.2)
|
||||
spring (~> 1.7.0)
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
licensePath: "/api/:version/templates/licenses/:key",
|
||||
gitignorePath: "/api/:version/templates/gitignores/:key",
|
||||
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
|
||||
dockerfilePath: "/api/:version/dockerfiles/:key",
|
||||
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
|
||||
group: function(group_id, callback) {
|
||||
var url = Api.buildUrl(Api.groupPath)
|
||||
|
@ -120,6 +121,10 @@
|
|||
return callback(file);
|
||||
});
|
||||
},
|
||||
dockerfileYml: function(key, callback) {
|
||||
var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
|
||||
$.get(url, callback);
|
||||
},
|
||||
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
|
||||
var url = Api.buildUrl(Api.issuableTemplatePath)
|
||||
.replace(':key', key)
|
||||
|
|
18
app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
Normal file
18
app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* global Api */
|
||||
/*= require blob/template_selector */
|
||||
|
||||
(() => {
|
||||
const global = window.gl || (window.gl = {});
|
||||
|
||||
class BlobDockerfileSelector extends gl.TemplateSelector {
|
||||
requestFile(query) {
|
||||
return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
|
||||
}
|
||||
|
||||
requestFileSuccess(file) {
|
||||
return super.requestFileSuccess(file);
|
||||
}
|
||||
}
|
||||
|
||||
global.BlobDockerfileSelector = BlobDockerfileSelector;
|
||||
})();
|
27
app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
Normal file
27
app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
Normal file
|
@ -0,0 +1,27 @@
|
|||
(() => {
|
||||
const global = window.gl || (window.gl = {});
|
||||
|
||||
class BlobDockerfileSelectors {
|
||||
constructor({ editor, $dropdowns } = {}) {
|
||||
this.editor = editor;
|
||||
this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
|
||||
this.initSelectors();
|
||||
}
|
||||
|
||||
initSelectors() {
|
||||
const editor = this.editor;
|
||||
this.$dropdowns.each((i, dropdown) => {
|
||||
const $dropdown = $(dropdown);
|
||||
return new gl.BlobDockerfileSelector({
|
||||
editor,
|
||||
pattern: /(Dockerfile)/,
|
||||
data: $dropdown.data('data'),
|
||||
wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
|
||||
dropdown: $dropdown,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
global.BlobDockerfileSelectors = BlobDockerfileSelectors;
|
||||
})();
|
|
@ -36,6 +36,9 @@
|
|||
new gl.BlobCiYamlSelectors({
|
||||
editor: this.editor
|
||||
});
|
||||
new gl.BlobDockerfileSelectors({
|
||||
editor: this.editor
|
||||
});
|
||||
}
|
||||
|
||||
EditBlob.prototype.initModePanesAndLinks = function() {
|
||||
|
|
|
@ -141,6 +141,11 @@
|
|||
case 'projects:merge_requests:builds':
|
||||
new MergedButtons();
|
||||
break;
|
||||
case 'projects:merge_requests:pipelines':
|
||||
new gl.MiniPipelineGraph({
|
||||
container: '.js-pipeline-table',
|
||||
});
|
||||
break;
|
||||
case "projects:merge_requests:diffs":
|
||||
new gl.Diff();
|
||||
new ZenMode();
|
||||
|
@ -158,6 +163,11 @@
|
|||
new ZenMode();
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
break;
|
||||
case 'projects:commit:pipelines':
|
||||
new gl.MiniPipelineGraph({
|
||||
container: '.js-pipeline-table',
|
||||
});
|
||||
break;
|
||||
case 'projects:commit:builds':
|
||||
new gl.Pipelines();
|
||||
break;
|
||||
|
@ -172,6 +182,11 @@
|
|||
new TreeView();
|
||||
}
|
||||
break;
|
||||
case 'projects:pipelines:index':
|
||||
new gl.MiniPipelineGraph({
|
||||
container: '.js-pipeline-table',
|
||||
});
|
||||
break;
|
||||
case 'projects:pipelines:builds':
|
||||
case 'projects:pipelines:show':
|
||||
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
* The environments array is a recursive tree structure and we need to filter
|
||||
* both root level environments and children environments.
|
||||
*
|
||||
* In order to acomplish that, both `filterState` and `filterEnvironmnetsByState`
|
||||
* In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
|
||||
* functions work together.
|
||||
* The first one works as the filter that verifies if the given environment matches
|
||||
* the given state.
|
||||
|
@ -34,9 +34,9 @@
|
|||
* @param {Array} array
|
||||
* @return {Array}
|
||||
*/
|
||||
const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
|
||||
const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
|
||||
if (item.children) {
|
||||
const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
|
||||
const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
|
||||
if (filteredChildren.length) {
|
||||
item.children = filteredChildren;
|
||||
return item;
|
||||
|
@ -76,12 +76,13 @@
|
|||
helpPagePath: environmentsData.helpPagePath,
|
||||
commitIconSvg: environmentsData.commitIconSvg,
|
||||
playIconSvg: environmentsData.playIconSvg,
|
||||
terminalIconSvg: environmentsData.terminalIconSvg,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredEnvironments() {
|
||||
return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
|
||||
return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
|
||||
},
|
||||
|
||||
scope() {
|
||||
|
@ -102,7 +103,7 @@
|
|||
},
|
||||
|
||||
/**
|
||||
* Fetches all the environmnets and stores them.
|
||||
* Fetches all the environments and stores them.
|
||||
* Toggles loading property.
|
||||
*/
|
||||
created() {
|
||||
|
@ -230,6 +231,7 @@
|
|||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg"></tr>
|
||||
|
||||
<tr v-if="model.isOpen && model.children && model.children.length > 0"
|
||||
|
@ -240,6 +242,7 @@
|
|||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg">
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
/*= require ./environment_external_url */
|
||||
/*= require ./environment_stop */
|
||||
/*= require ./environment_rollback */
|
||||
/*= require ./environment_terminal_button */
|
||||
|
||||
(() => {
|
||||
/**
|
||||
|
@ -33,6 +34,7 @@
|
|||
'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
|
||||
'stop-component': window.gl.environmentsList.StopComponent,
|
||||
'rollback-component': window.gl.environmentsList.RollbackComponent,
|
||||
'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -68,6 +70,12 @@
|
|||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
terminalIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -449,7 +457,7 @@
|
|||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="environments-build-cell">
|
||||
<a v-if="shouldRenderBuildName"
|
||||
class="build-link"
|
||||
:href="model.last_deployment.deployable.build_path">
|
||||
|
@ -506,6 +514,14 @@
|
|||
</stop-component>
|
||||
</div>
|
||||
|
||||
<div v-if="model.terminal_path"
|
||||
class="inline js-terminal-button-container">
|
||||
<terminal-button-component
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:terminal-path="model.terminal_path">
|
||||
</terminal-button-component>
|
||||
</div>
|
||||
|
||||
<div v-if="canRetry && canCreateDeployment"
|
||||
class="inline js-rollback-component-container">
|
||||
<rollback-component
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*= require vue */
|
||||
/* global Vue */
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
|
||||
props: {
|
||||
terminalPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
terminalIconSvg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn terminal-button"
|
||||
:href="terminalPath">
|
||||
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -2,19 +2,28 @@
|
|||
|
||||
// Creates the variables for setting up GFM auto-completion
|
||||
(function() {
|
||||
if (window.GitLab == null) {
|
||||
window.GitLab = {};
|
||||
if (window.gl == null) {
|
||||
window.gl = {};
|
||||
}
|
||||
|
||||
function sanitize(str) {
|
||||
return str.replace(/<(?:.|\n)*?>/gm, '');
|
||||
}
|
||||
|
||||
window.GitLab.GfmAutoComplete = {
|
||||
dataLoading: false,
|
||||
dataLoaded: false,
|
||||
window.gl.GfmAutoComplete = {
|
||||
dataSources: {},
|
||||
defaultLoadingData: ['loading'],
|
||||
cachedData: {},
|
||||
dataSource: '',
|
||||
isLoadingData: {},
|
||||
atTypeMap: {
|
||||
':': 'emojis',
|
||||
'@': 'members',
|
||||
'#': 'issues',
|
||||
'!': 'mergeRequests',
|
||||
'~': 'labels',
|
||||
'%': 'milestones',
|
||||
'/': 'commands'
|
||||
},
|
||||
// Emoji
|
||||
Emoji: {
|
||||
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
|
||||
|
@ -35,33 +44,31 @@
|
|||
template: '<li>${title}</li>'
|
||||
},
|
||||
Loading: {
|
||||
template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
|
||||
template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
|
||||
},
|
||||
DefaultOptions: {
|
||||
sorter: function(query, items, searchKey) {
|
||||
// Highlight first item only if at least one char was typed
|
||||
this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
|
||||
if ((items[0].name != null) && items[0].name === 'loading') {
|
||||
if (gl.GfmAutoComplete.isLoading(items)) {
|
||||
return items;
|
||||
}
|
||||
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
|
||||
},
|
||||
filter: function(query, data, searchKey) {
|
||||
if (data[0] === 'loading') {
|
||||
if (gl.GfmAutoComplete.isLoading(data)) {
|
||||
gl.GfmAutoComplete.togglePreventSelection.call(this, true);
|
||||
gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
|
||||
return data;
|
||||
} else {
|
||||
gl.GfmAutoComplete.togglePreventSelection.call(this, false);
|
||||
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
|
||||
}
|
||||
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
|
||||
},
|
||||
beforeInsert: function(value) {
|
||||
if (value && !this.setting.skipSpecialCharacterTest) {
|
||||
var withoutAt = value.substring(1);
|
||||
if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
|
||||
}
|
||||
if (!window.GitLab.GfmAutoComplete.dataLoaded) {
|
||||
return this.at;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
matcher: function (flag, subtext) {
|
||||
// The below is taken from At.js source
|
||||
|
@ -86,69 +93,46 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
setup: _.debounce(function(input) {
|
||||
setup: function(input) {
|
||||
// Add GFM auto-completion to all input fields, that accept GFM input.
|
||||
this.input = input || $('.js-gfm-input');
|
||||
// destroy previous instances
|
||||
this.destroyAtWho();
|
||||
// set up instances
|
||||
this.setupAtWho();
|
||||
|
||||
if (this.dataSource && !this.dataLoading && !this.cachedData) {
|
||||
this.dataLoading = true;
|
||||
return this.fetchData(this.dataSource)
|
||||
.done((data) => {
|
||||
this.dataLoading = false;
|
||||
this.loadData(data);
|
||||
});
|
||||
};
|
||||
|
||||
if (this.cachedData != null) {
|
||||
return this.loadData(this.cachedData);
|
||||
}
|
||||
}, 1000),
|
||||
setupAtWho: function() {
|
||||
this.setupLifecycle();
|
||||
},
|
||||
setupLifecycle() {
|
||||
this.input.each((i, input) => {
|
||||
const $input = $(input);
|
||||
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
|
||||
});
|
||||
},
|
||||
setupAtWho: function($input) {
|
||||
// Emoji
|
||||
this.input.atwho({
|
||||
$input.atwho({
|
||||
at: ':',
|
||||
displayTpl: (function(_this) {
|
||||
return function(value) {
|
||||
if (value.path != null) {
|
||||
return _this.Emoji.template;
|
||||
} else {
|
||||
return _this.Loading.template;
|
||||
}
|
||||
};
|
||||
})(this),
|
||||
displayTpl: function(value) {
|
||||
return value.path != null ? this.Emoji.template : this.Loading.template;
|
||||
}.bind(this),
|
||||
insertTpl: ':${name}:',
|
||||
data: ['loading'],
|
||||
startWithSpace: false,
|
||||
skipSpecialCharacterTest: true,
|
||||
data: this.defaultLoadingData,
|
||||
callbacks: {
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
filter: this.DefaultOptions.filter,
|
||||
beforeInsert: this.DefaultOptions.beforeInsert,
|
||||
matcher: this.DefaultOptions.matcher
|
||||
filter: this.DefaultOptions.filter
|
||||
}
|
||||
});
|
||||
// Team Members
|
||||
this.input.atwho({
|
||||
$input.atwho({
|
||||
at: '@',
|
||||
displayTpl: (function(_this) {
|
||||
return function(value) {
|
||||
if (value.username != null) {
|
||||
return _this.Members.template;
|
||||
} else {
|
||||
return _this.Loading.template;
|
||||
}
|
||||
};
|
||||
})(this),
|
||||
displayTpl: function(value) {
|
||||
return value.username != null ? this.Members.template : this.Loading.template;
|
||||
}.bind(this),
|
||||
insertTpl: '${atwho-at}${username}',
|
||||
searchKey: 'search',
|
||||
data: ['loading'],
|
||||
startWithSpace: false,
|
||||
alwaysHighlightFirst: true,
|
||||
skipSpecialCharacterTest: true,
|
||||
data: this.defaultLoadingData,
|
||||
callbacks: {
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
filter: this.DefaultOptions.filter,
|
||||
|
@ -179,20 +163,14 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
this.input.atwho({
|
||||
$input.atwho({
|
||||
at: '#',
|
||||
alias: 'issues',
|
||||
searchKey: 'search',
|
||||
displayTpl: (function(_this) {
|
||||
return function(value) {
|
||||
if (value.title != null) {
|
||||
return _this.Issues.template;
|
||||
} else {
|
||||
return _this.Loading.template;
|
||||
}
|
||||
};
|
||||
})(this),
|
||||
data: ['loading'],
|
||||
displayTpl: function(value) {
|
||||
return value.title != null ? this.Issues.template : this.Loading.template;
|
||||
}.bind(this),
|
||||
data: this.defaultLoadingData,
|
||||
insertTpl: '${atwho-at}${id}',
|
||||
startWithSpace: false,
|
||||
callbacks: {
|
||||
|
@ -214,26 +192,21 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
this.input.atwho({
|
||||
$input.atwho({
|
||||
at: '%',
|
||||
alias: 'milestones',
|
||||
searchKey: 'search',
|
||||
displayTpl: (function(_this) {
|
||||
return function(value) {
|
||||
if (value.title != null) {
|
||||
return _this.Milestones.template;
|
||||
} else {
|
||||
return _this.Loading.template;
|
||||
}
|
||||
};
|
||||
})(this),
|
||||
insertTpl: '${atwho-at}${title}',
|
||||
data: ['loading'],
|
||||
displayTpl: function(value) {
|
||||
return value.title != null ? this.Milestones.template : this.Loading.template;
|
||||
}.bind(this),
|
||||
startWithSpace: false,
|
||||
data: this.defaultLoadingData,
|
||||
callbacks: {
|
||||
matcher: this.DefaultOptions.matcher,
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
beforeInsert: this.DefaultOptions.beforeInsert,
|
||||
filter: this.DefaultOptions.filter,
|
||||
beforeSave: function(milestones) {
|
||||
return $.map(milestones, function(m) {
|
||||
if (m.title == null) {
|
||||
|
@ -248,21 +221,15 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
this.input.atwho({
|
||||
$input.atwho({
|
||||
at: '!',
|
||||
alias: 'mergerequests',
|
||||
searchKey: 'search',
|
||||
displayTpl: (function(_this) {
|
||||
return function(value) {
|
||||
if (value.title != null) {
|
||||
return _this.Issues.template;
|
||||
} else {
|
||||
return _this.Loading.template;
|
||||
}
|
||||
};
|
||||
})(this),
|
||||
data: ['loading'],
|
||||
startWithSpace: false,
|
||||
displayTpl: function(value) {
|
||||
return value.title != null ? this.Issues.template : this.Loading.template;
|
||||
}.bind(this),
|
||||
data: this.defaultLoadingData,
|
||||
insertTpl: '${atwho-at}${id}',
|
||||
callbacks: {
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
|
@ -283,18 +250,31 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
this.input.atwho({
|
||||
$input.atwho({
|
||||
at: '~',
|
||||
alias: 'labels',
|
||||
searchKey: 'search',
|
||||
displayTpl: this.Labels.template,
|
||||
data: this.defaultLoadingData,
|
||||
displayTpl: function(value) {
|
||||
return this.isLoading(value) ? this.Loading.template : this.Labels.template;
|
||||
}.bind(this),
|
||||
insertTpl: '${atwho-at}${title}',
|
||||
startWithSpace: false,
|
||||
callbacks: {
|
||||
matcher: this.DefaultOptions.matcher,
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
beforeInsert: this.DefaultOptions.beforeInsert,
|
||||
filter: this.DefaultOptions.filter,
|
||||
beforeSave: function(merges) {
|
||||
if (gl.GfmAutoComplete.isLoading(merges)) return merges;
|
||||
var sanitizeLabelTitle;
|
||||
sanitizeLabelTitle = function(title) {
|
||||
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
|
||||
return "\"" + (sanitize(title)) + "\"";
|
||||
} else {
|
||||
return sanitize(title);
|
||||
}
|
||||
};
|
||||
return $.map(merges, function(m) {
|
||||
return {
|
||||
title: sanitize(m.title),
|
||||
|
@ -306,12 +286,14 @@
|
|||
}
|
||||
});
|
||||
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
|
||||
this.input.filter('[data-supports-slash-commands="true"]').atwho({
|
||||
$input.filter('[data-supports-slash-commands="true"]').atwho({
|
||||
at: '/',
|
||||
alias: 'commands',
|
||||
searchKey: 'search',
|
||||
skipSpecialCharacterTest: true,
|
||||
data: this.defaultLoadingData,
|
||||
displayTpl: function(value) {
|
||||
if (this.isLoading(value)) return this.Loading.template;
|
||||
var tpl = '<li>/${name}';
|
||||
if (value.aliases.length > 0) {
|
||||
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
|
||||
|
@ -324,7 +306,7 @@
|
|||
}
|
||||
tpl += '</li>';
|
||||
return _.template(tpl)(value);
|
||||
},
|
||||
}.bind(this),
|
||||
insertTpl: function(value) {
|
||||
var tpl = "/${name} ";
|
||||
var reference_prefix = null;
|
||||
|
@ -342,6 +324,7 @@
|
|||
filter: this.DefaultOptions.filter,
|
||||
beforeInsert: this.DefaultOptions.beforeInsert,
|
||||
beforeSave: function(commands) {
|
||||
if (gl.GfmAutoComplete.isLoading(commands)) return commands;
|
||||
return $.map(commands, function(c) {
|
||||
var search = c.name;
|
||||
if (c.aliases.length > 0) {
|
||||
|
@ -369,32 +352,40 @@
|
|||
});
|
||||
return;
|
||||
},
|
||||
destroyAtWho: function() {
|
||||
return this.input.atwho('destroy');
|
||||
fetchData: function($input, at) {
|
||||
if (this.isLoadingData[at]) return;
|
||||
this.isLoadingData[at] = true;
|
||||
if (this.cachedData[at]) {
|
||||
this.loadData($input, at, this.cachedData[at]);
|
||||
} else {
|
||||
$.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
|
||||
this.loadData($input, at, data);
|
||||
}).fail(() => { this.isLoadingData[at] = false; });
|
||||
}
|
||||
},
|
||||
fetchData: function(dataSource) {
|
||||
return $.getJSON(dataSource);
|
||||
},
|
||||
loadData: function(data) {
|
||||
this.cachedData = data;
|
||||
this.dataLoaded = true;
|
||||
// load members
|
||||
this.input.atwho('load', '@', data.members);
|
||||
// load issues
|
||||
this.input.atwho('load', 'issues', data.issues);
|
||||
// load milestones
|
||||
this.input.atwho('load', 'milestones', data.milestones);
|
||||
// load merge requests
|
||||
this.input.atwho('load', 'mergerequests', data.mergerequests);
|
||||
// load emojis
|
||||
this.input.atwho('load', ':', data.emojis);
|
||||
// load labels
|
||||
this.input.atwho('load', '~', data.labels);
|
||||
// load commands
|
||||
this.input.atwho('load', '/', data.commands);
|
||||
loadData: function($input, at, data) {
|
||||
this.isLoadingData[at] = false;
|
||||
this.cachedData[at] = data;
|
||||
$input.atwho('load', at, data);
|
||||
// This trigger at.js again
|
||||
// otherwise we would be stuck with loading until the user types
|
||||
return $(':focus').trigger('keyup');
|
||||
return $input.trigger('keyup');
|
||||
},
|
||||
isLoading(data) {
|
||||
if (!data) return false;
|
||||
if (Array.isArray(data)) data = data[0];
|
||||
return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
|
||||
},
|
||||
togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
|
||||
this.setting.tabSelectsMatch = !isPrevented;
|
||||
this.setting.spaceSelectsMatch = !isPrevented;
|
||||
const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
|
||||
this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
|
||||
},
|
||||
preventSpaceTabEnter(e) {
|
||||
const key = e.which || e.keyCode;
|
||||
const preventables = [9, 13, 32];
|
||||
if (preventables.indexOf(key) > -1) e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -343,16 +343,18 @@
|
|||
selector = ".dropdown-page-one .dropdown-content a";
|
||||
}
|
||||
this.dropdown.on("click", selector, function(e) {
|
||||
var $el, selected;
|
||||
var $el, selected, selectedObj, isMarking;
|
||||
$el = $(this);
|
||||
selected = self.rowClicked($el);
|
||||
selectedObj = selected ? selected[0] : null;
|
||||
isMarking = selected ? selected[1] : null;
|
||||
if (self.options.clicked) {
|
||||
self.options.clicked(selected[0], $el, e, selected[1]);
|
||||
self.options.clicked(selectedObj, $el, e, isMarking);
|
||||
}
|
||||
|
||||
// Update label right after all modifications in dropdown has been done
|
||||
if (self.options.toggleLabel) {
|
||||
self.updateLabel(selected[0], $el, self);
|
||||
self.updateLabel(selectedObj, $el, self);
|
||||
}
|
||||
|
||||
$el.trigger('blur');
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
this.form.addClass('gfm-form');
|
||||
// remove notify commit author checkbox for non-commit notes
|
||||
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
|
||||
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
|
||||
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
|
||||
new DropzoneInput(this.form);
|
||||
autosize(this.textarea);
|
||||
// form and textarea event listeners
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
this.renderWipExplanation = bind(this.renderWipExplanation, this);
|
||||
this.resetAutosave = bind(this.resetAutosave, this);
|
||||
this.handleSubmit = bind(this.handleSubmit, this);
|
||||
GitLab.GfmAutoComplete.setup();
|
||||
gl.GfmAutoComplete.setup();
|
||||
new UsersSelect();
|
||||
new ZenMode();
|
||||
this.titleField = this.form.find("input[name*='[title]']");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */
|
||||
/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
|
||||
/* global MergeRequestTabs */
|
||||
|
||||
/*= require jquery.waitforimages */
|
||||
|
@ -27,6 +27,7 @@
|
|||
// Prevent duplicate event bindings
|
||||
this.disableTaskList();
|
||||
this.initMRBtnListeners();
|
||||
this.initCommitMessageListeners();
|
||||
if ($("a.btn-close").length) {
|
||||
this.initTaskList();
|
||||
}
|
||||
|
@ -108,6 +109,26 @@
|
|||
// note so that we can re-use its form here
|
||||
};
|
||||
|
||||
MergeRequest.prototype.initCommitMessageListeners = function() {
|
||||
var textarea = $('textarea.js-commit-message');
|
||||
|
||||
$('a.js-with-description-link').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
textarea.val(textarea.data('messageWithDescription'));
|
||||
$('p.js-with-description-hint').hide();
|
||||
$('p.js-without-description-hint').show();
|
||||
});
|
||||
|
||||
$('a.js-without-description-link').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
textarea.val(textarea.data('messageWithoutDescription'));
|
||||
$('p.js-with-description-hint').show();
|
||||
$('p.js-without-description-hint').hide();
|
||||
});
|
||||
};
|
||||
|
||||
return MergeRequest;
|
||||
|
||||
})();
|
||||
|
|
96
app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
Normal file
96
app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
Normal file
|
@ -0,0 +1,96 @@
|
|||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
|
||||
/**
|
||||
* In each pipelines table we have a mini pipeline graph for each pipeline.
|
||||
*
|
||||
* When we click in a pipeline stage, we need to make an API call to get the
|
||||
* builds list to render in a dropdown.
|
||||
*
|
||||
* The container should be the table element.
|
||||
*
|
||||
* The stage icon clicked needs to have the following HTML structure:
|
||||
* <div>
|
||||
* <button class="dropdown js-builds-dropdown-button"></button>
|
||||
* <div class="js-builds-dropdown-container"></div>
|
||||
* </div>
|
||||
*/
|
||||
(() => {
|
||||
class MiniPipelineGraph {
|
||||
constructor(opts = {}) {
|
||||
this.container = opts.container || '';
|
||||
this.dropdownListSelector = '.js-builds-dropdown-container';
|
||||
this.getBuildsList = this.getBuildsList.bind(this);
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds and removes the event listener.
|
||||
*/
|
||||
bindEvents() {
|
||||
const dropdownButtonSelector = 'button.js-builds-dropdown-button';
|
||||
|
||||
$(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
|
||||
.on('click', dropdownButtonSelector, this.getBuildsList);
|
||||
}
|
||||
|
||||
/**
|
||||
* For the clicked stage, renders the given data in the dropdown list.
|
||||
*
|
||||
* @param {HTMLElement} stageContainer
|
||||
* @param {Object} data
|
||||
*/
|
||||
renderBuildsList(stageContainer, data) {
|
||||
const dropdownContainer = stageContainer.parentElement.querySelector(
|
||||
`${this.dropdownListSelector} .js-builds-dropdown-list`,
|
||||
);
|
||||
|
||||
dropdownContainer.innerHTML = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the clicked stage, gets the list of builds.
|
||||
*
|
||||
* @param {Object} e
|
||||
* @return {Promise}
|
||||
*/
|
||||
getBuildsList(e) {
|
||||
const button = e.currentTarget;
|
||||
const endpoint = button.dataset.stageEndpoint;
|
||||
|
||||
return $.ajax({
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
url: endpoint,
|
||||
beforeSend: () => {
|
||||
this.renderBuildsList(button, '');
|
||||
this.toggleLoading(button);
|
||||
},
|
||||
success: (data) => {
|
||||
this.toggleLoading(button);
|
||||
this.renderBuildsList(button, data.html);
|
||||
},
|
||||
error: () => {
|
||||
this.toggleLoading(button);
|
||||
new Flash('An error occurred while fetching the builds.', 'alert');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the loading icon.
|
||||
*
|
||||
* @param {HTMLElement} stageContainer
|
||||
* @return {type}
|
||||
*/
|
||||
toggleLoading(stageContainer) {
|
||||
stageContainer.parentElement.querySelector(
|
||||
`${this.dropdownListSelector} .js-builds-dropdown-loading`,
|
||||
).classList.toggle('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.MiniPipelineGraph = MiniPipelineGraph;
|
||||
})();
|
|
@ -356,7 +356,7 @@
|
|||
icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
|
||||
nameText = this.text(x + 25, y + 10, commit.author.name);
|
||||
idText = this.text(x, y + 35, commit.id);
|
||||
messageText = this.text(x, y + 50, commit.message);
|
||||
messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
|
||||
textSet = this.set(icon, nameText, idText, messageText).attr({
|
||||
"text-anchor": "start",
|
||||
font: "12px Monaco, monospace"
|
||||
|
@ -368,6 +368,7 @@
|
|||
idText.attr({
|
||||
fill: "#AAA"
|
||||
});
|
||||
messageText.node.style["white-space"] = "pre";
|
||||
this.textWrap(messageText, boxWidth - 50);
|
||||
rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
|
||||
fill: "#FFF",
|
||||
|
@ -404,16 +405,21 @@
|
|||
s.push("\n");
|
||||
x = 0;
|
||||
}
|
||||
x += word.length * letterWidth;
|
||||
s.push(word + " ");
|
||||
if (word === "\n") {
|
||||
s.push("\n");
|
||||
x = 0;
|
||||
} else {
|
||||
s.push(word + " ");
|
||||
x += word.length * letterWidth;
|
||||
}
|
||||
}
|
||||
t.attr({
|
||||
text: s.join("")
|
||||
text: s.join("").trim()
|
||||
});
|
||||
b = t.getBBox();
|
||||
h = Math.abs(b.y2) - Math.abs(b.y) + 1;
|
||||
h = Math.abs(b.y2) + 1;
|
||||
return t.attr({
|
||||
y: b.y + h
|
||||
y: h
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
});
|
||||
$(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) {
|
||||
if (data.saved) {
|
||||
return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html);
|
||||
return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html);
|
||||
} else {
|
||||
return new Flash('Failed to save new settings', 'alert');
|
||||
}
|
||||
|
|
62
app/assets/javascripts/terminal/terminal.js.es6
Normal file
62
app/assets/javascripts/terminal/terminal.js.es6
Normal file
|
@ -0,0 +1,62 @@
|
|||
/* global Terminal */
|
||||
|
||||
(() => {
|
||||
class GLTerminal {
|
||||
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
|
||||
this.options.cursorBlink = options.cursorBlink || true;
|
||||
this.options.screenKeys = options.screenKeys || true;
|
||||
this.container = document.querySelector(options.selector);
|
||||
|
||||
this.setSocketUrl();
|
||||
this.createTerminal();
|
||||
$(window).off('resize.terminal').on('resize.terminal', () => {
|
||||
this.terminal.fit();
|
||||
});
|
||||
}
|
||||
|
||||
setSocketUrl() {
|
||||
const { protocol, hostname, port } = window.location;
|
||||
const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const path = this.container.dataset.projectPath;
|
||||
|
||||
this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`;
|
||||
}
|
||||
|
||||
createTerminal() {
|
||||
this.terminal = new Terminal(this.options);
|
||||
this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
|
||||
this.terminal.open(this.container);
|
||||
this.socket.onopen = () => { this.runTerminal(); };
|
||||
this.socket.onerror = () => { this.handleSocketFailure(); };
|
||||
}
|
||||
|
||||
runTerminal() {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const encoder = new TextEncoder('utf-8');
|
||||
|
||||
this.terminal.on('data', (data) => {
|
||||
this.socket.send(encoder.encode(data));
|
||||
});
|
||||
|
||||
this.socket.addEventListener('message', (ev) => {
|
||||
this.terminal.write(decoder.decode(ev.data));
|
||||
});
|
||||
|
||||
this.isTerminalInitialized = true;
|
||||
this.terminal.fit();
|
||||
}
|
||||
|
||||
handleSocketFailure() {
|
||||
this.terminal.write('\r\nConnection failure');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.gl = window.gl || {};
|
||||
gl.Terminal = GLTerminal;
|
||||
})();
|
5
app/assets/javascripts/terminal/terminal_bundle.js.es6
Normal file
5
app/assets/javascripts/terminal/terminal_bundle.js.es6
Normal file
|
@ -0,0 +1,5 @@
|
|||
//= require xterm/xterm.js
|
||||
//= require xterm/fit.js
|
||||
//= require ./terminal.js
|
||||
|
||||
$(() => new gl.Terminal({ selector: '#terminal' }));
|
|
@ -89,7 +89,8 @@
|
|||
|
||||
U2FAuthenticate.prototype.renderError = function(error) {
|
||||
this.renderTemplate('error', {
|
||||
error_message: error.message()
|
||||
error_message: error.message(),
|
||||
error_code: error.errorCode
|
||||
});
|
||||
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
this.errorCode = errorCode;
|
||||
this.message = bind(this.message, this);
|
||||
this.httpsDisabled = window.location.protocol !== 'https:';
|
||||
console.error("U2F Error Code: " + this.errorCode);
|
||||
}
|
||||
|
||||
U2FError.prototype.message = function() {
|
||||
|
|
|
@ -76,7 +76,8 @@
|
|||
|
||||
U2FRegister.prototype.renderError = function(error) {
|
||||
this.renderTemplate('error', {
|
||||
error_message: error.message()
|
||||
error_message: error.message(),
|
||||
error_code: error.errorCode
|
||||
});
|
||||
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
|
||||
};
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
&.s32 { font-size: 20px; line-height: 30px; }
|
||||
&.s40 { font-size: 16px; line-height: 38px; }
|
||||
&.s60 { font-size: 32px; line-height: 58px; }
|
||||
&.s70 { font-size: 34px; line-height: 68px; }
|
||||
&.s70 { font-size: 34px; line-height: 70px; }
|
||||
&.s90 { font-size: 36px; line-height: 88px; }
|
||||
&.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
|
||||
&.s140 { font-size: 72px; line-height: 138px; }
|
||||
|
|
|
@ -230,6 +230,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.btn-terminal {
|
||||
svg {
|
||||
height: 14px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
@extend .dropdown-toggle;
|
||||
padding-right: 20px;
|
||||
position: relative;
|
||||
width: 160px;
|
||||
width: 163px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
|
|
|
@ -96,6 +96,10 @@ label {
|
|||
code {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: $screen-xs-max) {
|
||||
|
|
|
@ -49,3 +49,11 @@
|
|||
fill: $gray-darkest;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-manual {
|
||||
color: $gl-text-color;
|
||||
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,47 @@ body {
|
|||
|
||||
.container-limited {
|
||||
max-width: $fixed-layout-width;
|
||||
|
||||
&.limit-container-width {
|
||||
max-width: $limited-layout-width;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-wrapper {
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
|
||||
.alert-warning {
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
background-color: lighten($gl-warning, 4%);
|
||||
border-color: lighten($gl-warning, 4%);
|
||||
}
|
||||
|
||||
.alert-warning + .alert-warning {
|
||||
background-color: $gl-warning;
|
||||
border-color: $gl-warning;
|
||||
}
|
||||
|
||||
.alert-warning + .alert-warning + .alert-warning {
|
||||
background-color: darken($gl-warning, 4%);
|
||||
border-color: darken($gl-warning, 4%);
|
||||
}
|
||||
|
||||
.alert-warning + .alert-warning + .alert-warning + .alert-warning {
|
||||
background-color: darken($gl-warning, 8%);
|
||||
border-color: darken($gl-warning, 8%);
|
||||
}
|
||||
|
||||
.alert-warning:only-of-type {
|
||||
background-color: $gl-warning;
|
||||
border-color: $gl-warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
}
|
||||
|
||||
// Display Star and Fork buttons without counters on mobile.
|
||||
.project-action-buttons {
|
||||
.project-repo-buttons {
|
||||
display: block;
|
||||
|
||||
.count-buttons .btn {
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
}
|
||||
|
||||
.ci-status-link {
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
|
|
@ -18,6 +18,20 @@
|
|||
margin-top: -2px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&.split {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
@import "bootstrap/alerts";
|
||||
// @import "bootstrap/progress-bars";
|
||||
@import "bootstrap/list-group";
|
||||
// @import "bootstrap/wells";
|
||||
@import "bootstrap/wells";
|
||||
@import "bootstrap/close";
|
||||
@import "bootstrap/panels";
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ $gray-lightest: #fdfdfd;
|
|||
$gray-light: #fafafa;
|
||||
$gray-lighter: #f9f9f9;
|
||||
$gray-normal: #f5f5f5;
|
||||
$gray-dark: darken($gray-light, $darken-dark-factor);
|
||||
$gray-darker: #eee;
|
||||
$gray-darkest: #c4c4c4;
|
||||
|
||||
|
@ -154,6 +155,8 @@ $row-hover-border: #b2d7ff;
|
|||
$progress-color: #c0392b;
|
||||
$header-height: 50px;
|
||||
$fixed-layout-width: 1280px;
|
||||
$limited-layout-width: 990px;
|
||||
$gl-avatar-size: 40px;
|
||||
$error-exclamation-point: #e62958;
|
||||
$border-radius-default: 2px;
|
||||
$settings-icon-size: 18px;
|
||||
|
@ -444,7 +447,7 @@ $jq-ui-default-color: #777;
|
|||
$label-gray-bg: #f8fafc;
|
||||
$label-inverse-bg: #333;
|
||||
$label-remove-border: rgba(0, 0, 0, .1);
|
||||
$label-border-radius: 14px;
|
||||
$label-border-radius: 100px;
|
||||
|
||||
/*
|
||||
* Lint
|
||||
|
@ -480,7 +483,6 @@ $project-option-descr-color: #54565b;
|
|||
$project-breadcrumb-color: #999;
|
||||
$project-private-forks-notice-odd: #2aa056;
|
||||
$project-network-controls-color: #888;
|
||||
$project-limit-message-bg: #f28d35;
|
||||
|
||||
/*
|
||||
* Runners
|
||||
|
@ -530,3 +532,9 @@ $body-text-shadow: rgba(255,255,255,0.01);
|
|||
*/
|
||||
$ui-dev-kit-example-color: #bbb;
|
||||
$ui-dev-kit-example-border: #ddd;
|
||||
|
||||
/*
|
||||
Pipeline Graph
|
||||
*/
|
||||
$stage-hover-bg: #eaf3fc;
|
||||
$stage-hover-border: #d1e7fc;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
padding: $gl-padding-top 0;
|
||||
border-bottom: 1px solid $border-color;
|
||||
color: $gl-text-color-dark;
|
||||
font-size: 16px;
|
||||
line-height: 34px;
|
||||
|
||||
.author {
|
||||
|
|
|
@ -75,7 +75,8 @@
|
|||
.soft-wrap-toggle,
|
||||
.license-selector,
|
||||
.gitignore-selector,
|
||||
.gitlab-ci-yml-selector {
|
||||
.gitlab-ci-yml-selector,
|
||||
.dockerfile-selector {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
font-family: $regular_font;
|
||||
|
@ -105,7 +106,8 @@
|
|||
|
||||
.gitignore-selector,
|
||||
.license-selector,
|
||||
.gitlab-ci-yml-selector {
|
||||
.gitlab-ci-yml-selector,
|
||||
.dockerfile-selector {
|
||||
.dropdown {
|
||||
line-height: 21px;
|
||||
}
|
||||
|
|
|
@ -30,19 +30,25 @@
|
|||
display: table-cell;
|
||||
}
|
||||
|
||||
.environments-name,
|
||||
.environments-commit,
|
||||
.environments-actions {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.environments-deploy,
|
||||
.environments-build,
|
||||
.environments-date {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.environments-name {
|
||||
width: 30%;
|
||||
.environments-deploy,
|
||||
.environments-build {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.environment-name,
|
||||
.environments-build-cell,
|
||||
.deployment-column {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.deployment-column {
|
||||
|
|
|
@ -27,12 +27,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.group-buttons {
|
||||
.notification-dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.groups-header {
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.nav-links {
|
||||
|
|
|
@ -1,3 +1,50 @@
|
|||
// Limit MR description for side-by-side diff view
|
||||
.limit-container-width {
|
||||
.detail-page-header {
|
||||
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.issuable-details {
|
||||
.detail-page-description,
|
||||
.mr-source-target,
|
||||
.mr-state-widget,
|
||||
.merge-manually {
|
||||
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.merge-request-tabs-holder {
|
||||
&.affix {
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.nav-links {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diffs {
|
||||
.mr-version-controls,
|
||||
.files-changed {
|
||||
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-details {
|
||||
section {
|
||||
.issuable-discussion {
|
||||
|
@ -9,7 +56,6 @@
|
|||
.description img:not(.emoji) {
|
||||
border: 1px solid $white-normal;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
}
|
||||
|
||||
.label {
|
||||
padding: 9px;
|
||||
padding: 8px 9px 9px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +201,8 @@
|
|||
.label-remove {
|
||||
border-left: 1px solid $label-remove-border;
|
||||
z-index: 3;
|
||||
border-radius: $label-border-radius;
|
||||
padding: 6px 10px 6px 9px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
|
|
@ -78,6 +78,21 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
margin-top: 0;
|
||||
width: 155px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding-right: 35px;
|
||||
|
@ -85,12 +100,22 @@
|
|||
@media (min-width: $screen-sm-min) {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
&.input-short {
|
||||
@media (min-width: $screen-md-min) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-lg-min) {
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-search-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: 4px;
|
||||
top: 0;
|
||||
height: 35px;
|
||||
padding-left: 10px;
|
||||
|
@ -99,4 +124,8 @@
|
|||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
right: 160px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,10 +383,6 @@ ul.notes {
|
|||
.note-action-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-actions {
|
||||
|
|
|
@ -22,17 +22,22 @@
|
|||
|
||||
.table.ci-table {
|
||||
min-width: 1200px;
|
||||
table-layout: fixed;
|
||||
|
||||
.pipeline-id {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.branch-commit {
|
||||
width: 30%;
|
||||
.pipeline-date,
|
||||
.pipeline-status {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.branch-name {
|
||||
max-width: 195px;
|
||||
}
|
||||
.pipeline-info,
|
||||
.pipeline-commit,
|
||||
.pipeline-actions,
|
||||
.pipeline-stages {
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +111,7 @@
|
|||
|
||||
.branch-name {
|
||||
font-weight: bold;
|
||||
max-width: 150px;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
|
@ -132,7 +137,7 @@
|
|||
|
||||
.commit-title {
|
||||
margin-top: 4px;
|
||||
max-width: 300px;
|
||||
max-width: 225px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -192,10 +197,6 @@
|
|||
border-bottom: 2px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,15 +289,40 @@
|
|||
}
|
||||
|
||||
// Pipeline visualization
|
||||
.pipeline-actions {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.toggle-pipeline-btn {
|
||||
background-color: $white-normal;
|
||||
.tab-pane {
|
||||
&.pipelines {
|
||||
.ci-table {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
&.graph-collapsed {
|
||||
background-color: $white-light;
|
||||
.content-list.pipelines {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.stage {
|
||||
max-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.pipeline-actions {
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.builds {
|
||||
.ci-table {
|
||||
tr {
|
||||
height: 71px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline graph
|
||||
.pipeline-graph {
|
||||
width: 100%;
|
||||
background-color: $gray-light;
|
||||
|
@ -305,52 +331,120 @@
|
|||
white-space: nowrap;
|
||||
transition: max-height 0.3s, padding 0.3s;
|
||||
|
||||
&.graph-collapsed {
|
||||
max-height: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-visualization {
|
||||
position: relative;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-column {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 44px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $gl-text-color-light;
|
||||
}
|
||||
|
||||
&.left-margin {
|
||||
&:not(:first-child) {
|
||||
margin-left: 44px;
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.left-connector {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 48%;
|
||||
left: -48px;
|
||||
border-top: 2px solid $border-color;
|
||||
width: 48px;
|
||||
height: 1px;
|
||||
.stage-column {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 44px;
|
||||
}
|
||||
|
||||
&.left-margin {
|
||||
&:not(:first-child) {
|
||||
margin-left: 44px;
|
||||
|
||||
.left-connector {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 48%;
|
||||
left: -48px;
|
||||
border-top: 2px solid $border-color;
|
||||
width: 48px;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
&.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.build {
|
||||
// Remove right connecting horizontal line from first build in last stage
|
||||
&:first-child {
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove right curved connectors from all builds in last stage
|
||||
&:not(:first-child) {
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove opposite curve
|
||||
.curve {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.build {
|
||||
// Remove left curved connectors from all builds in first stage
|
||||
&:not(:first-child) {
|
||||
&::before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove opposite curve
|
||||
.curve {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Curve first child connecting lines in opposite direction
|
||||
.curve {
|
||||
display: none;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
width: 21px;
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
top: -31px;
|
||||
border-top: 2px solid $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -44px;
|
||||
border-right: 2px solid $border-color;
|
||||
border-radius: 0 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: -44px;
|
||||
border-left: 2px solid $border-color;
|
||||
border-radius: 20px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
|
@ -363,164 +457,88 @@
|
|||
}
|
||||
|
||||
.build {
|
||||
border: 1px solid $border-color;
|
||||
background-color: $white-light;
|
||||
position: relative;
|
||||
padding: 7px 10px 8px;
|
||||
border-radius: 30px;
|
||||
width: 186px;
|
||||
margin-bottom: 10px;
|
||||
white-space: normal;
|
||||
color: $gl-text-color-light;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-lighter;
|
||||
}
|
||||
.dropdown-menu-toggle {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: $gl-text-color-light;
|
||||
|
||||
&.playable {
|
||||
|
||||
svg {
|
||||
height: 13px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
path {
|
||||
fill: $layout-link-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-content {
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
width: 164px;
|
||||
|
||||
.ci-status-icon {
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
color: $gl-text-color;
|
||||
|
||||
.tooltip-inner {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-text {
|
||||
width: 135px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gl-text-color-light;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
color: $gl-text-color-light;
|
||||
flex-grow: 1;
|
||||
|
||||
.ci-status-text {
|
||||
max-width: 112px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.grouped-pipeline-dropdown {
|
||||
padding: 0;
|
||||
width: 186px;
|
||||
left: auto;
|
||||
right: -197px;
|
||||
top: -9px;
|
||||
|
||||
ul {
|
||||
max-height: 245px;
|
||||
overflow: auto;
|
||||
|
||||
li:first-child {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
.dropdown-counter-badge {
|
||||
color: $gl-text-color;
|
||||
padding: 7px 8px 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-light-transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.ci-status-text {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.ci-status-text {
|
||||
width: 112px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -5px;
|
||||
margin-top: -6px;
|
||||
border-width: 7px 5px 7px 0;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -4px;
|
||||
margin-top: -9px;
|
||||
border-width: 10px 7px 10px 0;
|
||||
border-right-color: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-left: $btn-xs-side-margin;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
> .build-content {
|
||||
display: inline-block;
|
||||
padding: 8px 10px 9px;
|
||||
width: 100%;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 30px;
|
||||
background-color: $white-light;
|
||||
|
||||
&:hover {
|
||||
background-color: $stage-hover-bg;
|
||||
border: 1px solid $stage-hover-border;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
> .ci-action-icon-container {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.ci-status-icon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.ci-status-icon svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -5px;
|
||||
margin-top: -6px;
|
||||
border-width: 7px 5px 7px 0;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -4px;
|
||||
margin-top: -9px;
|
||||
border-width: 10px 7px 10px 0;
|
||||
border-right-color: $white-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect first build in each stage with right horizontal line
|
||||
|
@ -576,110 +594,299 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.build {
|
||||
// Remove right connecting horizontal line from first build in last stage
|
||||
&:first-child {
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove right curved connectors from all builds in last stage
|
||||
&:not(:first-child) {
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove opposite curve
|
||||
.curve {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
.dropdown-counter-badge {
|
||||
float: right;
|
||||
color: $border-color;
|
||||
font-weight: 100;
|
||||
font-size: 15px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.grouped-pipeline-dropdown {
|
||||
padding: 0;
|
||||
width: 191px;
|
||||
left: auto;
|
||||
right: -195px;
|
||||
top: -4px;
|
||||
box-shadow: 0 1px 5px $black-transparent;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
background-color: $stage-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
max-height: 245px;
|
||||
overflow: auto;
|
||||
margin: 5px 0;
|
||||
|
||||
li {
|
||||
padding-top: 2px;
|
||||
margin: 0 5px;
|
||||
padding-left: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-text {
|
||||
max-width: 110px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
// Action Icons
|
||||
.ci-action-icon-container .ci-action-icon-wrapper {
|
||||
i {
|
||||
color: $border-color;
|
||||
border-radius: 100%;
|
||||
border: 1px solid $border-color;
|
||||
padding: 5px 6px;
|
||||
font-size: 13px;
|
||||
background: $white-light;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
|
||||
&::before {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $gl-text-color;
|
||||
background-color: $stage-hover-bg;
|
||||
border: 1px solid $stage-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-play-icon {
|
||||
padding: 5px 5px 5px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-build {
|
||||
color: $gl-text-color-light;
|
||||
|
||||
.ci-action-icon-container {
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
float: right;
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $stage-hover-bg;
|
||||
border-radius: 3px;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.ci-action-icon-container {
|
||||
i {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
|
||||
&::before {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.build {
|
||||
// Remove left curved connectors from all builds in first stage
|
||||
&:not(:first-child) {
|
||||
&::before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove opposite curve
|
||||
.curve {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.stage {
|
||||
max-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.ci-status-icon svg {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.ci-status-text {
|
||||
max-width: 95px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds dropdown in mini pipeline
|
||||
*/
|
||||
.mini-pipeline-graph {
|
||||
.builds-dropdown {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: $gl-text-color-light;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.builds-dropdown-loading {
|
||||
margin: 10px auto;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.grouped-pipeline-dropdown {
|
||||
right: -172px;
|
||||
top: 23px;
|
||||
min-height: 50px;
|
||||
|
||||
a {
|
||||
color: $gl-text-color-light;
|
||||
}
|
||||
}
|
||||
|
||||
// Curve first child connecting lines in opposite direction
|
||||
.curve {
|
||||
display: none;
|
||||
|
||||
.arrow-up {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
width: 21px;
|
||||
height: 25px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: -32px;
|
||||
border-top: 2px solid $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -44px;
|
||||
border-right: 2px solid $border-color;
|
||||
border-radius: 0 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: -6px;
|
||||
left: 2px;
|
||||
border-width: 0 5px 6px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: -44px;
|
||||
border-left: 2px solid $border-color;
|
||||
border-radius: 20px 0 0;
|
||||
border-width: 0 5px 5px;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-top: 1px;
|
||||
border-bottom-color: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-actions {
|
||||
border-bottom: none;
|
||||
}
|
||||
/**
|
||||
* Icons in mini pipeline graph
|
||||
*/
|
||||
.mini-pipeline-graph-icon-container .ci-status-icon {
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-radius: 20px;
|
||||
margin-right: 1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
|
||||
|
||||
.toggle-pipeline-btn {
|
||||
|
||||
.fa {
|
||||
color: $gl-gray-light;
|
||||
svg {
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
.builds-dropdown {
|
||||
&:focus {
|
||||
outline: none;
|
||||
margin-right: -8px;
|
||||
|
||||
&.pipelines {
|
||||
.ci-status-icon {
|
||||
width: 28px;
|
||||
padding: 0 8px 0 0;
|
||||
transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
|
||||
|
||||
.ci-table {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.stage {
|
||||
max-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.pipeline-actions {
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.builds {
|
||||
|
||||
.ci-table {
|
||||
tr {
|
||||
height: 71px;
|
||||
+ .dropdown-caret {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
.ci-status-icon-success {
|
||||
background-color: rgba($gl-success, .1);
|
||||
}
|
||||
|
||||
.ci-status-icon-failed {
|
||||
background-color: rgba($gl-danger, .1);
|
||||
}
|
||||
|
||||
.ci-status-icon-pending,
|
||||
.ci-status-icon-success_with_warnings {
|
||||
background-color: rgba($gl-warning, .1);
|
||||
}
|
||||
|
||||
.ci-status-icon-running {
|
||||
background-color: rgba($blue-normal, .1);
|
||||
}
|
||||
|
||||
.ci-status-icon-canceled,
|
||||
.ci-status-icon-disabled,
|
||||
.ci-status-icon-not-found {
|
||||
background-color: rgba($gl-gray, .1);
|
||||
}
|
||||
|
||||
.ci-status-icon-created,
|
||||
.ci-status-icon-skipped {
|
||||
background-color: rgba($gray-darkest, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.mini-pipeline-graph-icon-container {
|
||||
.ci-status-icon:hover,
|
||||
.ci-status-icon:focus {
|
||||
width: 28px;
|
||||
padding: 0 8px 0 0;
|
||||
|
||||
+ .dropdown-caret {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-caret {
|
||||
font-size: 11px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
left: -11px;
|
||||
margin-right: -6px;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
.content-block {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
margin-top: 10px;
|
||||
min-height: 450px;
|
||||
box-sizing: border-box;
|
||||
|
||||
> div {
|
||||
min-height: 450px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -262,3 +262,13 @@ table.u2f-registrations {
|
|||
border-right: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-application-show {
|
||||
.scope-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scopes-list {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
|
@ -6,12 +6,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.no-ssh-key-message,
|
||||
.project-limit-message {
|
||||
background-color: $project-limit-message-bg;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.new_project,
|
||||
.edit-project {
|
||||
|
||||
|
@ -99,7 +93,6 @@
|
|||
.group-avatar {
|
||||
float: none;
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
|
||||
&.identicon {
|
||||
border-radius: 50%;
|
||||
|
@ -151,8 +144,6 @@
|
|||
|
||||
.project-repo-buttons,
|
||||
.group-buttons {
|
||||
margin-top: 15px;
|
||||
|
||||
.btn {
|
||||
@include btn-gray;
|
||||
padding: 3px 10px;
|
||||
|
@ -181,11 +172,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.download-button,
|
||||
.dropdown-toggle,
|
||||
.notification-dropdown,
|
||||
.project-dropdown {
|
||||
margin-left: 10px;
|
||||
.project-action-button {
|
||||
margin: 15px 5px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.notification-dropdown .dropdown-menu {
|
||||
|
@ -201,13 +190,15 @@
|
|||
.count-buttons {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.project-clone-holder {
|
||||
display: inline-block;
|
||||
margin: 15px 5px 0 0;
|
||||
|
||||
input {
|
||||
height: 29px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,7 +252,7 @@
|
|||
line-height: 13px;
|
||||
padding: $gl-vert-padding $gl-padding;
|
||||
letter-spacing: .4px;
|
||||
padding: 7px 14px;
|
||||
padding: 6px 14px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
|
@ -498,6 +489,7 @@ a.deploy-project-label {
|
|||
|
||||
.project-stats {
|
||||
font-size: 0;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.nav {
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
height: 13px;
|
||||
width: 13px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-right: 3px;
|
||||
top: 2px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
@ -25,7 +24,7 @@
|
|||
border-color: $gl-danger;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $gl-danger, .07);
|
||||
background-color: rgba($gl-danger, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -39,7 +38,7 @@
|
|||
border-color: $gl-success;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $gl-success, .07);
|
||||
background-color: rgba($gl-success, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -52,7 +51,7 @@
|
|||
border-color: $gl-info;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $gl-info, .07);
|
||||
background-color: rgba($gl-info, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -66,7 +65,7 @@
|
|||
border-color: $gl-gray;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $gl-gray, .07);
|
||||
background-color: rgba($gl-gray, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -79,7 +78,7 @@
|
|||
border-color: $gl-warning;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $gl-warning, .07);
|
||||
background-color: rgba($gl-warning, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -92,7 +91,7 @@
|
|||
border-color: $blue-normal;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $blue-normal, .07);
|
||||
background-color: rgba($blue-normal, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -106,13 +105,26 @@
|
|||
border-color: $gl-gray-light;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba( $gl-gray-light, .07);
|
||||
background-color: rgba($gl-gray-light, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $gl-gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
&.ci-manual {
|
||||
color: $gl-text-color;
|
||||
border-color: $gl-text-color;
|
||||
|
||||
&:not(span):hover {
|
||||
background-color: rgba($gl-text-color, .07);
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
.add-to-tree {
|
||||
vertical-align: top;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.tree-table {
|
||||
|
@ -172,7 +173,7 @@
|
|||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.download-button {
|
||||
.project-action-button {
|
||||
margin-left: $btn-side-margin;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
class Admin::ApplicationsController < Admin::ApplicationController
|
||||
include OauthApplications
|
||||
|
||||
before_action :set_application, only: [:show, :edit, :update, :destroy]
|
||||
before_action :load_scopes, only: [:new, :edit]
|
||||
|
||||
def index
|
||||
@applications = Doorkeeper::Application.where("owner_id IS NULL")
|
||||
|
@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
|
|||
|
||||
# Only allow a trusted parameter "white list" through.
|
||||
def application_params
|
||||
params[:doorkeeper_application].permit(:name, :redirect_uri)
|
||||
params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
|
|||
protect_from_forgery with: :exception
|
||||
|
||||
helper_method :can?, :current_application_settings
|
||||
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
|
||||
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
|
||||
|
||||
rescue_from Encoding::CompatibilityError do |exception|
|
||||
log_exception(exception)
|
||||
|
@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base
|
|||
current_application_settings.import_sources.include?('github')
|
||||
end
|
||||
|
||||
def gitea_import_enabled?
|
||||
current_application_settings.import_sources.include?('gitea')
|
||||
end
|
||||
|
||||
def github_import_configured?
|
||||
Gitlab::OAuth::Provider.enabled?(:github)
|
||||
end
|
||||
|
@ -262,7 +266,7 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def bitbucket_import_configured?
|
||||
Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
|
||||
Gitlab::OAuth::Provider.enabled?(:bitbucket)
|
||||
end
|
||||
|
||||
def google_code_import_enabled?
|
||||
|
|
19
app/controllers/concerns/oauth_applications.rb
Normal file
19
app/controllers/concerns/oauth_applications.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
module OauthApplications
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :prepare_scopes, only: [:create, :update]
|
||||
end
|
||||
|
||||
def prepare_scopes
|
||||
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
|
||||
|
||||
if scopes
|
||||
params[:doorkeeper_application][:scopes] = scopes.join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
def load_scopes
|
||||
@scopes = Doorkeeper.configuration.scopes
|
||||
end
|
||||
end
|
|
@ -1,20 +1,20 @@
|
|||
class Groups::GroupMembersController < Groups::ApplicationController
|
||||
include MembershipActions
|
||||
include SortingHelper
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
|
||||
|
||||
def index
|
||||
@sort = params[:sort].presence || sort_value_name
|
||||
@project = @group.projects.find(params[:project_id]) if params[:project_id]
|
||||
|
||||
@members = @group.group_members
|
||||
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
|
||||
@members = @members.search(params[:search]) if params[:search].present?
|
||||
@members = @members.sort(@sort)
|
||||
@members = @members.page(params[:page]).per(50)
|
||||
|
||||
if params[:search].present?
|
||||
users = @group.users.search(params[:search]).to_a
|
||||
@members = @members.where(user_id: users)
|
||||
end
|
||||
|
||||
@members = @members.order('access_level DESC').page(params[:page]).per(50)
|
||||
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
|
||||
|
||||
@group_member = @group.group_members.new
|
||||
|
|
|
@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
|
|||
before_action :verify_bitbucket_import_enabled
|
||||
before_action :bitbucket_auth, except: :callback
|
||||
|
||||
rescue_from OAuth::Error, with: :bitbucket_unauthorized
|
||||
rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
|
||||
rescue_from OAuth2::Error, with: :bitbucket_unauthorized
|
||||
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
|
||||
|
||||
def callback
|
||||
request_token = session.delete(:oauth_request_token)
|
||||
raise "Session expired!" if request_token.nil?
|
||||
response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
|
||||
|
||||
request_token.symbolize_keys!
|
||||
|
||||
access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
|
||||
|
||||
session[:bitbucket_access_token] = access_token.token
|
||||
session[:bitbucket_access_token_secret] = access_token.secret
|
||||
session[:bitbucket_token] = response.token
|
||||
session[:bitbucket_expires_at] = response.expires_at
|
||||
session[:bitbucket_expires_in] = response.expires_in
|
||||
session[:bitbucket_refresh_token] = response.refresh_token
|
||||
|
||||
redirect_to status_import_bitbucket_url
|
||||
end
|
||||
|
||||
def status
|
||||
@repos = client.projects
|
||||
@incompatible_repos = client.incompatible_projects
|
||||
bitbucket_client = Bitbucket::Client.new(credentials)
|
||||
repos = bitbucket_client.repos
|
||||
|
||||
@already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
|
||||
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
|
||||
|
||||
@already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
|
||||
already_added_projects_names = @already_added_projects.pluck(:import_source)
|
||||
|
||||
@repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
|
||||
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
|
||||
end
|
||||
|
||||
def jobs
|
||||
jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
|
||||
render json: jobs
|
||||
render json: current_user.created_projects
|
||||
.where(import_type: 'bitbucket')
|
||||
.to_json(only: [:id, :import_status])
|
||||
end
|
||||
|
||||
def create
|
||||
bitbucket_client = Bitbucket::Client.new(credentials)
|
||||
|
||||
@repo_id = params[:repo_id].to_s
|
||||
repo = client.project(@repo_id.gsub('___', '/'))
|
||||
@project_name = repo['slug']
|
||||
@target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
|
||||
name = @repo_id.gsub('___', '/')
|
||||
repo = bitbucket_client.repo(name)
|
||||
@project_name = params[:new_name].presence || repo.name
|
||||
|
||||
unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
|
||||
render 'deploy_key' and return
|
||||
end
|
||||
repo_owner = repo.owner
|
||||
repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
|
||||
@target_namespace = params[:new_namespace].presence || repo_owner
|
||||
|
||||
if current_user.can?(:create_projects, @target_namespace)
|
||||
@project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
|
||||
namespace = find_or_create_namespace(@target_namespace, current_user)
|
||||
|
||||
if current_user.can?(:create_projects, namespace)
|
||||
# The token in a session can be expired, we need to get most recent one because
|
||||
# Bitbucket::Connection class refreshes it.
|
||||
session[:bitbucket_token] = bitbucket_client.connection.token
|
||||
@project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
|
||||
else
|
||||
render 'unauthorized'
|
||||
end
|
||||
|
@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController
|
|||
private
|
||||
|
||||
def client
|
||||
@client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token],
|
||||
session[:bitbucket_access_token_secret])
|
||||
@client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
|
||||
end
|
||||
|
||||
def provider
|
||||
Gitlab::OAuth::Provider.config_for('bitbucket')
|
||||
end
|
||||
|
||||
def options
|
||||
OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
|
||||
end
|
||||
|
||||
def verify_bitbucket_import_enabled
|
||||
|
@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController
|
|||
end
|
||||
|
||||
def bitbucket_auth
|
||||
if session[:bitbucket_access_token].blank?
|
||||
go_to_bitbucket_for_permissions
|
||||
end
|
||||
go_to_bitbucket_for_permissions if session[:bitbucket_token].blank?
|
||||
end
|
||||
|
||||
def go_to_bitbucket_for_permissions
|
||||
request_token = client.request_token(callback_import_bitbucket_url)
|
||||
session[:oauth_request_token] = request_token
|
||||
|
||||
redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
|
||||
redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
|
||||
end
|
||||
|
||||
def bitbucket_unauthorized
|
||||
go_to_bitbucket_for_permissions
|
||||
end
|
||||
|
||||
def access_params
|
||||
def credentials
|
||||
{
|
||||
bitbucket_access_token: session[:bitbucket_access_token],
|
||||
bitbucket_access_token_secret: session[:bitbucket_access_token_secret]
|
||||
token: session[:bitbucket_token],
|
||||
expires_at: session[:bitbucket_expires_at],
|
||||
expires_in: session[:bitbucket_expires_in],
|
||||
refresh_token: session[:bitbucket_refresh_token]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
45
app/controllers/import/gitea_controller.rb
Normal file
45
app/controllers/import/gitea_controller.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
class Import::GiteaController < Import::GithubController
|
||||
def new
|
||||
if session[access_token_key].present? && session[host_key].present?
|
||||
redirect_to status_import_url
|
||||
end
|
||||
end
|
||||
|
||||
def personal_access_token
|
||||
session[host_key] = params[host_key]
|
||||
super
|
||||
end
|
||||
|
||||
def status
|
||||
@gitea_host_url = session[host_key]
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def host_key
|
||||
:"#{provider}_host_url"
|
||||
end
|
||||
|
||||
# Overriden methods
|
||||
def provider
|
||||
:gitea
|
||||
end
|
||||
|
||||
# Gitea is not yet an OAuth provider
|
||||
# See https://github.com/go-gitea/gitea/issues/27
|
||||
def logged_in_with_provider?
|
||||
false
|
||||
end
|
||||
|
||||
def provider_auth
|
||||
if session[access_token_key].blank? || session[host_key].blank?
|
||||
redirect_to new_import_gitea_url,
|
||||
alert: 'You need to specify both an Access Token and a Host URL.'
|
||||
end
|
||||
end
|
||||
|
||||
def client_options
|
||||
{ host: session[host_key], api_version: 'v1' }
|
||||
end
|
||||
end
|
|
@ -1,39 +1,37 @@
|
|||
class Import::GithubController < Import::BaseController
|
||||
before_action :verify_github_import_enabled
|
||||
before_action :github_auth, only: [:status, :jobs, :create]
|
||||
before_action :verify_import_enabled
|
||||
before_action :provider_auth, only: [:status, :jobs, :create]
|
||||
|
||||
rescue_from Octokit::Unauthorized, with: :github_unauthorized
|
||||
|
||||
helper_method :logged_in_with_github?
|
||||
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
|
||||
|
||||
def new
|
||||
if logged_in_with_github?
|
||||
go_to_github_for_permissions
|
||||
elsif session[:github_access_token]
|
||||
redirect_to status_import_github_url
|
||||
if logged_in_with_provider?
|
||||
go_to_provider_for_permissions
|
||||
elsif session[access_token_key]
|
||||
redirect_to status_import_url
|
||||
end
|
||||
end
|
||||
|
||||
def callback
|
||||
session[:github_access_token] = client.get_token(params[:code])
|
||||
redirect_to status_import_github_url
|
||||
session[access_token_key] = client.get_token(params[:code])
|
||||
redirect_to status_import_url
|
||||
end
|
||||
|
||||
def personal_access_token
|
||||
session[:github_access_token] = params[:personal_access_token]
|
||||
redirect_to status_import_github_url
|
||||
session[access_token_key] = params[:personal_access_token]
|
||||
redirect_to status_import_url
|
||||
end
|
||||
|
||||
def status
|
||||
@repos = client.repos
|
||||
@already_added_projects = current_user.created_projects.where(import_type: "github")
|
||||
@already_added_projects = current_user.created_projects.where(import_type: provider)
|
||||
already_added_projects_names = @already_added_projects.pluck(:import_source)
|
||||
|
||||
@repos.reject!{ |repo| already_added_projects_names.include? repo.full_name }
|
||||
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
|
||||
end
|
||||
|
||||
def jobs
|
||||
jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status])
|
||||
jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
|
||||
render json: jobs
|
||||
end
|
||||
|
||||
|
@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController
|
|||
namespace_path = params[:target_namespace].presence || current_user.namespace_path
|
||||
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
|
||||
|
||||
if current_user.can?(:create_projects, @target_namespace)
|
||||
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
|
||||
if can?(current_user, :create_projects, @target_namespace)
|
||||
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
|
||||
else
|
||||
render 'unauthorized'
|
||||
end
|
||||
|
@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController
|
|||
private
|
||||
|
||||
def client
|
||||
@client ||= Gitlab::GithubImport::Client.new(session[:github_access_token])
|
||||
@client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
|
||||
end
|
||||
|
||||
def verify_github_import_enabled
|
||||
render_404 unless github_import_enabled?
|
||||
def verify_import_enabled
|
||||
render_404 unless import_enabled?
|
||||
end
|
||||
|
||||
def github_auth
|
||||
if session[:github_access_token].blank?
|
||||
go_to_github_for_permissions
|
||||
end
|
||||
def go_to_provider_for_permissions
|
||||
redirect_to client.authorize_url(callback_import_url)
|
||||
end
|
||||
|
||||
def go_to_github_for_permissions
|
||||
redirect_to client.authorize_url(callback_import_github_url)
|
||||
def import_enabled?
|
||||
__send__("#{provider}_import_enabled?")
|
||||
end
|
||||
|
||||
def github_unauthorized
|
||||
session[:github_access_token] = nil
|
||||
redirect_to new_import_github_url,
|
||||
alert: 'Access denied to your GitHub account.'
|
||||
def new_import_url
|
||||
public_send("new_import_#{provider}_url")
|
||||
end
|
||||
|
||||
def logged_in_with_github?
|
||||
current_user.identities.exists?(provider: 'github')
|
||||
def status_import_url
|
||||
public_send("status_import_#{provider}_url")
|
||||
end
|
||||
|
||||
def callback_import_url
|
||||
public_send("callback_import_#{provider}_url")
|
||||
end
|
||||
|
||||
def provider_unauthorized
|
||||
session[access_token_key] = nil
|
||||
redirect_to new_import_url,
|
||||
alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account."
|
||||
end
|
||||
|
||||
def access_token_key
|
||||
:"#{provider}_access_token"
|
||||
end
|
||||
|
||||
def access_params
|
||||
{ github_access_token: session[:github_access_token] }
|
||||
{ github_access_token: session[access_token_key] }
|
||||
end
|
||||
|
||||
# The following methods are overriden in subclasses
|
||||
def provider
|
||||
:github
|
||||
end
|
||||
|
||||
def logged_in_with_provider?
|
||||
current_user.identities.exists?(provider: provider)
|
||||
end
|
||||
|
||||
def provider_auth
|
||||
if session[access_token_key].blank?
|
||||
go_to_provider_for_permissions
|
||||
end
|
||||
end
|
||||
|
||||
def client_options
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,7 +26,7 @@ class JwtController < ApplicationController
|
|||
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
|
||||
|
||||
render_unauthorized unless @authentication_result.success? &&
|
||||
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
|
||||
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
|
||||
end
|
||||
rescue Gitlab::Auth::MissingPersonalTokenError
|
||||
render_missing_personal_token
|
||||
|
|
|
@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
|||
include Gitlab::CurrentSettings
|
||||
include Gitlab::GonHelper
|
||||
include PageLayoutHelper
|
||||
include OauthApplications
|
||||
|
||||
before_action :verify_user_oauth_applications_enabled
|
||||
before_action :authenticate_user!
|
||||
before_action :add_gon_variables
|
||||
before_action :load_scopes, only: [:index, :create, :edit]
|
||||
|
||||
layout 'profile'
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
|
||||
before_action :load_personal_access_tokens, only: :index
|
||||
|
||||
def index
|
||||
@personal_access_token = current_user.personal_access_tokens.build
|
||||
set_index_vars
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
|
|||
flash[:personal_access_token] = @personal_access_token.token
|
||||
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
|
||||
else
|
||||
load_personal_access_tokens
|
||||
set_index_vars
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
|
|||
private
|
||||
|
||||
def personal_access_token_params
|
||||
params.require(:personal_access_token).permit(:name, :expires_at)
|
||||
params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
|
||||
end
|
||||
|
||||
def load_personal_access_tokens
|
||||
def set_index_vars
|
||||
@personal_access_token ||= current_user.personal_access_tokens.build
|
||||
@scopes = Gitlab::Auth::SCOPES
|
||||
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
|
||||
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
|
||||
end
|
||||
|
|
|
@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
end
|
||||
|
||||
@qr_code = build_qr_code
|
||||
@account_string = account_string
|
||||
setup_u2f_registration
|
||||
end
|
||||
|
||||
|
@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
private
|
||||
|
||||
def build_qr_code
|
||||
issuer = "#{issuer_host} | #{current_user.email}"
|
||||
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
|
||||
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
|
||||
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
|
||||
end
|
||||
|
||||
def account_string
|
||||
"#{issuer_host}:#{current_user.email}"
|
||||
end
|
||||
|
||||
def issuer_host
|
||||
Gitlab.config.gitlab.host
|
||||
end
|
||||
|
|
48
app/controllers/projects/autocomplete_sources_controller.rb
Normal file
48
app/controllers/projects/autocomplete_sources_controller.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
||||
before_action :load_autocomplete_service, except: [:emojis, :members]
|
||||
|
||||
def emojis
|
||||
render json: Gitlab::AwardEmoji.urls
|
||||
end
|
||||
|
||||
def members
|
||||
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
|
||||
end
|
||||
|
||||
def issues
|
||||
render json: @autocomplete_service.issues
|
||||
end
|
||||
|
||||
def merge_requests
|
||||
render json: @autocomplete_service.merge_requests
|
||||
end
|
||||
|
||||
def labels
|
||||
render json: @autocomplete_service.labels
|
||||
end
|
||||
|
||||
def milestones
|
||||
render json: @autocomplete_service.milestones
|
||||
end
|
||||
|
||||
def commands
|
||||
render json: @autocomplete_service.commands(noteable, params[:type])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_autocomplete_service
|
||||
@autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
|
||||
end
|
||||
|
||||
def noteable
|
||||
case params[:type]
|
||||
when 'Issue'
|
||||
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
|
||||
when 'MergeRequest'
|
||||
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
|
||||
when 'Commit'
|
||||
@project.commit(params[:type_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,9 @@ class Projects::BlameController < Projects::ApplicationController
|
|||
|
||||
def show
|
||||
@blob = @repository.blob_at(@commit.id, @path)
|
||||
|
||||
return render_404 unless @blob
|
||||
|
||||
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,17 +4,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
before_action :authorize_create_environment!, only: [:new, :create]
|
||||
before_action :authorize_create_deployment!, only: [:stop]
|
||||
before_action :authorize_update_environment!, only: [:edit, :update]
|
||||
before_action :environment, only: [:show, :edit, :update, :stop]
|
||||
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
|
||||
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
|
||||
before_action :verify_api_request!, only: :terminal_websocket_authorize
|
||||
|
||||
def index
|
||||
@scope = params[:scope]
|
||||
@environments = project.environments
|
||||
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: EnvironmentSerializer
|
||||
.new(project: @project)
|
||||
.new(project: @project, user: current_user)
|
||||
.represent(@environments)
|
||||
end
|
||||
end
|
||||
|
@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
|
||||
end
|
||||
|
||||
def terminal
|
||||
# Currently, this acts as a hint to load the terminal details into the cache
|
||||
# if they aren't there already. In the future, users will need these details
|
||||
# to choose between terminals to connect to.
|
||||
@terminals = environment.terminals
|
||||
end
|
||||
|
||||
# GET .../terminal.ws : implemented in gitlab-workhorse
|
||||
def terminal_websocket_authorize
|
||||
# Just return the first terminal for now. If the list is in the process of
|
||||
# being looked up, this may result in a 404 response, so the frontend
|
||||
# should retry those errors
|
||||
terminal = environment.terminals.try(:first)
|
||||
if terminal
|
||||
set_workhorse_internal_api_content_type
|
||||
render json: Gitlab::Workhorse.terminal_websocket(terminal)
|
||||
else
|
||||
render text: 'Not found', status: 404
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_api_request!
|
||||
Gitlab::Workhorse.verify_api_request!(request.headers)
|
||||
end
|
||||
|
||||
def environment_params
|
||||
params.require(:environment).permit(:name, :external_url)
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
def index
|
||||
@scope = params[:scope]
|
||||
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
|
||||
@pipelines = @pipelines.includes(project: :namespace)
|
||||
|
||||
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
|
||||
@pipelines_count = PipelinesFinder.new(project).execute.count
|
||||
|
@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def stage
|
||||
@stage = pipeline.stage(params[:stage])
|
||||
return not_found unless @stage
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
|
||||
end
|
||||
end
|
||||
|
||||
def retry
|
||||
pipeline.retry_failed(current_user)
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
class Projects::ProjectMembersController < Projects::ApplicationController
|
||||
include MembershipActions
|
||||
include SortingHelper
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
|
||||
|
||||
def index
|
||||
@sort = params[:sort].presence || sort_value_name
|
||||
@group_links = @project.project_group_links
|
||||
|
||||
@project_members = @project.project_members
|
||||
|
@ -35,12 +37,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
|
|||
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
|
||||
end
|
||||
|
||||
wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
|
||||
wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
|
||||
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
|
||||
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
|
||||
|
||||
@project_members = Member.
|
||||
where(wheres.join(' OR ')).
|
||||
order(access_level: :desc).page(params[:page])
|
||||
sort(@sort).
|
||||
page(params[:page])
|
||||
|
||||
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
|
||||
|
||||
|
|
|
@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
|
|||
redirect_to edit_project_path(@project), alert: ex.message
|
||||
end
|
||||
|
||||
def autocomplete_sources
|
||||
noteable =
|
||||
case params[:type]
|
||||
when 'Issue'
|
||||
IssuesFinder.new(current_user, project_id: @project.id).
|
||||
execute.find_by(iid: params[:type_id])
|
||||
when 'MergeRequest'
|
||||
MergeRequestsFinder.new(current_user, project_id: @project.id).
|
||||
execute.find_by(iid: params[:type_id])
|
||||
when 'Commit'
|
||||
@project.commit(params[:type_id])
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
|
||||
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
|
||||
|
||||
@suggestions = {
|
||||
emojis: Gitlab::AwardEmoji.urls,
|
||||
issues: autocomplete.issues,
|
||||
milestones: autocomplete.milestones,
|
||||
mergerequests: autocomplete.merge_requests,
|
||||
labels: autocomplete.labels,
|
||||
members: participants,
|
||||
commands: autocomplete.commands(noteable, params[:type])
|
||||
}
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: @suggestions }
|
||||
end
|
||||
end
|
||||
|
||||
def new_issue_address
|
||||
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ class SessionsController < Devise::SessionsController
|
|||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
end
|
||||
|
||||
def log_audit_event(user, options = {})
|
||||
|
|
|
@ -191,6 +191,10 @@ module BlobHelper
|
|||
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
|
||||
end
|
||||
|
||||
def dockerfile_names
|
||||
@dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
|
||||
end
|
||||
|
||||
def blob_editor_paths
|
||||
{
|
||||
'relative-url-root' => Rails.application.config.relative_url_root,
|
||||
|
|
|
@ -128,50 +128,11 @@ module CommitsHelper
|
|||
end
|
||||
|
||||
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
|
||||
return unless current_user
|
||||
|
||||
tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
|
||||
|
||||
if can_collaborate_with_project?
|
||||
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
|
||||
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
|
||||
elsif can?(current_user, :fork_project, @project)
|
||||
continue_params = {
|
||||
to: continue_to_path,
|
||||
notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
|
||||
notice_now: edit_in_new_fork_notice_now
|
||||
}
|
||||
fork_path = namespace_project_forks_path(@project.namespace, @project,
|
||||
namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
|
||||
btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
|
||||
|
||||
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
|
||||
end
|
||||
commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
|
||||
end
|
||||
|
||||
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
|
||||
return unless current_user
|
||||
|
||||
tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
|
||||
|
||||
if can_collaborate_with_project?
|
||||
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
|
||||
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
|
||||
elsif can?(current_user, :fork_project, @project)
|
||||
continue_params = {
|
||||
to: continue_to_path,
|
||||
notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
|
||||
notice_now: edit_in_new_fork_notice_now
|
||||
}
|
||||
fork_path = namespace_project_forks_path(@project.namespace, @project,
|
||||
namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
|
||||
btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
|
||||
link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
|
||||
end
|
||||
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
|
||||
end
|
||||
|
||||
protected
|
||||
|
@ -211,6 +172,28 @@ module CommitsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true)
|
||||
return unless current_user
|
||||
|
||||
tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
|
||||
btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
|
||||
|
||||
if can_collaborate_with_project?
|
||||
link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
|
||||
elsif can?(current_user, :fork_project, @project)
|
||||
continue_params = {
|
||||
to: continue_to_path,
|
||||
notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.",
|
||||
notice_now: edit_in_new_fork_notice_now
|
||||
}
|
||||
fork_path = namespace_project_forks_path(@project.namespace, @project,
|
||||
namespace_key: current_user.namespace.id,
|
||||
continue: continue_params)
|
||||
|
||||
link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
|
||||
end
|
||||
end
|
||||
|
||||
def view_file_btn(commit_sha, diff_new_path, project)
|
||||
link_to(
|
||||
namespace_project_blob_path(project.namespace, project,
|
||||
|
|
|
@ -7,12 +7,12 @@ module FormHelper
|
|||
|
||||
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
|
||||
content_tag(:h4, headline) <<
|
||||
content_tag(:ul) do
|
||||
model.errors.full_messages.
|
||||
map { |msg| content_tag(:li, msg) }.
|
||||
join.
|
||||
html_safe
|
||||
end
|
||||
content_tag(:ul) do
|
||||
model.errors.full_messages.
|
||||
map { |msg| content_tag(:li, msg) }.
|
||||
join.
|
||||
html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,8 +4,10 @@ module ImportHelper
|
|||
"#{namespace}/#{name}"
|
||||
end
|
||||
|
||||
def github_project_link(path_with_namespace)
|
||||
link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
|
||||
def provider_project_link(provider, path_with_namespace)
|
||||
url = __send__("#{provider}_project_url", path_with_namespace)
|
||||
|
||||
link_to path_with_namespace, url, target: '_blank'
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -20,4 +22,8 @@ module ImportHelper
|
|||
provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
|
||||
@github_url = provider.fetch('url', 'https://github.com') if provider
|
||||
end
|
||||
|
||||
def gitea_project_url(path_with_namespace)
|
||||
"#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,4 +36,12 @@ module MembersHelper
|
|||
"Are you sure you want to leave the " \
|
||||
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
|
||||
end
|
||||
|
||||
def filter_group_project_member_path(options = {})
|
||||
options = params.slice(:search, :sort).merge(options)
|
||||
|
||||
path = request.path
|
||||
path << "?#{options.to_param}"
|
||||
path
|
||||
end
|
||||
end
|
||||
|
|
|
@ -59,6 +59,10 @@ module MergeRequestsHelper
|
|||
@mr_closes_issues ||= @merge_request.closes_issues
|
||||
end
|
||||
|
||||
def mr_issues_mentioned_but_not_closing
|
||||
@mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
|
||||
end
|
||||
|
||||
def mr_change_branches_path(merge_request)
|
||||
new_namespace_project_merge_request_path(
|
||||
@project.namespace, @project,
|
||||
|
|
|
@ -7,12 +7,12 @@ module NavHelper
|
|||
|
||||
def page_gutter_class
|
||||
if current_path?('merge_requests#show') ||
|
||||
current_path?('merge_requests#diffs') ||
|
||||
current_path?('merge_requests#commits') ||
|
||||
current_path?('merge_requests#builds') ||
|
||||
current_path?('merge_requests#conflicts') ||
|
||||
current_path?('merge_requests#pipelines') ||
|
||||
current_path?('issues#show')
|
||||
current_path?('merge_requests#diffs') ||
|
||||
current_path?('merge_requests#commits') ||
|
||||
current_path?('merge_requests#builds') ||
|
||||
current_path?('merge_requests#conflicts') ||
|
||||
current_path?('merge_requests#pipelines') ||
|
||||
current_path?('issues#show')
|
||||
if cookies[:collapsed_gutter] == 'true'
|
||||
"page-gutter right-sidebar-collapsed"
|
||||
else
|
||||
|
@ -21,9 +21,9 @@ module NavHelper
|
|||
elsif current_path?('builds#show')
|
||||
"page-gutter build-sidebar right-sidebar-expanded"
|
||||
elsif current_path?('wikis#show') ||
|
||||
current_path?('wikis#edit') ||
|
||||
current_path?('wikis#history') ||
|
||||
current_path?('wikis#git_access')
|
||||
current_path?('wikis#edit') ||
|
||||
current_path?('wikis#history') ||
|
||||
current_path?('wikis#git_access')
|
||||
"page-gutter wiki-sidebar right-sidebar-expanded"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ module SortingHelper
|
|||
sort_value_recently_updated => sort_title_recently_updated,
|
||||
sort_value_oldest_updated => sort_title_oldest_updated,
|
||||
sort_value_recently_created => sort_title_recently_created,
|
||||
sort_value_oldest_created => sort_title_oldest_created,
|
||||
sort_value_oldest_created => sort_title_oldest_created
|
||||
}
|
||||
|
||||
if current_controller?('admin/projects')
|
||||
|
@ -35,6 +35,19 @@ module SortingHelper
|
|||
options
|
||||
end
|
||||
|
||||
def member_sort_options_hash
|
||||
{
|
||||
sort_value_access_level_asc => sort_title_access_level_asc,
|
||||
sort_value_access_level_desc => sort_title_access_level_desc,
|
||||
sort_value_last_joined => sort_title_last_joined,
|
||||
sort_value_oldest_joined => sort_title_oldest_joined,
|
||||
sort_value_name => sort_title_name_asc,
|
||||
sort_value_name_desc => sort_title_name_desc,
|
||||
sort_value_recently_signin => sort_title_recently_signin,
|
||||
sort_value_oldest_signin => sort_title_oldest_signin
|
||||
}
|
||||
end
|
||||
|
||||
def sort_title_priority
|
||||
'Priority'
|
||||
end
|
||||
|
@ -95,6 +108,50 @@ module SortingHelper
|
|||
'Most popular'
|
||||
end
|
||||
|
||||
def sort_title_last_joined
|
||||
'Last joined'
|
||||
end
|
||||
|
||||
def sort_title_oldest_joined
|
||||
'Oldest joined'
|
||||
end
|
||||
|
||||
def sort_title_access_level_asc
|
||||
'Access level, ascending'
|
||||
end
|
||||
|
||||
def sort_title_access_level_desc
|
||||
'Access level, descending'
|
||||
end
|
||||
|
||||
def sort_title_name_asc
|
||||
'Name, ascending'
|
||||
end
|
||||
|
||||
def sort_title_name_desc
|
||||
'Name, descending'
|
||||
end
|
||||
|
||||
def sort_value_last_joined
|
||||
'last_joined'
|
||||
end
|
||||
|
||||
def sort_value_oldest_joined
|
||||
'oldest_joined'
|
||||
end
|
||||
|
||||
def sort_value_access_level_asc
|
||||
'access_level_asc'
|
||||
end
|
||||
|
||||
def sort_value_access_level_desc
|
||||
'access_level_desc'
|
||||
end
|
||||
|
||||
def sort_value_name_desc
|
||||
'name_desc'
|
||||
end
|
||||
|
||||
def sort_value_priority
|
||||
'priority'
|
||||
end
|
||||
|
|
|
@ -106,9 +106,9 @@ module TabHelper
|
|||
|
||||
def branches_tab_class
|
||||
if current_controller?(:protected_branches) ||
|
||||
current_controller?(:branches) ||
|
||||
current_page?(namespace_project_repository_path(@project.namespace,
|
||||
@project))
|
||||
current_controller?(:branches) ||
|
||||
current_page?(namespace_project_repository_path(@project.namespace,
|
||||
@project))
|
||||
'active'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ module Ci
|
|||
end
|
||||
|
||||
serialize :options
|
||||
serialize :yaml_variables
|
||||
serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
|
||||
|
||||
validates :coverage, numericality: true, allow_blank: true
|
||||
validates_presence_of :ref
|
||||
|
@ -155,7 +155,7 @@ module Ci
|
|||
end
|
||||
|
||||
def has_environment?
|
||||
self.environment.present?
|
||||
environment.present?
|
||||
end
|
||||
|
||||
def starts_environment?
|
||||
|
@ -221,6 +221,7 @@ module Ci
|
|||
variables += pipeline.predefined_variables
|
||||
variables += runner.predefined_variables if runner
|
||||
variables += project.container_registry_variables
|
||||
variables += project.deployment_variables if has_environment?
|
||||
variables += yaml_variables
|
||||
variables += user_variables
|
||||
variables += project.secret_variables
|
||||
|
|
|
@ -116,6 +116,11 @@ module Ci
|
|||
where.not(duration: nil).sum(:duration)
|
||||
end
|
||||
|
||||
def stage(name)
|
||||
stage = Ci::Stage.new(self, name: name)
|
||||
stage unless stage.statuses_count.zero?
|
||||
end
|
||||
|
||||
def stages_count
|
||||
statuses.select(:stage).distinct.count
|
||||
end
|
||||
|
|
|
@ -18,6 +18,10 @@ module Ci
|
|||
name
|
||||
end
|
||||
|
||||
def statuses_count
|
||||
@statuses_count ||= statuses.count
|
||||
end
|
||||
|
||||
def status
|
||||
@status ||= statuses.latest.status
|
||||
end
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
module Milestoneish
|
||||
def closed_items_count(user)
|
||||
issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
|
||||
memoize_per_user(user, :closed_items_count) do
|
||||
(count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size
|
||||
end
|
||||
end
|
||||
|
||||
def total_items_count(user)
|
||||
issues_visible_to_user(user).size + merge_requests.size
|
||||
memoize_per_user(user, :total_items_count) do
|
||||
issues_count = count_issues_by_state(user).values.sum
|
||||
issues_count + merge_requests.size
|
||||
end
|
||||
end
|
||||
|
||||
def complete?(user)
|
||||
|
@ -30,7 +35,10 @@ module Milestoneish
|
|||
end
|
||||
|
||||
def issues_visible_to_user(user)
|
||||
IssuesFinder.new(user).execute.where(id: issues)
|
||||
memoize_per_user(user, :issues_visible_to_user) do
|
||||
params = try(:project_id) ? { project_id: project_id } : {}
|
||||
IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def upcoming?
|
||||
|
@ -50,4 +58,18 @@ module Milestoneish
|
|||
def expired?
|
||||
due_date && due_date.past?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def count_issues_by_state(user)
|
||||
memoize_per_user(user, :count_issues_by_state) do
|
||||
issues_visible_to_user(user).reorder(nil).group(:state).count
|
||||
end
|
||||
end
|
||||
|
||||
def memoize_per_user(user, method_name)
|
||||
@memoized ||= {}
|
||||
@memoized[method_name] ||= {}
|
||||
@memoized[method_name][user.try!(:id)] ||= yield
|
||||
end
|
||||
end
|
||||
|
|
114
app/models/concerns/reactive_caching.rb
Normal file
114
app/models/concerns/reactive_caching.rb
Normal file
|
@ -0,0 +1,114 @@
|
|||
# The ReactiveCaching concern is used to fetch some data in the background and
|
||||
# store it in the Rails cache, keeping it up-to-date for as long as it is being
|
||||
# requested. If the data hasn't been requested for +reactive_cache_lifetime+,
|
||||
# it stop being refreshed, and then be removed.
|
||||
#
|
||||
# Example of use:
|
||||
#
|
||||
# class Foo < ActiveRecord::Base
|
||||
# include ReactiveCaching
|
||||
#
|
||||
# self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
|
||||
#
|
||||
# after_save :clear_reactive_cache!
|
||||
#
|
||||
# def calculate_reactive_cache
|
||||
# # Expensive operation here. The return value of this method is cached
|
||||
# end
|
||||
#
|
||||
# def result
|
||||
# with_reactive_cache do |data|
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# In this example, the first time `#result` is called, it will return `nil`.
|
||||
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
|
||||
# and set an initial cache lifetime of ten minutes.
|
||||
#
|
||||
# Each time the background job completes, it stores the return value of
|
||||
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
|
||||
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
|
||||
# Calculations are never run concurrently.
|
||||
#
|
||||
# Calling `#result` while a value is in the cache will call the block given to
|
||||
# `#with_reactive_cache`, yielding the cached value. It will also extend the
|
||||
# lifetime by `reactive_cache_lifetime`.
|
||||
#
|
||||
# Once the lifetime has expired, no more background jobs will be enqueued and
|
||||
# calling `#result` will again return `nil` - starting the process all over
|
||||
# again
|
||||
module ReactiveCaching
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
class_attribute :reactive_cache_lease_timeout
|
||||
|
||||
class_attribute :reactive_cache_key
|
||||
class_attribute :reactive_cache_lifetime
|
||||
class_attribute :reactive_cache_refresh_interval
|
||||
|
||||
# defaults
|
||||
self.reactive_cache_lease_timeout = 2.minutes
|
||||
|
||||
self.reactive_cache_refresh_interval = 1.minute
|
||||
self.reactive_cache_lifetime = 10.minutes
|
||||
|
||||
def calculate_reactive_cache
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def with_reactive_cache(&blk)
|
||||
within_reactive_cache_lifetime do
|
||||
data = Rails.cache.read(full_reactive_cache_key)
|
||||
yield data if data.present?
|
||||
end
|
||||
ensure
|
||||
Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime)
|
||||
ReactiveCachingWorker.perform_async(self.class, id)
|
||||
end
|
||||
|
||||
def clear_reactive_cache!
|
||||
Rails.cache.delete(full_reactive_cache_key)
|
||||
end
|
||||
|
||||
def exclusively_update_reactive_cache!
|
||||
locking_reactive_cache do
|
||||
within_reactive_cache_lifetime do
|
||||
enqueuing_update do
|
||||
value = calculate_reactive_cache
|
||||
Rails.cache.write(full_reactive_cache_key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def full_reactive_cache_key(*qualifiers)
|
||||
prefix = self.class.reactive_cache_key
|
||||
prefix = prefix.call(self) if prefix.respond_to?(:call)
|
||||
|
||||
([prefix].flatten + qualifiers).join(':')
|
||||
end
|
||||
|
||||
def locking_reactive_cache
|
||||
lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout)
|
||||
uuid = lease.try_obtain
|
||||
yield if uuid
|
||||
ensure
|
||||
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid)
|
||||
end
|
||||
|
||||
def within_reactive_cache_lifetime
|
||||
yield if Rails.cache.read(full_reactive_cache_key('alive'))
|
||||
end
|
||||
|
||||
def enqueuing_update
|
||||
yield
|
||||
ensure
|
||||
ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def has_terminals?
|
||||
project.deployment_service.present? && available? && last_deployment.present?
|
||||
end
|
||||
|
||||
def terminals
|
||||
project.deployment_service.terminals(self) if has_terminals?
|
||||
end
|
||||
|
||||
# An environment name is not necessarily suitable for use in URLs, DNS
|
||||
# or other third-party contexts, so provide a slugified version. A slug has
|
||||
# the following properties:
|
||||
|
|
|
@ -24,12 +24,16 @@ class GlobalMilestone
|
|||
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
|
||||
end
|
||||
|
||||
def milestoneish_ids
|
||||
milestones.select(:id)
|
||||
end
|
||||
|
||||
def safe_title
|
||||
@title.to_slug.normalize.to_s
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= Project.for_milestones(milestones.select(:id))
|
||||
@projects ||= Project.for_milestones(milestoneish_ids)
|
||||
end
|
||||
|
||||
def state
|
||||
|
@ -49,11 +53,11 @@ class GlobalMilestone
|
|||
end
|
||||
|
||||
def issues
|
||||
@issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
|
||||
@issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
|
||||
end
|
||||
|
||||
def merge_requests
|
||||
@merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
|
||||
@merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
|
||||
end
|
||||
|
||||
def participants
|
||||
|
|
|
@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
|
||||
|
||||
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
|
||||
|
||||
attr_spammable :title, spam_title: true
|
||||
attr_spammable :description, spam_description: true
|
||||
|
||||
|
|
|
@ -57,6 +57,11 @@ class Member < ActiveRecord::Base
|
|||
scope :owners, -> { active.where(access_level: OWNER) }
|
||||
scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) }
|
||||
|
||||
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
|
||||
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
|
||||
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
|
||||
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
|
||||
|
||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
||||
|
||||
after_create :send_invite, if: :invite?, unless: :importing?
|
||||
|
@ -72,6 +77,34 @@ class Member < ActiveRecord::Base
|
|||
default_value_for :notification_level, NotificationSetting.levels[:global]
|
||||
|
||||
class << self
|
||||
def search(query)
|
||||
joins(:user).merge(User.search(query))
|
||||
end
|
||||
|
||||
def sort(method)
|
||||
case method.to_s
|
||||
when 'access_level_asc' then reorder(access_level: :asc)
|
||||
when 'access_level_desc' then reorder(access_level: :desc)
|
||||
when 'recent_sign_in' then order_recent_sign_in
|
||||
when 'oldest_sign_in' then order_oldest_sign_in
|
||||
when 'last_joined' then order_created_desc
|
||||
when 'oldest_joined' then order_created_asc
|
||||
else
|
||||
order_by(method)
|
||||
end
|
||||
end
|
||||
|
||||
def left_join_users
|
||||
users = User.arel_table
|
||||
members = Member.arel_table
|
||||
|
||||
member_users = members.join(users, Arel::Nodes::OuterJoin).
|
||||
on(members[:user_id].eq(users[:id])).
|
||||
join_sources
|
||||
|
||||
joins(member_users)
|
||||
end
|
||||
|
||||
def access_for_user_ids(user_ids)
|
||||
where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
|
||||
end
|
||||
|
@ -89,8 +122,8 @@ class Member < ActiveRecord::Base
|
|||
member =
|
||||
if user.is_a?(User)
|
||||
source.members.find_by(user_id: user.id) ||
|
||||
source.requesters.find_by(user_id: user.id) ||
|
||||
source.members.build(user_id: user.id)
|
||||
source.requesters.find_by(user_id: user.id) ||
|
||||
source.members.build(user_id: user.id)
|
||||
else
|
||||
source.members.build(invite_email: user)
|
||||
end
|
||||
|
|
|
@ -97,7 +97,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
validates :source_branch, presence: true
|
||||
validates :target_project, presence: true
|
||||
validates :target_branch, presence: true
|
||||
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
|
||||
validates :merge_user, presence: true, if: :merge_when_build_succeeds?, unless: :importing?
|
||||
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
||||
validate :validate_fork, unless: :closed_without_fork?
|
||||
|
||||
|
@ -568,6 +568,19 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def issues_mentioned_but_not_closing(current_user = self.author)
|
||||
return [] unless target_branch == project.default_branch
|
||||
|
||||
ext = Gitlab::ReferenceExtractor.new(project, current_user)
|
||||
ext.analyze(description)
|
||||
|
||||
issues = ext.issues
|
||||
closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
|
||||
closed_by_message(description)
|
||||
|
||||
issues - closing_issues
|
||||
end
|
||||
|
||||
def target_project_path
|
||||
if target_project
|
||||
target_project.path_with_namespace
|
||||
|
@ -612,13 +625,24 @@ class MergeRequest < ActiveRecord::Base
|
|||
self.target_project.repository.branch_names.include?(self.target_branch)
|
||||
end
|
||||
|
||||
def merge_commit_message
|
||||
message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
|
||||
message << "#{title}\n\n"
|
||||
message << "#{description}\n\n" if description.present?
|
||||
def merge_commit_message(include_description: false)
|
||||
closes_issues_references = closes_issues.map do |issue|
|
||||
issue.to_reference(target_project)
|
||||
end
|
||||
|
||||
message = [
|
||||
"Merge branch '#{source_branch}' into '#{target_branch}'",
|
||||
title
|
||||
]
|
||||
|
||||
if !include_description && closes_issues_references.present?
|
||||
message << "Closes #{closes_issues_references.to_sentence}"
|
||||
end
|
||||
|
||||
message << "#{description}" if include_description && description.present?
|
||||
message << "See merge request #{to_reference}"
|
||||
|
||||
message
|
||||
message.join("\n\n")
|
||||
end
|
||||
|
||||
def reset_merge_when_build_succeeds
|
||||
|
|
|
@ -129,6 +129,10 @@ class Milestone < ActiveRecord::Base
|
|||
self.title
|
||||
end
|
||||
|
||||
def milestoneish_ids
|
||||
id
|
||||
end
|
||||
|
||||
def can_be_closed?
|
||||
active? && issues.opened.count.zero?
|
||||
end
|
||||
|
|
|
@ -161,8 +161,8 @@ module Network
|
|||
def is_overlap?(range, overlap_space)
|
||||
range.each do |i|
|
||||
if i != range.first &&
|
||||
i != range.last &&
|
||||
@commits[i].spaces.include?(overlap_space)
|
||||
i != range.last &&
|
||||
@commits[i].spaces.include?(overlap_space)
|
||||
|
||||
return true
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base
|
|||
include TokenAuthenticatable
|
||||
add_authentication_token_field :token
|
||||
|
||||
serialize :scopes, Array
|
||||
|
||||
belongs_to :user
|
||||
|
||||
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
|
||||
|
|
|
@ -79,7 +79,6 @@ class Project < ActiveRecord::Base
|
|||
|
||||
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
|
||||
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
|
||||
has_many :chat_services
|
||||
|
||||
# Project services
|
||||
has_one :campfire_service, dependent: :destroy
|
||||
|
@ -95,6 +94,8 @@ class Project < ActiveRecord::Base
|
|||
has_one :asana_service, dependent: :destroy
|
||||
has_one :gemnasium_service, dependent: :destroy
|
||||
has_one :mattermost_slash_commands_service, dependent: :destroy
|
||||
has_one :mattermost_service, dependent: :destroy
|
||||
has_one :slack_slash_commands_service, dependent: :destroy
|
||||
has_one :slack_service, dependent: :destroy
|
||||
has_one :buildkite_service, dependent: :destroy
|
||||
has_one :bamboo_service, dependent: :destroy
|
||||
|
@ -532,6 +533,10 @@ class Project < ActiveRecord::Base
|
|||
import_type == 'gitlab_project'
|
||||
end
|
||||
|
||||
def gitea_import?
|
||||
import_type == 'gitea'
|
||||
end
|
||||
|
||||
def check_limit
|
||||
unless creator.can_create_project? or namespace.kind == 'group'
|
||||
projects_limit = creator.projects_limit
|
||||
|
@ -1229,6 +1234,12 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def deployment_variables
|
||||
return [] unless deployment_service
|
||||
|
||||
deployment_service.predefined_variables
|
||||
end
|
||||
|
||||
def append_or_update_attribute(name, value)
|
||||
old_values = public_send(name.to_s)
|
||||
|
||||
|
|
|
@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base
|
|||
validates :project, presence: true
|
||||
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
|
||||
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
|
||||
|
||||
def self.insert_authorizations(rows, per_batch = 1000)
|
||||
rows.each_slice(per_batch) do |slice|
|
||||
tuples = slice.map do |tuple|
|
||||
tuple.map { |value| connection.quote(value) }
|
||||
end
|
||||
|
||||
connection.execute <<-EOF.strip_heredoc
|
||||
INSERT INTO project_authorizations (user_id, project_id, access_level)
|
||||
VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
|
||||
EOF
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'slack-notifier'
|
||||
|
||||
class SlackService
|
||||
module ChatMessage
|
||||
class BaseMessage
|
||||
def initialize(params)
|
||||
raise NotImplementedError
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class BuildMessage < BaseMessage
|
||||
attr_reader :sha
|
||||
attr_reader :ref_type
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class IssueMessage < BaseMessage
|
||||
attr_reader :user_name
|
||||
attr_reader :title
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class MergeMessage < BaseMessage
|
||||
attr_reader :user_name
|
||||
attr_reader :project_name
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class NoteMessage < BaseMessage
|
||||
attr_reader :message
|
||||
attr_reader :user_name
|
|
@ -1,4 +1,4 @@
|
|||
class SlackService
|
||||
module ChatMessage
|
||||
class PipelineMessage < BaseMessage
|
||||
attr_reader :ref_type, :ref, :status, :project_name, :project_url,
|
||||
:user_name, :duration, :pipeline_id
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue