Merge branch 'master' into 'badge-color-on-white-bg'

# Conflicts:
#   app/assets/stylesheets/pages/pipelines.scss
This commit is contained in:
Dimitrie Hoekstra 2016-12-21 10:36:27 +00:00
commit 9b4b7da2c9
546 changed files with 15363 additions and 4105 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -1 +1 @@
4.0.3
4.1.1

View file

@ -1 +1 @@
1.2.0
1.2.1

View file

@ -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'

View file

@ -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)

View file

@ -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)

View 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;
})();

View 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;
})();

View file

@ -36,6 +36,9 @@
new gl.BlobCiYamlSelectors({
editor: this.editor
});
new gl.BlobDockerfileSelectors({
editor: this.editor
});
}
EditBlob.prototype.initModePanesAndLinks = function() {

View file

@ -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;

View file

@ -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>

View file

@ -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

View file

@ -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>
`,
});
})();

View file

@ -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();
}
};

View file

@ -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');

View file

@ -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

View file

@ -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]']");

View file

@ -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;
})();

View 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;
})();

View file

@ -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
});
};

View file

@ -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');
}

View 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;
})();

View file

@ -0,0 +1,5 @@
//= require xterm/xterm.js
//= require xterm/fit.js
//= require ./terminal.js
$(() => new gl.Terminal({ selector: '#terminal' }));

View file

@ -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);
};

View file

@ -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() {

View file

@ -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);
};

View file

@ -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; }

View file

@ -230,6 +230,13 @@
}
}
.btn-terminal {
svg {
height: 14px;
width: 18px;
}
}
.btn-lg {
padding: 12px 20px;
}

View file

@ -98,7 +98,7 @@
@extend .dropdown-toggle;
padding-right: 20px;
position: relative;
width: 160px;
width: 163px;
text-overflow: ellipsis;
overflow: hidden;

View file

@ -96,6 +96,10 @@ label {
code {
line-height: 1.8;
}
img {
margin-right: $gl-padding;
}
}
@media(max-width: $screen-xs-max) {

View file

@ -49,3 +49,11 @@
fill: $gray-darkest;
}
}
.ci-status-icon-manual {
color: $gl-text-color;
svg {
fill: $gl-text-color;
}
}

View file

@ -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;
}
}

View file

@ -54,7 +54,7 @@
}
// Display Star and Fork buttons without counters on mobile.
.project-action-buttons {
.project-repo-buttons {
display: block;
.count-buttons .btn {

View file

@ -57,7 +57,6 @@
}
.ci-status-link {
svg {
position: relative;
top: 2px;

View file

@ -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 {

View file

@ -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";

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 {

View file

@ -27,12 +27,6 @@
}
}
.group-buttons {
.notification-dropdown {
display: inline-block;
}
}
.groups-header {
@media (min-width: $screen-sm-min) {
.nav-links {

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -383,10 +383,6 @@ ul.notes {
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) {
position: relative;
}
}
.discussion-actions {

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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?

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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?

View file

@ -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 = {})

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -18,6 +18,10 @@ module Ci
name
end
def statuses_count
@statuses_count ||= statuses.count
end
def status
@status ||= statuses.latest.status
end

View file

@ -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

View 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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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") }

View file

@ -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)

View file

@ -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

View file

@ -1,6 +1,6 @@
require 'slack-notifier'
class SlackService
module ChatMessage
class BaseMessage
def initialize(params)
raise NotImplementedError

View file

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type

View file

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title

View file

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class MergeMessage < BaseMessage
attr_reader :user_name
attr_reader :project_name

View file

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name

View file

@ -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