Commit 54ab4adc authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 19703-direct-link-pipelines

* master: (175 commits)
  Fix typo
  Always use `fixture_file_upload` helper to upload files in tests.
  Add CHANGELOG
  Fix extra spacing in all rgba methods in status file
  Improve spacing and fixes manual status color
  Add `ci-manual` status CSS with darkest gray color
  Move admin application spinach test to rspec
  Move admin deploy keys spinach test to rspec
  Fix CI/CD statuses actions' CSS on pipeline graphs
  Fix rubocop failures
  Store mattermost_url in settings
  Improve Mattermost Session specs
  Ensure the session is destroyed
  Improve session tests
  Setup mattermost session
  Fix link from doc/development/performance.md to 'Performance Monitoring'
  Fix query in Projects::ProjectMembersController to fetch members
  Improve test for sort dropdown on members page
  Fix sort dropdown alignment
  Undo changes on members search button stylesheet
  ...
parents 083e185c 16f950b4
......@@ -15,6 +15,7 @@ variables:
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
before_script:
- source ./scripts/prepare_build.sh
......
......@@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout:
# Checks indentation of binary operations that span more than one line.
Style/MultilineOperationIndentation:
Enabled: false
Enabled: true
EnforcedStyle: indented
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
......
......@@ -22,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'
......
......@@ -432,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)
......@@ -902,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)
......
......@@ -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)
......
/* 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;
})();
(() => {
const global = window.gl || (window.gl = {});
class BlobDockerfileSelectors {
constructor({ editor, $dropdowns } = {}) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new gl.BlobDockerfileSelector({
editor,
pattern: /(Dockerfile)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
global.BlobDockerfileSelectors = BlobDockerfileSelectors;
})();
......@@ -36,6 +36,9 @@
new gl.BlobCiYamlSelectors({
editor: this.editor
});
new gl.BlobDockerfileSelectors({
editor: this.editor
});
}
EditBlob.prototype.initModePanesAndLinks = function() {
......
......@@ -449,7 +449,7 @@
</span>
</td>
<td>
<td class="environments-build-cell">
<a v-if="shouldRenderBuildName"
class="build-link"
:href="model.last_deployment.deployable.build_path">
......
......@@ -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);
}
},
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;
}
},
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);
this.setupLifecycle();
},
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
});
};
if (this.cachedData != null) {
return this.loadData(this.cachedData);
}
}, 1000),
setupAtWho: function() {
},
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(dataSource) {
return $.getJSON(dataSource);
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; });
}
},
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();
}
};
......
......@@ -30,7 +30,7 @@
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
......
......@@ -19,7 +19,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
GitLab.GfmAutoComplete.setup();
gl.GfmAutoComplete.setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
......
/* 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;
})();
......
......@@ -49,3 +49,11 @@
fill: $gray-darkest;
}
}
.ci-status-icon-manual {
color: $gl-text-color;
svg {
fill: $gl-text-color;
}
}
......@@ -32,6 +32,41 @@ body {
}
}
.alert-wrapper {
margin-bottom: $gl-padding;
.alert {
margin-bottom: 0;
}
/* 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;
}
}
/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
......
......@@ -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";
......
......@@ -438,7 +438,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
......@@ -474,7 +474,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
......
......@@ -75,7 +75,8 @@
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
.gitlab-ci-yml-selector,
.dockerfile-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
......@@ -105,7 +106,8 @@
.gitignore-selector,
.license-selector,
.gitlab-ci-yml-selector {
.gitlab-ci-yml-selector,
.dockerfile-selector {
.dropdown {
line-height: 21px;
}
......
......@@ -30,19 +30,25 @@
display: table-cell;
}
.environments-name,
.environments-commit,
.environments-actions {
width: 20%;
}
.environments-deploy,
.environments-build,
.environments-date {
width: 10%;
}
.environments-name {
width: 30%;
.environments-deploy,
.environments-build {
width: 15%;
}
.environment-name,
.environments-build-cell,
.deployment-column {
word-break: break-all;
}
.deployment-column {
......
......@@ -98,7 +98,7 @@
}
.label {
padding: 9px;
padding: 8px 9px 9px;
font-size: 14px;
}
}
......@@ -201,6 +201,8 @@
.label-remove {
border-left: 1px solid $label-remove-border;
z-index: 3;
border-radius: $label-border-radius;
padding: 6px 10px 6px 9px;
}
.btn {
......
......@@ -78,6 +78,21 @@
float: right;
}
.dropdown {
width: 100%;
margin-top: 5px;
.dropdown-menu-toggle {
vertical-align: middle;
width: 100%;
}
@media (min-width: $screen-sm-min) {
margin-top: 0;
width: 155px;
}
}
.form-control {
width: 100%;
padding-right: 35px;
......@@ -85,12 +100,22 @@
@media (min-width: $screen-sm-min) {
width: 350px;
}
&.input-short {
@media (min-width: $screen-md-min) {
width: 170px;
}
@media (min-width: $screen-lg-min) {
width: 210px;
}
}
}
}
.member-search-btn {
position: absolute;
right: 0;
right: 4px;
top: 0;
height: 35px;
padding-left: 10px;
......@@ -99,4 +124,8 @@
background: transparent;
border: 0;
outline: 0;
@media (min-width: $screen-sm-min) {
right: 160px;
}
}
......@@ -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;
}
}
\ No newline at end of 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 {
......
.container-fluid {
.ci-status {
display: inline-block;
padding: 2px 7px;
margin-right: 10px;
border: 1px solid $gray-darker;
......@@ -15,8 +16,7 @@
height: 13px;
width: 13px;
position: relative;
top: 1px;
margin-right: 3px;
top: 2px;
overflow: visible;
}
......@@ -25,7 +25,7 @@
border-color: $gl-danger;
&:not(span):hover {
background-color: rgba( $gl-danger, .07);
background-color: rgba($gl-danger, .07);
}
svg {
......@@ -39,7 +39,7 @@
border-color: $gl-success;
&:not(span):hover {
background-color: rgba( $gl-success, .07);
background-color: rgba($gl-success, .07);
}
svg {
......@@ -52,7 +52,7 @@
border-color: $gl-info;
&:not(span):hover {
background-color: rgba( $gl-info, .07);
background-color: rgba($gl-info, .07);
}
svg {
......@@ -66,7 +66,7 @@
border-color: $gl-gray;
&:not(span):hover {
background-color: rgba( $gl-gray, .07);
background-color: rgba($gl-gray, .07);
}
svg {
......@@ -79,7 +79,7 @@
border-color: $gl-warning;
&:not(span):hover {
background-color: rgba( $gl-warning, .07);
background-color: rgba($gl-warning, .07);
}
svg {
......@@ -92,7 +92,7 @@
border-color: $blue-normal;
&:not(span):hover {
background-color: rgba( $blue-normal, .07);
background-color: rgba($blue-normal, .07);
}
svg {
......@@ -106,13 +106,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;
}
}
}
}
......
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
......@@ -262,7 +262,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?
......
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
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include SortingHelper
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@members = @members.where(user_id: users)
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
......
......@@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
before_action :verify_bitbucket_import_enabled
before_action :bitbucket_auth, except: :callback
rescue_from OAuth::Error, with: :bitbucket_unauthorized
rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
rescue_from OAuth2::Error, with: :bitbucket_unauthorized
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
request_token = session.delete(:oauth_request_token)
raise "Session expired!" if request_token.nil?
response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
request_token.symbolize_keys!
access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
session[:bitbucket_access_token] = access_token.token
session[:bitbucket_access_token_secret] = access_token.secret
session[:bitbucket_token] = response.token
session[:bitbucket_expires_at] = response.expires_at
session[:bitbucket_expires_in] = response.expires_in
session[:bitbucket_refresh_token] = response.refresh_token
redirect_to status_import_bitbucket_url
end
def status
@repos = client.projects
@incompatible_repos = client.incompatible_projects
bitbucket_client = Bitbucket::Client.new(credentials)
repos = bitbucket_client.repos
@already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
@already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
render json: jobs
render json: current_user.created_projects
.where(import_type: 'bitbucket')
.to_json(only: [:id, :import_status])
end
def create
bitbucket_client = Bitbucket::Client.new(credentials)
@repo_id = params[:repo_id].to_s
repo = client.project(@repo_id.gsub('___', '/'))
@project_name = repo['slug']
@target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
name = @repo_id.gsub('___', '/')
repo = bitbucket_client.repo(name)
@project_name = params[:new_name].presence || repo.name
unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
render 'deploy_key' and return
end
repo_owner = repo.owner
repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
@target_namespace = params[:new_namespace].presence || repo_owner
namespace = find_or_create_namespace(@target_namespace, current_user)
if current_user.can?(:create_projects, @target_namespace)
@project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
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
......@@ -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'
......
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
......
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
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
include SortingHelper
# Authorize
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
......@@ -35,12 +37,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
order(access_level: :desc).page(params[:page])
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
......
......@@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
redirect_to edit_project_path(@project), alert: ex.message
end
def autocomplete_sources
noteable =
case params[:type]
when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
else
nil
end
autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
@suggestions = {
emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
members: participants,
commands: autocomplete.commands(noteable, params[:type])
}
respond_to do |format|
format.json { render json: @suggestions }
end
end
def new_issue_address
return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
......
......@@ -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,
......
......@@ -36,4 +36,12 @@ module MembersHelper
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
def filter_group_project_member_path(options = {})
options = params.slice(:search, :sort).merge(options)
path = request.path
path << "?#{options.to_param}"
path
end
end
......@@ -59,6 +59,10 @@ module MergeRequestsHelper
@mr_closes_issues ||= @merge_request.closes_issues
end
def mr_issues_mentioned_but_not_closing
@mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
end
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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") }
......
......@@ -1230,6 +1230,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)
......
......@@ -8,4 +8,8 @@ class DeploymentService < Service
def supported_events
[]
end
def predefined_variables
[]
end
end
......@@ -83,6 +83,16 @@ class KubernetesService < DeploymentService
{ success: false, result: err }
end
def predefined_variables
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true }
]
variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
variables
end
private
def build_kubeclient(api_path = '/api', api_version = 'v1')
......
......@@ -178,6 +178,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
......@@ -205,8 +207,8 @@ class User < ActiveRecord::Base
def sort(method)
case method.to_s
when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
else
order_by(method)
end
......
AccessTokenValidationService = Struct.new(:token) do
# Results:
VALID = :valid
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
def validate(scopes: [])
if token.expired?
return EXPIRED
elsif token.revoked?
return REVOKED
elsif !self.include_any_scope?(scopes)
return INSUFFICIENT_SCOPE
else
return VALID
end
end
# True if the token's scope contains any of the passed scopes.
def include_any_scope?(scopes)
if scopes.blank?
true
else
# Check whether the token is allowed access to any of the required scopes.
Set.new(scopes).intersection(Set.new(token.scopes)).present?
end
end
end
module Oauth2::AccessTokenValidationService
# Results:
VALID = :valid
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
class << self
def validate(token, scopes: [])
if token.expired?
return EXPIRED
elsif token.revoked?
return REVOKED
elsif !self.sufficient_scope?(token, scopes)
return INSUFFICIENT_SCOPE
else
return VALID
end
end
protected
# True if the token's scope is a superset of required scopes,
# or the required scopes is empty.
def sufficient_scope?(token, scopes)
if scopes.blank?
# if no any scopes required, the scopes of token is sufficient.
return true
else
# If there are scopes required, then check whether
# the set of authorized scopes is a superset of the set of required scopes
required_scopes = Set.new(scopes)
authorized_scopes = Set.new(token.scopes)
return authorized_scopes >= required_scopes
end
end
end
end
......@@ -18,6 +18,12 @@
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
.form-group
= f.label :scopes, class: 'col-sm-2 control-label'
.col-sm-10
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
.form-actions
= f.submit 'Submit', class: "btn btn-save wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-default"
......@@ -2,8 +2,7 @@
%h3.page-title
Application: #{@application.name}
.table-holder
.table-holder.oauth-application-show
%table.table
%tr
%td
......@@ -23,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
= render "shared/tokens/scopes_list", token: @application
.form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
- status = local_assigns.fetch(:status)
- css_classes = "ci-status ci-#{status.group}"
- if status.has_details?
= link_to status.details_path, class: "ci-status ci-#{status}" do
= link_to status.details_path, class: css_classes do
= custom_icon(status.icon)
= status.text
- else
%span{ class: "ci-status ci-#{status}" }
%span{ class: css_classes }
= custom_icon(status.icon)
= status.text
......@@ -2,7 +2,7 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status}"
- klass = "ci-status-icon ci-status-icon-#{status.group}"
- if status.has_details?
= link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do
......
......@@ -17,5 +17,9 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
.form-group
= f.label :scopes, class: 'label-light'
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
.prepend-top-default
= f.submit 'Save application', class: "btn btn-create"
......@@ -2,7 +2,7 @@
%h3.page-title
Application: #{@application.name}
.table-holder
.table-holder.oauth-application-show
%table.table
%tr
%td
......@@ -22,6 +22,9 @@
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
= render "shared/tokens/scopes_list", token: @application
.form-actions
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
......@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
= render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
Users with access to
......
- page_title "Bitbucket import"
- header_title "Projects", root_path
- page_title 'Bitbucket import'
- header_title 'Projects', root_path
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
......@@ -10,13 +11,13 @@
%hr
%p
- if @incompatible_repos.any?
= button_tag class: "btn btn-import btn-success js-import-all" do
= button_tag class: 'btn btn-import btn-success js-import-all' do
Import all compatible projects
= icon("spinner spin", class: "loading-icon")
= icon('spinner spin', class: 'loading-icon')
- else
= button_tag class: "btn btn-success js-import-all" do
= button_tag class: 'btn btn-import btn-success js-import-all' do
Import all projects
= icon("spinner spin", class: "loading-icon")
= icon('spinner spin', class: 'loading-icon')
.table-responsive
%table.table.import-jobs
......@@ -32,7 +33,7 @@
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
= link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
= link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
......@@ -47,31 +48,41 @@
= project.human_import_status_name
- @repos.each do |repo|
%tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
%tr{id: "repo_#{repo.owner}___#{repo.slug}"}
%td
= link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
= link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
%td.import-target
= import_project_target(repo['owner'], repo['slug'])
%fieldset.row
.input-group
.project-path.input-group-btn
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
%span.input-group-addon /
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
= button_tag class: 'btn btn-import js-add-to-import' do
Import
= icon("spinner spin", class: "loading-icon")
= icon('spinner spin', class: 'loading-icon')
- @incompatible_repos.each do |repo|
%tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
%tr{id: "repo_#{repo.owner}___#{repo.slug}"}
%td
= link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
= link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
%td.import-target
%td.import-actions-job-status
= label_tag "Incompatible Project", nil, class: "label label-danger"
= label_tag 'Incompatible Project', nil, class: 'label label-danger'
- if @incompatible_repos.any?
%p
One or more of your Bitbucket projects cannot be imported into GitLab
directly because they use Subversion or Mercurial for version control,
rather than Git. Please convert
= link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview"
= link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
= link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true"
= link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
......@@ -3,6 +3,14 @@
- if project
:javascript
GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
GitLab.GfmAutoComplete.cachedData = undefined;
GitLab.GfmAutoComplete.setup();
gl.GfmAutoComplete.dataSources = {
emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
};
gl.GfmAutoComplete.setup();
......@@ -22,6 +22,7 @@
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
.alert-wrapper
= render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
......
- personal_access_token = local_assigns.fetch(:personal_access_token)
- scopes = local_assigns.fetch(:scopes)
= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
= form_errors(personal_access_token)
.form-group
= f.label :name, class: 'label-light'
= f.text_field :name, class: "form-control", required: true
.form-group
= f.label :expires_at, class: 'label-light'
= f.text_field :expires_at, class: "datepicker form-control"
.form-group
= f.label :scopes, class: 'label-light'
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
.prepend-top-default
= f.submit 'Create Personal Access Token', class: "btn btn-create"
......@@ -28,21 +28,8 @@
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
= form_for [:profile, @personal_access_token],
method: :post, html: { class: 'js-requires-input' } do |f|
= form_errors(@personal_access_token)
.form-group
= f.label :name, class: 'label-light'
= f.text_field :name, class: "form-control", required: true
.form-group
= f.label :expires_at, class: 'label-light'
= f.text_field :expires_at, class: "datepicker form-control", required: false
.prepend-top-default
= f.submit 'Create Personal Access Token', class: "btn btn-create"
= render "form", personal_access_token: @personal_access_token, scopes: @scopes
%hr
......@@ -56,6 +43,7 @@
%th Name
%th Created
%th Expires
%th Scopes
%th
%tbody
- @active_personal_access_tokens.each do |token|
......@@ -67,6 +55,7 @@
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
......
......@@ -21,6 +21,8 @@
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.hidden
= dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
%span.no-wrap
= custom_icon('icon_no_wrap')
......
......@@ -30,11 +30,18 @@
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- if mr_closes_issues.present?
- if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present?
.mr-widget-footer
%span
%i.fa.fa-check
= icon('check')
- if mr_closes_issues.present?
Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
= succeed '.' do
!= markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
= mr_assign_issues_link
- if mr_issues_mentioned_but_not_closing.present?
#{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
!= markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
#{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed.
......@@ -41,6 +41,8 @@
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
message_with_description: @merge_request.merge_commit_message(include_description: true),
message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
......
......@@ -21,6 +21,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
= render 'shared/members/sort_dropdown'
- if @group_links.any?
= render 'groups', group_links: @group_links
......
- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}"
- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}"
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
.well
This service allows GitLab users to perform common operations on this
......@@ -27,7 +26,7 @@
.form-group
= label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
= text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#display_name')
......
.form-group.commit_message-group
- nonce = SecureRandom.hex
- descriptions = local_assigns.slice(:message_with_description, :message_without_description)
= label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message
.col-sm-10
......@@ -8,9 +9,17 @@
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
data: descriptions,
required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
Try to keep the first line under 52 characters
and the others under 72.
- if descriptions.present?
%p.hint.js-with-description-hint
= link_to "#", class: "js-with-description-link" do
Include description in commit message
%p.hint.js-without-description-hint.hide
= link_to "#", class: "js-without-description-link" do
Don't include description in commit message
.dropdown.inline.member-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- member_sort_options_hash.each do |value, title|
%li
= link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do
= title
- scopes = local_assigns.fetch(:scopes)
- prefix = local_assigns.fetch(:prefix)
- token = local_assigns.fetch(:token)
- scopes.each do |scope|
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag "#{prefix}_scopes_#{scope}", scope
%span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
- token = local_assigns.fetch(:token)
- return unless token.scopes.present?
%tr
%td
Scopes
%td
%ul.scopes-list.append-bottom-0
- token.scopes.each do |scope|
%li
%span.scope-name= scope
= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
---
title: Made comment autocomplete more performant and removed some loading bugs
merge_request: 6856
author:
---
title: Add scopes for personal access tokens and OAuth tokens
merge_request: 5951
author:
---
title: Add sorting functionality for group/project members
merge_request: 7032
author:
---
title: Prevent enviroment table to overflow when name has underscores
merge_request: 8142
author:
---
title: Accept environment variables from the `pre-receive` script
merge_request: 7967
author:
---
title: fix colors and margins for adjacent alert banners
merge_request: 8151
author:
---
title: Refactor Bitbucket importer to use BitBucket API Version 2
merge_request:
author:
---
title: Add support for Dockerfile templates
merge_request: 7247
author:
---
title: Pass variables from deployment project services to CI runner
merge_request: 8107
author:
---
title: Additional rounded label fixes
merge_request:
author:
......@@ -153,6 +153,12 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
## Mattermost
## For enabling Add to Mattermost button
mattermost:
enabled: false
host: 'https://mattermost.example.com'
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
......
......@@ -261,6 +261,13 @@ Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
#
# Mattermost
#
Settings['mattermost'] ||= Settingslogic.new({})
Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil?
Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
#
# Gravatar
#
......
......@@ -52,8 +52,8 @@ Doorkeeper.configure do
# Define access token scopes for your provider
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
default_scopes :api
# optional_scopes :write, :update
default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
......
......@@ -26,3 +26,9 @@ if Gitlab.config.omniauth.enabled
end
end
end
module OmniAuth
module Strategies
autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
end
end
path = File.expand_path("~/.ssh/bitbucket_rsa.pub")
Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
......@@ -59,6 +59,7 @@ en:
unknown: "The access token is invalid"
scopes:
api: Access your API
read_user: Read user information
flash:
applications:
......
......@@ -11,6 +11,18 @@ constraints(ProjectUrlConstrainer.new) do
module: :projects,
as: :project) do
resources :autocomplete_sources, only: [] do
collection do
get 'emojis'
get 'members'
get 'issues'
get 'merge_requests'
get 'labels'
get 'milestones'
get 'commands'
end
end
#
# Templates
#
......@@ -317,7 +329,6 @@ constraints(ProjectUrlConstrainer.new) do
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources
get :activity
get :refs
put :new_issue_address
......
class Gitlab::Seeder::Pipelines
STAGES = %w[build test deploy notify]
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success },
{ name: 'rspec:linux 0 3', stage: 'test', status: :success },
{ name: 'rspec:linux 1 3', stage: 'test', status: :success },
{ name: 'rspec:linux 2 3', stage: 'test', status: :success },
{ name: 'rspec:windows 0 3', stage: 'test', status: :success },
{ name: 'rspec:windows 1 3', stage: 'test', status: :success },
{ name: 'rspec:windows 2 3', stage: 'test', status: :success },
{ name: 'rspec:windows 2 3', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
{ name: 'spinach:linux', stage: 'test', status: :success },
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
{ name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
{ name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
{ name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
{ name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } },
{ name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
# build stage
{ name: 'build:linux', stage: 'build', status: :success,
queued_at: 10.hour.ago, started_at: 9.hour.ago, finished_at: 8.hour.ago },
{ name: 'build:osx', stage: 'build', status: :success,
queued_at: 10.hour.ago, started_at: 10.hour.ago, finished_at: 9.hour.ago },
# test stage
{ name: 'rspec:linux 0 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:linux 1 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:linux 2 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:windows 0 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:windows 1 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:windows 2 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:windows 2 3', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'rspec:osx', stage: 'test', status_event: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'spinach:linux', stage: 'test', status: :success,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
# deploy stage
{ name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success,
options: { environment: { action: 'start', on_stop: 'stop staging' } },
queued_at: 7.hour.ago, started_at: 6.hour.ago, finished_at: 4.hour.ago },
{ name: 'stop staging', stage: 'deploy', environment: 'staging',
when: 'manual', status: :skipped },
{ name: 'production', stage: 'deploy', environment: 'production',
when: 'manual', status: :skipped },
# notify stage
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
EXTERNAL_JOBS = [
{ name: 'jenkins', stage: 'test', status: :success,
queued_at: 7.hour.ago, started_at: 6.hour.ago, finished_at: 4.hour.ago },
]
def initialize(project)
@project = project
......@@ -30,11 +54,12 @@ class Gitlab::Seeder::Pipelines
pipelines.each do |pipeline|
begin
BUILDS.each { |opts| build_create!(pipeline, opts) }
commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success)
EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) }
print '.'
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
pipeline.update_duration
pipeline.update_status
end
end
......
# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to
# `[]`.
class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml
end
def down
remove_column :personal_access_tokens, :scopes
end
end
# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
# It's easier to achieve this by adding the column with the `['api']` default (regular migration), and
# then changing the default to `[]` (in this post-migration).
#
# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951#note_19721973
class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
change_column_default :personal_access_tokens, :scopes, [].to_yaml
end
def down
change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml
end
end
......@@ -854,6 +854,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do
t.datetime "expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "scopes", default: "--- []\n", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
......
......@@ -8,7 +8,7 @@
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
......
......@@ -13,6 +13,7 @@ this order:
1. [Secret variables](#secret-variables)
1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
1. YAML-defined [global variables](../yaml/README.md#variables)
1. [Deployment variables](#deployment-variables)
1. [Predefined variables](#predefined-variables-environment-variables) (are the
lowest in the chain)
......@@ -60,6 +61,9 @@ version of Runner required.
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a build |
| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a build |
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a build |
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
......@@ -148,6 +152,20 @@ Secret variables can be added by going to your project's
Once you set them, they will be available for all subsequent builds.
## Deployment variables
>**Note:**
This feature requires GitLab CI 8.15 or higher.
[Project services](../../project_services/project_services.md) that are
responsible for deployment configuration may define their own variables that
are set in the build environment. These variables are only defined for
[deployment builds](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is
[Kubernetes Service](../../project_services/kubernetes.md).
## Debug tracing
> Introduced in GitLab Runner 1.7.
......
......@@ -1034,6 +1034,31 @@ variables:
GIT_STRATEGY: none
```
## Build stages attempts
> Introduced in GitLab, it requires GitLab Runner v1.9+.
You can set the number for attempts the running build will try to execute each
of the following stages:
| Variable | Description |
|-------------------------|-------------|
| **GET_SOURCES_ATTEMPTS** | Number of attempts to fetch sources running a build |
| **ARTIFACT_DOWNLOAD_ATTEMPTS** | Number of attempts to download artifacts running a build |
| **RESTORE_CACHE_ATTEMPTS** | Number of attempts to restore the cache running a build |
The default is one single attempt.
Example:
```
variables:
GET_SOURCES_ATTEMPTS: "3"
```
You can set them in the global [`variables`](#variables) section or the [`variables`](#job-variables)
section for individual jobs.
## Shallow cloning
> Introduced in GitLab 8.9 as an experimental feature. May change in future
......
......@@ -37,7 +37,7 @@ graphs/dashboards.
GitLab provides built-in tools to aid the process of improving performance:
* [Sherlock](profiling.md#sherlock)
* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md)
* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
GitLab employees can use GitLab.com's performance monitoring systems located at
......
......@@ -18,8 +18,10 @@ Bitbucket.org.
## Bitbucket OmniAuth provider
> **Note:**
Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
before proceeding with setting up the Bitbucket integration.
GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with
GitLab. You are encouraged to upgrade your GitLab instance if you haven't done
already. If you're using GitLab 8.14 and below, [use the previous integration
docs][bb-old].
To enable the Bitbucket OmniAuth provider you must register your application
with Bitbucket.org. Bitbucket will generate an application ID and secret key for
......@@ -44,14 +46,12 @@ you to use.
And grant at least the following permissions:
```
Account: Email
Repositories: Read, Admin
Account: Email, Read
Repositories: Read
Pull Requests: Read
Issues: Read
```
>**Note:**
It may seem a little odd to giving GitLab admin permissions to repositories,
but this is needed in order for GitLab to be able to clone the repositories.
![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
1. Select **Save**.
......@@ -93,7 +93,8 @@ you to use.
```yaml
- { name: 'bitbucket',
app_id: 'BITBUCKET_APP_KEY',
app_secret: 'BITBUCKET_APP_SECRET' }
app_secret: 'BITBUCKET_APP_SECRET',
url: 'https://bitbucket.org/' }
```
---
......@@ -112,100 +113,12 @@ well, the user will be returned to GitLab and will be signed in.
## Bitbucket project import
To allow projects to be imported directly into GitLab, Bitbucket requires two
extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
instead requires GitLab to use SSH and identify itself using your GitLab
server's SSH key.
To be able to access repositories on Bitbucket, GitLab will automatically
register your public key with Bitbucket as a deploy key for the repositories to
be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
`/home/git/.ssh/bitbucket_rsa` for installations from source.
---
Below are the steps that will allow GitLab to be able to import your projects
from Bitbucket.
1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
1. Create a new SSH key with an **empty passphrase**:
```sh
sudo -u git -H ssh-keygen
```
When asked to 'Enter file in which to save the key' enter:
`/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
`/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
important so make sure to get it right.
> **Warning:**
This key must NOT be associated with ANY existing Bitbucket accounts. If it
is, the import will fail with an `Access denied! Please verify you can add
deploy keys to this repository.` error.
1. Next, you need to to configure the SSH client to use your new key. Open the
SSH configuration file of the `git` user:
```
# For Omnibus packages
sudo editor /var/opt/gitlab/.ssh/config
# For installations from source
sudo editor /home/git/.ssh/config
```
1. Add a host configuration for `bitbucket.org`:
```sh
Host bitbucket.org
IdentityFile ~/.ssh/bitbucket_rsa
User git
```
1. Save the file and exit.
1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
user that GitLab will use:
```sh
sudo -u git -H ssh bitbucket.org
```
That step is performed because GitLab needs to connect to Bitbucket over SSH,
in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
1. Verify the RSA key fingerprint you'll see in the response matches the one
in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
doesn't matter):
```sh
The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
Are you sure you want to continue connecting (yes/no)?
```
1. If the fingerprint matches, type `yes` to continue connecting and have
`bitbucket.org` be added to your known SSH hosts. After confirming you should
see a permission denied message. If you see an authentication successful
message you have done something wrong. The key you are using has already been
added to a Bitbucket account and will cause the import script to fail. Ensure
the key you are using CANNOT authenticate with Bitbucket.
1. Restart GitLab to allow it to find the new public key.
Your GitLab server is now able to connect to Bitbucket over SSH. You should be
able to see the "Import projects from Bitbucket" option on the New Project page
enabled.
## Acknowledgements
Special thanks to the writer behind the following article:
- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
Once the above configuration is set up, you can use Bitbucket to sign into
GitLab and [start importing your projects][bb-import].
[init-oauth]: omniauth.md#initial-omniauth-configuration
[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
......@@ -36,3 +36,14 @@ to create one. You can also view or create service tokens in the
Fill in the service token and namespace according to the values you just got.
If the API is using a self-signed TLS certificate, you'll also need to include
the `ca.crt` contents as the `Custom CA bundle`.
## Deployment variables
The Kubernetes service exposes following
[deployment variables](../ci/variables/README.md#deployment-variables) in the
GitLab CI build environment:
- `KUBE_URL` - equal to the API URL
- `KUBE_TOKEN`
- `KUBE_NAMESPACE`
- `KUBE_CA_PEM` - only if a custom CA bundle was specified
# Import your project from Bitbucket to GitLab
It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
Import your projects from Bitbucket to GitLab with minimal effort.
* Sign in to GitLab.com and go to your dashboard
## Overview
* Click on "New project"
>**Note:**
The [Bitbucket integration][bb-import] must be first enabled in order to be
able to import your projects from Bitbucket. Ask your GitLab administrator
to enable this if not already.
![New project in GitLab](bitbucket_importer/bitbucket_import_new_project.png)
- At its current state, the Bitbucket importer can import:
- the repository description (GitLab 7.7+)
- the Git repository data (GitLab 7.7+)
- the issues (GitLab 7.7+)
- the issue comments (GitLab 8.15+)
- the pull requests (GitLab 8.4+)
- the pull request comments (GitLab 8.15+)
- the milestones (GitLab 8.15+)
- References to pull requests and issues are preserved (GitLab 8.7+)
- Repository public access is retained. If a repository is private in Bitbucket
it will be created as private in GitLab as well.
* Click on the "Bitbucket" button
![Bitbucket](bitbucket_importer/bitbucket_import_select_bitbucket.png)
## How it works
* Grant GitLab access to your Bitbucket account
When issues/pull requests are being imported, the Bitbucket importer tries to find
the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
and [**associated their Bitbucket account**][social sign-in]. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original Bitbucket author is kept.
![Grant access](bitbucket_importer/bitbucket_import_grant_access.png)
The importer will create any new namespaces (groups) if they don't exist or in
the case the namespace is taken, the repository will be imported under the user's
namespace that started the import process.
* Click on the projects that you'd like to import or "Import all projects"
## Importing your Bitbucket repositories
![Import projects](bitbucket_importer/bitbucket_import_select_project.png)
1. Sign in to GitLab and go to your dashboard.
1. Click on **New project**.
A new GitLab project will be created with your imported data.
![New project in GitLab](img/bitbucket_import_new_project.png)
### Note
Milestones and wiki pages are not imported from Bitbucket.
1. Click on the "Bitbucket" button
![Bitbucket](img/import_projects_from_new_project_page.png)
1. Grant GitLab access to your Bitbucket account
![Grant access](img/bitbucket_import_grant_access.png)
1. Click on the projects that you'd like to import or **Import all projects**.
You can also select the namespace under which each project will be
imported.
![Import projects](img/bitbucket_import_select_project.png)
[bb-import]: ../../integration/bitbucket.md
[social sign-in]: ../../user/profile/account/social_sign_in.md
......@@ -40,7 +40,7 @@ namespace that started the import process.
The importer page is visible when you create a new project.
![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
![New project page on GitLab](img/import_projects_from_new_project_page.png)
Click on the **GitHub** link and the import authorization process will start.
There are two ways to authorize access to your GitHub repositories:
......
@admin
Feature: Admin Applications
Background:
Given I sign in as an admin
And I visit applications page
Scenario: I can manage application
Then I click on new application button
And I should see application form
Then I fill application form out and submit
And I see application
Then I click edit
And I see edit application form
Then I change name of application and submit
And I see that application was changed
Then I visit applications page
And I click to remove application
Then I see that application is removed
\ No newline at end of file
@admin
Feature: Admin Deploy Keys
Background:
Given I sign in as an admin
And there are public deploy keys in system
Scenario: Deploy Keys list
When I visit admin deploy keys page
Then I should see all public deploy keys
Scenario: Deploy Keys new
When I visit admin deploy keys page
And I click 'New Deploy Key'
And I submit new deploy key
Then I should be on admin deploy keys page
And I should see newly created deploy key
class Spinach::Features::AdminApplications < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedAdmin
step 'I click on new application button' do
click_on 'New Application'
end
step 'I should see application form' do
expect(page).to have_content "New application"
end
step 'I fill application form out and submit' do
fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
click_on "Submit"
end
step 'I see application' do
expect(page).to have_content "Application: test"
expect(page).to have_content "Application Id"
expect(page).to have_content "Secret"
end
step 'I click edit' do
click_on "Edit"
end
step 'I see edit application form' do
expect(page).to have_content "Edit application"
end
step 'I change name of application and submit' do
expect(page).to have_content "Edit application"
fill_in :doorkeeper_application_name, with: 'test_changed'
click_on "Submit"
end
step 'I see that application was changed' do
expect(page).to have_content "test_changed"
expect(page).to have_content "Application Id"
expect(page).to have_content "Secret"
end
step 'I click to remove application' do
page.within '.oauth-applications' do
click_on "Destroy"
end
end
step "I see that application is removed" do
expect(page.find(".oauth-applications")).not_to have_content "test_changed"
end
end
......@@ -207,10 +207,6 @@ module SharedPaths
visit admin_spam_logs_path
end
step 'I visit applications page' do
visit admin_applications_path
end
# ----------------------------------------
# Generic Project
# ----------------------------------------
......
......@@ -3,6 +3,8 @@ module API
include APIGuard
version 'v3', using: :path
before { allow_access_with_scope :api }
rescue_from Gitlab::Access::AccessDeniedError do
rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
end
......
......@@ -6,6 +6,9 @@ module API
module APIGuard
extend ActiveSupport::Concern
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
......@@ -44,27 +47,60 @@ module API
access_token = find_access_token
return nil unless access_token
case validate_access_token(access_token, scopes)
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
case AccessTokenValidationService.new(access_token).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when Oauth2::AccessTokenValidationService::EXPIRED
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when Oauth2::AccessTokenValidationService::REVOKED
when AccessTokenValidationService::REVOKED
raise RevokedError
when Oauth2::AccessTokenValidationService::VALID
when AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id)
end
end
def find_user_by_private_token(scopes: [])
token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
return nil unless token_string.present?
find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes)
end
def current_user
@current_user
end
# Set the authorization scope(s) allowed for the current request.
#
# Note: A call to this method adds to any previous scopes in place. This is done because
# `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then
# the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the
# given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they
# need to be stored.
def allow_access_with_scope(*scopes)
@scopes ||= []
@scopes.concat(scopes.map(&:to_s))
end
private
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end
def find_user_by_personal_access_token(token_string, scopes)
access_token = PersonalAccessToken.active.find_by_token(token_string)
return unless access_token
if AccessTokenValidationService.new(access_token).include_any_scope?(scopes)
User.find(access_token.user_id)
end
end
def find_access_token
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
end
......@@ -72,10 +108,6 @@ module API
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
def validate_access_token(access_token, scopes)
Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
end
end
module ClassMethods
......
......@@ -2,8 +2,6 @@ module API
module Helpers
include Gitlab::Utils
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
......@@ -308,7 +306,7 @@ module API
private
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
end
def warden
......@@ -323,18 +321,11 @@ module API
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
token = private_token
return nil unless token.present?
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
@initial_current_user ||= find_user_by_private_token
@initial_current_user ||= doorkeeper_guard
@initial_current_user ||= find_user_by_private_token(scopes: @scopes)
@initial_current_user ||= doorkeeper_guard(scopes: @scopes)
@initial_current_user ||= find_user_from_warden
unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
......
......@@ -52,6 +52,14 @@ module API
:push_code
]
end
def parse_allowed_environment_variables
return if params[:env].blank?
JSON.parse(params[:env])
rescue JSON::ParserError
end
end
end
end
......@@ -32,7 +32,11 @@ module API
if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
else
Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
Gitlab::GitAccess.new(actor,
project,
protocol,
authentication_abilities: ssh_authentication_abilities,
env: parse_allowed_environment_variables)
end
access_status = access.check(params[:action], params[:changes])
......
......@@ -8,6 +8,10 @@ module API
gitlab_ci_ymls: {
klass: Gitlab::Template::GitlabCiYmlTemplate,
gitlab_version: 8.9
},
dockerfiles: {
klass: Gitlab::Template::DockerfileTemplate,
gitlab_version: 8.15
}
}.freeze
PROJECT_TEMPLATE_REGEX =
......
......@@ -2,7 +2,10 @@ module API
class Users < Grape::API
include PaginationParams
before { authenticate! }
before do
allow_access_with_scope :read_user if request.get?
authenticate!
end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
helpers do
......
module Bitbucket
class Client
attr_reader :connection
def initialize(options = {})
@connection = Connection.new(options)
end
def issues(repo)
path = "/repositories/#{repo}/issues"
get_collection(path, :issue)
end
def issue_comments(repo, issue_id)
path = "/repositories/#{repo}/issues/#{issue_id}/comments"
get_collection(path, :comment)
end
def pull_requests(repo)
path = "/repositories/#{repo}/pullrequests?state=ALL"
get_collection(path, :pull_request)
end
def pull_request_comments(repo, pull_request)
path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments"
get_collection(path, :pull_request_comment)
end
def pull_request_diff(repo, pull_request)
path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff"
connection.get(path)
end
def repo(name)
parsed_response = connection.get("/repositories/#{name}")
Representation::Repo.new(parsed_response)
end
def repos
path = "/repositories?role=member"
get_collection(path, :repo)
end
def user
@user ||= begin
parsed_response = connection.get('/user')
Representation::User.new(parsed_response)
end
end
private
def get_collection(path, type)
paginator = Paginator.new(connection, path, type)
Collection.new(paginator)
end
end
end
module Bitbucket
class Collection < Enumerator
def initialize(paginator)
super() do |yielder|
loop do
paginator.items.each { |item| yielder << item }
end
end
lazy
end
def method_missing(method, *args)
return super unless self.respond_to?(method)
self.send(method, *args) do |item|
block_given? ? yield(item) : item
end
end
end
end
module Bitbucket
class Connection
DEFAULT_API_VERSION = '2.0'
DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
DEFAULT_QUERY = {}
attr_reader :expires_at, :expires_in, :refresh_token, :token
def initialize(options = {})
@api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
@base_uri = options.fetch(:base_uri, DEFAULT_BASE_URI)
@default_query = options.fetch(:query, DEFAULT_QUERY)
@token = options[:token]
@expires_at = options[:expires_at]
@expires_in = options[:expires_in]
@refresh_token = options[:refresh_token]
end
def get(path, extra_query = {})
refresh! if expired?
response = connection.get(build_url(path), params: @default_query.merge(extra_query))
response.parsed
end
def expired?
connection.expired?
end
def refresh!
response = connection.refresh!
@token = response.token
@expires_at = response.expires_at
@expires_in = response.expires_in
@refresh_token = response.refresh_token
@connection = nil
end
private
def client
@client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
end
def connection
@connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in)
end
def build_url(path)
return path if path.starts_with?(root_url)
"#{root_url}#{path}"
end
def root_url
@root_url ||= "#{@base_uri}#{@api_version}"
end
def provider
Gitlab::OAuth::Provider.config_for('bitbucket')
end
def options
OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
end
end
end
module Bitbucket
module Error
class Unauthorized < StandardError
end
end
end
module Bitbucket
class Page
attr_reader :attrs, :items
def initialize(raw, type)
@attrs = parse_attrs(raw)
@items = parse_values(raw, representation_class(type))
end
def next?
attrs.fetch(:next, false)
end
def next
attrs.fetch(:next)
end
private
def parse_attrs(raw)
raw.slice(*%w(size page pagelen next previous)).symbolize_keys
end
def parse_values(raw, bitbucket_rep_class)
return [] unless raw['values'] && raw['values'].is_a?(Array)
bitbucket_rep_class.decorate(raw['values'])
end
def representation_class(type)
Bitbucket::Representation.const_get(type.to_s.camelize)
end
end
end
module Bitbucket
class Paginator
PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100.
def initialize(connection, url, type)
@connection = connection
@type = type
@url = url
@page = nil
end
def items
raise StopIteration unless has_next_page?
@page = fetch_next_page
@page.items
end
private
attr_reader :connection, :page, :url, :type
def has_next_page?
page.nil? || page.next?
end
def next_url
page.nil? ? url : page.next
end
def fetch_next_page
parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on)
Page.new(parsed_response, type)
end
end
end
module Bitbucket
module Representation
class Base
def initialize(raw)
@raw = raw
end
def self.decorate(entries)
entries.map { |entry| new(entry)}
end
private
attr_reader :raw
end
end
end
module Bitbucket
module Representation
class Comment < Representation::Base
def author
user['username']
end
def note
raw.fetch('content', {}).fetch('raw', nil)
end
def created_at
raw['created_on']
end
def updated_at
raw['updated_on'] || raw['created_on']
end
private
def user
raw.fetch('user', {})
end
end
end
end
module Bitbucket
module Representation
class Issue < Representation::Base
CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze
def iid
raw['id']
end
def kind
raw['kind']
end
def author
raw.fetch('reporter', {}).fetch('username', nil)
end
def description
raw.fetch('content', {}).fetch('raw', nil)
end
def state
closed? ? 'closed' : 'opened'
end
def title
raw['title']
end
def milestone
raw['milestone']['name'] if raw['milestone'].present?
end
def created_at
raw['created_on']
end
def updated_at
raw['edited_on']
end
def to_s
iid
end
private
def closed?
CLOSED_STATUS.include?(raw['state'])
end
end
end
end
module Bitbucket
module Representation
class PullRequest < Representation::Base
def author
raw.fetch('author', {}).fetch('username', nil)
end
def description
raw['description']
end
def iid
raw['id']
end
def state
if raw['state'] == 'MERGED'
'merged'
elsif raw['state'] == 'DECLINED'
'closed'
else
'opened'
end
end
def created_at
raw['created_on']
end
def updated_at
raw['updated_on']
end
def title
raw['title']
end
def source_branch_name
source_branch.fetch('branch', {}).fetch('name', nil)
end
def source_branch_sha
source_branch.fetch('commit', {}).fetch('hash', nil)
end
def target_branch_name
target_branch.fetch('branch', {}).fetch('name', nil)
end
def target_branch_sha
target_branch.fetch('commit', {}).fetch('hash', nil)
end
private
def source_branch
raw['source']
end
def target_branch
raw['destination']
end
end
end
end
module Bitbucket
module Representation
class PullRequestComment < Comment
def iid
raw['id']
end
def file_path
inline.fetch('path')
end
def old_pos
inline.fetch('from')
end
def new_pos
inline.fetch('to')
end
def parent_id
raw.fetch('parent', {}).fetch('id', nil)
end
def inline?
raw.has_key?('inline')
end
def has_parent?
raw.has_key?('parent')
end
private
def inline
raw.fetch('inline', {})
end
end
end
end
module Bitbucket
module Representation
class Repo < Representation::Base
attr_reader :owner, :slug
def initialize(raw)
super(raw)
end
def owner_and_slug
@owner_and_slug ||= full_name.split('/', 2)
end
def owner
owner_and_slug.first
end
def slug
owner_and_slug.last
end
def clone_url(token = nil)
url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
if token.present?
clone_url = URI::parse(url)
clone_url.user = "x-token-auth:#{token}"
clone_url.to_s
else
url
end
end
def description
raw['description']
end
def full_name
raw['full_name']
end
def issues_enabled?
raw['has_issues']
end
def name
raw['name']
end
def valid?
raw['scm'] == 'git'
end
def visibility_level
if raw['is_private']
Gitlab::VisibilityLevel::PRIVATE
else
Gitlab::VisibilityLevel::PUBLIC
end
end
def to_s
full_name
end
end
end
end
module Bitbucket
module Representation
class User < Representation::Base
def username
raw['username']
end
end
end
end
......@@ -2,6 +2,10 @@ module Gitlab
module Auth
class MissingPersonalTokenError < StandardError; end
SCOPES = [:api, :read_user]
DEFAULT_SCOPES = [:api]
OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
......@@ -88,7 +92,7 @@ module Gitlab
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
if token && token.accessible?
if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id)
Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end
......@@ -97,12 +101,27 @@ module Gitlab
def personal_access_token_check(login, password)
if login && password
user = User.find_by_personal_access_token(password)
token = PersonalAccessToken.active.find_by_token(password)
validation = User.by_login(login)
Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
if valid_personal_access_token?(token, validation)
Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
end
end
end
def valid_oauth_token?(token)
token && token.accessible? && valid_api_token?(token)
end
def valid_personal_access_token?(token, user)
token && token.user == user && valid_api_token?(token)
end
def valid_api_token?(token)
AccessTokenValidationService.new(token).include_any_scope?(['api'])
end
def lfs_token_check(login, password)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
......
module Gitlab
module BitbucketImport
mattr_accessor :public_key
@public_key = nil
end
end
module Gitlab
module BitbucketImport
class Client
class Unauthorized < StandardError; end
attr_reader :consumer, :api
def self.from_project(project)
import_data_credentials = project.import_data.credentials if project.import_data
if import_data_credentials && import_data_credentials[:bb_session]
token = import_data_credentials[:bb_session][:bitbucket_access_token]
token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret]
new(token, token_secret)
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}"
end
end
def initialize(access_token = nil, access_token_secret = nil)
@consumer = ::OAuth::Consumer.new(
config.app_id,
config.app_secret,
bitbucket_options
)
if access_token && access_token_secret
@api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
end
end
def request_token(redirect_uri)
request_token = consumer.get_request_token(oauth_callback: redirect_uri)
{
oauth_token: request_token.token,
oauth_token_secret: request_token.secret,
oauth_callback_confirmed: request_token.callback_confirmed?.to_s
}
end
def authorize_url(request_token, redirect_uri)
request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
if request_token.callback_confirmed?
request_token.authorize_url
else
request_token.authorize_url(oauth_callback: redirect_uri)
end
end
def get_token(request_token, oauth_verifier, redirect_uri)
request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
if request_token.callback_confirmed?
request_token.get_access_token(oauth_verifier: oauth_verifier)
else
request_token.get_access_token(oauth_callback: redirect_uri)
end
end
def user
JSON.parse(get("/api/1.0/user").body)
end
def issues(project_identifier)
all_issues = []
offset = 0
per_page = 50 # Maximum number allowed by Bitbucket
index = 0
begin
issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body)
# Find out how many total issues are present
total = issues["count"] if index == 0
all_issues.concat(issues["issues"])
offset += issues["issues"].count
index += 1
end while all_issues.count < total
all_issues
end
def issue_comments(project_identifier, issue_id)
comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
comments.sort_by { |comment| comment["utc_created_on"] }
end
def project(project_identifier)
JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body)
end
def find_deploy_key(project_identifier, key)
JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
deploy_key["key"].chomp == key.chomp
end
end
def add_deploy_key(project_identifier, key)
deploy_key = find_deploy_key(project_identifier, key)
return if deploy_key
JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
end
def delete_deploy_key(project_identifier, key)
deploy_key = find_deploy_key(project_identifier, key)
return unless deploy_key
api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204"
end
def projects
JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" }
end
def incompatible_projects
JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" }
end
private
def get(url)
response = api.get(url)
raise Unauthorized if (400..499).cover?(response.code.to_i)
response
end
def issue_api_endpoint(project_identifier, per_page, offset)
"/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}"
end
def config
Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" }
end
def bitbucket_options
OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys
end
end
end
end
module Gitlab
module BitbucketImport
class Importer
attr_reader :project, :client
LABELS = [{ title: 'bug', color: '#FF0000' },
{ title: 'enhancement', color: '#428BCA' },
{ title: 'proposal', color: '#69D100' },
{ title: 'task', color: '#7F8C8D' }].freeze
attr_reader :project, :client, :errors, :users
def initialize(project)
@project = project
@client = Client.from_project(@project)
@client = Bitbucket::Client.new(project.import_data.credentials)
@formatter = Gitlab::ImportFormatter.new
@labels = {}
@errors = []
@users = {}
end
def execute
import_issues if has_issues?
import_issues
import_pull_requests
handle_errors
true
rescue ActiveRecord::RecordInvalid => e
raise Projects::ImportService::Error.new, e.message
ensure
Gitlab::BitbucketImport::KeyDeleter.new(project).execute
end
private
def gitlab_user_id(project, bitbucket_id)
if bitbucket_id
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
(user && user.id) || project.creator_id
else
project.creator_id
def handle_errors
return unless errors.any?
project.update_column(:import_error, {
message: 'The remote data could not be fully imported.',
errors: errors
}.to_json)
end
def gitlab_user_id(project, username)
find_user_id(username) || project.creator_id
end
def identifier
project.import_source
def find_user_id(username)
return nil unless username
return users[username] if users.key?(username)
users[username] = User.select(:id)
.joins(:identities)
.find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username)
.try(:id)
end
def has_issues?
client.project(identifier)["has_issues"]
def repo
@repo ||= client.repo(project.import_source)
end
def import_issues
issues = client.issues(identifier)
return unless repo.issues_enabled?
create_labels
client.issues(repo).each do |issue|
begin
description = ''
description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
description += issue.description
label_name = issue.kind
milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
gitlab_issue = project.issues.create!(
iid: issue.iid,
title: issue.title,
description: description,
state: issue.state,
author_id: gitlab_user_id(project, issue.author),
milestone: milestone,
created_at: issue.created_at,
updated_at: issue.updated_at
)
gitlab_issue.labels << @labels[label_name]
import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
rescue StandardError => e
errors << { type: :issue, iid: issue.iid, errors: e.message }
end
end
end
def import_issue_comments(issue, gitlab_issue)
client.issue_comments(repo, issue.iid).each do |comment|
# The note can be blank for issue service messages like "Changed title: ..."
# We would like to import those comments as well but there is no any
# specific parameter that would allow to process them, it's just an empty comment.
# To prevent our importer from just crashing or from creating useless empty comments
# we do this check.
next unless comment.note.present?
note = ''
note += @formatter.author_line(comment.author) unless find_user_id(comment.author)
note += comment.note
begin
gitlab_issue.notes.create!(
project: project,
note: note,
author_id: gitlab_user_id(project, comment.author),
created_at: comment.created_at,
updated_at: comment.updated_at
)
rescue StandardError => e
errors << { type: :issue_comment, iid: issue.iid, errors: e.message }
end
end
end
def create_labels
LABELS.each do |label|
@labels[label[:title]] = project.labels.create!(label)
end
end
issues.each do |issue|
body = ''
reporter = nil
author = 'Anonymous'
def import_pull_requests
pull_requests = client.pull_requests(repo)
pull_requests.each do |pull_request|
begin
description = ''
description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
description += pull_request.description
merge_request = project.merge_requests.create(
iid: pull_request.iid,
title: pull_request.title,
description: description,
source_project: project,
source_branch: pull_request.source_branch_name,
source_branch_sha: pull_request.source_branch_sha,
target_project: project,
target_branch: pull_request.target_branch_name,
target_branch_sha: pull_request.target_branch_sha,
state: pull_request.state,
author_id: gitlab_user_id(project, pull_request.author),
assignee_id: nil,
created_at: pull_request.created_at,
updated_at: pull_request.updated_at
)
if issue["reported_by"] && issue["reported_by"]["username"]
reporter = issue["reported_by"]["username"]
author = reporter
import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
rescue StandardError => e
errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
end
end
end
body = @formatter.author_line(author)
body += issue["content"]
def import_pull_request_comments(pull_request, merge_request)
comments = client.pull_request_comments(repo, pull_request.iid)
comments = client.issue_comments(identifier, issue["local_id"])
inline_comments, pr_comments = comments.partition(&:inline?)
if comments.any?
body += @formatter.comments_header
import_inline_comments(inline_comments, pull_request, merge_request)
import_standalone_pr_comments(pr_comments, merge_request)
end
comments.each do |comment|
author = 'Anonymous'
def import_inline_comments(inline_comments, pull_request, merge_request)
line_code_map = {}
children, parents = inline_comments.partition(&:has_parent?)
if comment["author_info"] && comment["author_info"]["username"]
author = comment["author_info"]["username"]
# The Bitbucket API returns threaded replies as parent-child
# relationships. We assume that the child can appear in any order in
# the JSON.
parents.each do |comment|
line_code_map[comment.iid] = generate_line_code(comment)
end
body += @formatter.comment(author, comment["utc_created_on"], comment["content"])
children.each do |comment|
line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil)
end
project.issues.create!(
description: body,
title: issue["title"],
state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
author_id: gitlab_user_id(project, reporter)
)
inline_comments.each do |comment|
begin
attributes = pull_request_comment_attributes(comment)
attributes.merge!(
position: build_position(merge_request, comment),
line_code: line_code_map.fetch(comment.iid),
type: 'DiffNote')
merge_request.notes.create!(attributes)
rescue StandardError => e
errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
end
end
def build_position(merge_request, pr_comment)
params = {
diff_refs: merge_request.diff_refs,
old_path: pr_comment.file_path,
new_path: pr_comment.file_path,
old_line: pr_comment.old_pos,
new_line: pr_comment.new_pos
}
Gitlab::Diff::Position.new(params)
end
def import_standalone_pr_comments(pr_comments, merge_request)
pr_comments.each do |comment|
begin
merge_request.notes.create!(pull_request_comment_attributes(comment))
rescue StandardError => e
errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
end
rescue ActiveRecord::RecordInvalid => e
raise Projects::ImportService::Error, e.message
end
def generate_line_code(pr_comment)
Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
end
def pull_request_comment_attributes(comment)
{
project: project,
note: comment.note,
author_id: gitlab_user_id(project, comment.author),
created_at: comment.created_at,
updated_at: comment.updated_at
}
end
end
end
......
module Gitlab
module BitbucketImport
class KeyAdder
attr_reader :repo, :current_user, :client
def initialize(repo, current_user, access_params)
@repo, @current_user = repo, current_user
@client = Client.new(access_params[:bitbucket_access_token],
access_params[:bitbucket_access_token_secret])
end
def execute
return false unless BitbucketImport.public_key.present?
project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
client.add_deploy_key(project_identifier, BitbucketImport.public_key)
true
rescue
false
end
end
end
end
module Gitlab
module BitbucketImport
class KeyDeleter
attr_reader :project, :current_user, :client
def initialize(project)
@project = project
@current_user = project.creator
@client = Client.from_project(@project)
end
def execute
return false unless BitbucketImport.public_key.present?
client.delete_deploy_key(project.import_source, BitbucketImport.public_key)
true
rescue
false
end
end
end
end
module Gitlab
module BitbucketImport
class ProjectCreator
attr_reader :repo, :namespace, :current_user, :session_data
attr_reader :repo, :name, :namespace, :current_user, :session_data
def initialize(repo, namespace, current_user, session_data)
def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
@name = name
@namespace = namespace
@current_user = current_user
@session_data = session_data
......@@ -13,15 +14,15 @@ module Gitlab
def execute
::Projects::CreateService.new(
current_user,
name: repo["name"],
path: repo["slug"],
description: repo["description"],
name: name,
path: name,
description: repo.description,
namespace_id: namespace.id,
visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "bitbucket",
import_source: "#{repo["owner"]}/#{repo["slug"]}",
import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
import_data: { credentials: { bb_session: session_data } }
visibility_level: repo.visibility_level,
import_type: 'bitbucket',
import_source: repo.full_name,
import_url: repo.clone_url(session_data[:token]),
import_data: { credentials: session_data }
).execute
end
end
......
......@@ -3,11 +3,12 @@ module Gitlab
class ChangeAccess
attr_reader :user_access, :project
def initialize(change, user_access:, project:)
def initialize(change, user_access:, project:, env: {})
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@user_access = user_access
@project = project
@env = env
end
def exec
......@@ -68,7 +69,7 @@ module Gitlab
end
def forced_push?
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
end
def matching_merge_request?
......
module Gitlab
module Checks
class ForcePush
def self.force_push?(project, oldrev, newrev)
def self.force_push?(project, oldrev, newrev, env: {})
return false if project.empty_repo?
# Created or deleted branch
if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
false
else
missed_ref, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list --max-count=1 #{oldrev} ^#{newrev}))
missed_ref, exit_status = Gitlab::Git::RevList.new(oldrev, newrev, project: project, env: env).execute
if exit_status == 0
missed_ref.present?
else
raise "Got a non-zero exit code while calling out to `git rev-list` in the force-push check."
end
end
end
end
......
......@@ -17,6 +17,10 @@ module Gitlab
'icon_status_manual'
end
def group
'manual'
end
def has_action?
can?(user, :update_build, subject)
end
......
......@@ -17,6 +17,10 @@ module Gitlab
'icon_status_manual'
end
def group
'manual'
end
def has_action?
can?(user, :update_build, subject)
end
......
......@@ -22,15 +22,8 @@ module Gitlab
raise NotImplementedError
end
# Deprecation warning: this method is here because we need to maintain
# backwards compatibility with legacy statuses. We often do something
# like "ci-status ci-status-#{status}" to set CSS class.
#
# `to_s` method should be renamed to `group` at some point, after
# phasing legacy satuses out.
#
def to_s
self.class.name.demodulize.downcase.underscore
def group
self.class.name.demodulize.underscore
end
def has_details?
......
......@@ -17,7 +17,7 @@ module Gitlab
'icon_status_warning'
end
def to_s
def group
'success_with_warnings'
end
......
module Gitlab
module Git
class RevList
attr_reader :project, :env
ALLOWED_VARIABLES = %w[GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES].freeze
def initialize(oldrev, newrev, project:, env: nil)
@project = project
@env = env.presence || {}
@args = [Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
"rev-list",
"--max-count=1",
oldrev,
"^#{newrev}"]
end
def execute
Gitlab::Popen.popen(@args, nil, parse_environment_variables)
end
def valid?
environment_variables.all? do |(name, value)|
value.start_with?(project.repository.path_to_repo)
end
end
private
def parse_environment_variables
return {} unless valid?
environment_variables
end
def environment_variables
@environment_variables ||= env.slice(*ALLOWED_VARIABLES)
end
end
end
end
......@@ -17,12 +17,13 @@ module Gitlab
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
def initialize(actor, project, protocol, authentication_abilities:)
def initialize(actor, project, protocol, authentication_abilities:, env: {})
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
@env = env
end
def check(cmd, changes)
......@@ -103,7 +104,7 @@ module Gitlab
end
def change_access_check(change)
Checks::ChangeAccess.new(change, user_access: user_access, project: project).exec
Checks::ChangeAccess.new(change, user_access: user_access, project: project, env: @env).exec
end
def protocol_allowed?
......
......@@ -5,13 +5,13 @@ module Gitlab
module Popen
extend self
def popen(cmd, path = nil)
def popen(cmd, path = nil, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
path ||= Dir.pwd
vars = { "PWD" => path }
vars['PWD'] = path
options = { chdir: path }
unless File.directory?(path)
......
module Gitlab
module Template
class DockerfileTemplate < BaseTemplate
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
end
class << self
def extension
'Dockerfile'
end
def categories
{
"General" => ''
}
end
def base_dir
Rails.root.join('vendor/dockerfile')
end
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
end
end
end
end
end
module Mattermost
class NoSessionError < StandardError; end
# This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider.
#
# The process depends on OAuth, but skips a step in the authentication cycle.
# For example, usually a user would click the 'login in GitLab' button on
# Mattermost, which would yield a 302 status code and redirects you to GitLab
# to approve the use of your account on Mattermost. Which would trigger a
# callback so Mattermost knows this request is approved and gets the required
# data to create the user account etc.
#
# This class however skips the button click, and also the approval phase to
# speed up the process and keep it without manual action and get a session
# going.
class Session
include Doorkeeper::Helpers::Controller
include HTTParty
base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token
def initialize(current_user)
@current_resource_owner = current_user
end
def with_session
raise NoSessionError unless create
begin
yield self
ensure
destroy
end
end
# Next methods are needed for Doorkeeper
def pre_auth
@pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
Doorkeeper.configuration, server.client_via_uid, params)
end
def authorization
@authorization ||= strategy.request
end
def strategy
@strategy ||= server.authorization_request(pre_auth.response_type)
end
def request
@request ||= OpenStruct.new(parameters: params)
end
def params
Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
end
def get(path, options = {})
self.class.get(path, options.merge(headers: @headers))
end
def post(path, options = {})
self.class.post(path, options.merge(headers: @headers))
end
private
def create
return unless oauth_uri
return unless token_uri
@token = request_token
@headers = {
Authorization: "Bearer #{@token}"
}
@token
end
def destroy
post('/api/v3/users/logout')
end
def oauth_uri
return @oauth_uri if defined?(@oauth_uri)
@oauth_uri = nil
response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
return unless 300 <= response.code && response.code < 400
redirect_uri = response.headers['location']
return unless redirect_uri
@oauth_uri = URI.parse(redirect_uri)
end
def token_uri
@token_uri ||=
if oauth_uri
authorization.authorize.redirect_uri if pre_auth.authorizable?
end
end
def request_token
response = get(token_uri, follow_redirects: false)
if 200 <= response.code && response.code < 400
response.headers['token']
end
end
end
end
require 'omniauth-oauth2'
module OmniAuth
module Strategies
class Bitbucket < OmniAuth::Strategies::OAuth2
option :name, 'bitbucket'
option :client_options, {
site: 'https://bitbucket.org',
authorize_url: 'https://bitbucket.org/site/oauth2/authorize',
token_url: 'https://bitbucket.org/site/oauth2/access_token'
}
uid do
raw_info['username']
end
info do
{
name: raw_info['display_name'],
avatar: raw_info['links']['avatar']['href'],
email: primary_email
}
end
def raw_info
@raw_info ||= access_token.get('api/2.0/user').parsed
end
def primary_email
primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] }
primary && primary['email'] || nil
end
def emails
email_response = access_token.get('api/2.0/user/emails').parsed
@emails ||= email_response && email_response['values'] || nil
end
end
end
end
......@@ -20,6 +20,11 @@ upstream gitlab-workhorse {
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
map $http_upgrade $connection_upgrade_gitlab {
default upgrade;
'' close;
}
## Normal HTTP host
server {
## Either remove "default_server" from the listen line below,
......@@ -53,6 +58,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade_gitlab;
proxy_pass http://gitlab-workhorse;
}
......
......@@ -24,6 +24,11 @@ upstream gitlab-workhorse {
server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
map $http_upgrade $connection_upgrade_gitlab_ssl {
default upgrade;
'' close;
}
## Redirects all HTTP traffic to the HTTPS host
server {
## Either remove "default_server" from the listen line below,
......@@ -98,6 +103,9 @@ server {
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade_gitlab_ssl;
proxy_pass http://gitlab-workhorse;
}
......
......@@ -6,11 +6,11 @@ describe Import::BitbucketController do
let(:user) { create(:user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
let(:refresh_token) { SecureRandom.hex(15) }
let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
def assign_session_tokens
session[:bitbucket_access_token] = token
session[:bitbucket_access_token_secret] = secret
session[:bitbucket_token] = token
end
before do
......@@ -24,29 +24,36 @@ describe Import::BitbucketController do
end
it "updates access token" do
access_token = double(token: token, secret: secret)
allow_any_instance_of(Gitlab::BitbucketImport::Client).
expires_at = Time.now + 1.day
expires_in = 1.day
access_token = double(token: token,
secret: secret,
expires_at: expires_at,
expires_in: expires_in,
refresh_token: refresh_token)
allow_any_instance_of(OAuth2::Client).
to receive(:get_token).and_return(access_token)
stub_omniauth_provider('bitbucket')
get :callback
expect(session[:bitbucket_access_token]).to eq(token)
expect(session[:bitbucket_access_token_secret]).to eq(secret)
expect(session[:bitbucket_token]).to eq(token)
expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
expect(session[:bitbucket_expires_at]).to eq(expires_at)
expect(session[:bitbucket_expires_in]).to eq(expires_in)
expect(controller).to redirect_to(status_import_bitbucket_url)
end
end
describe "GET status" do
before do
@repo = OpenStruct.new(slug: 'vim', owner: 'asd')
@repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true)
assign_session_tokens
end
it "assigns variables" do
@project = create(:project, import_type: 'bitbucket', creator_id: user.id)
client = stub_client(projects: [@repo])
allow(client).to receive(:incompatible_projects).and_return([])
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
......@@ -57,7 +64,7 @@ describe Import::BitbucketController do
it "does not show already added project" do
@project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
stub_client(projects: [@repo])
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
get :status
......@@ -70,19 +77,16 @@ describe Import::BitbucketController do
let(:bitbucket_username) { user.username }
let(:bitbucket_user) do
{ user: { username: bitbucket_username } }.with_indifferent_access
double(username: bitbucket_username)
end
let(:bitbucket_repo) do
{ slug: "vim", owner: bitbucket_username }.with_indifferent_access
double(slug: "vim", owner: bitbucket_username, name: 'vim')
end
before do
allow(Gitlab::BitbucketImport::KeyAdder).
to receive(:new).with(bitbucket_repo, user, access_params).
and_return(double(execute: true))
stub_client(user: bitbucket_user, project: bitbucket_repo)
allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo)
allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user)
assign_session_tokens
end
......@@ -90,7 +94,7 @@ describe Import::BitbucketController do
context "when the Bitbucket user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
......@@ -102,7 +106,7 @@ describe Import::BitbucketController do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
......@@ -114,7 +118,7 @@ describe Import::BitbucketController do
let(:other_username) { "someone_else" }
before do
bitbucket_repo["owner"] = other_username
allow(bitbucket_repo).to receive(:owner).and_return(other_username)
end
context "when a namespace with the Bitbucket user's username already exists" do
......@@ -123,7 +127,7 @@ describe Import::BitbucketController do
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
......@@ -156,7 +160,7 @@ describe Import::BitbucketController do
it "takes the new namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params).
and_return(double(execute: true))
post :create, format: :js
......@@ -177,7 +181,7 @@ describe Import::BitbucketController do
it "takes the current user's namespace" do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
......
require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
describe '#create' do
def created_token
PersonalAccessToken.order(:created_at).last
end
before { sign_in(user) }
it "allows creation of a token" do
name = FFaker::Product.brand
post :create, personal_access_token: { name: name }
expect(created_token).not_to be_nil
expect(created_token.name).to eq(name)
expect(created_token.expires_at).to be_nil
expect(PersonalAccessToken.active).to include(created_token)
end
it "allows creation of a token with an expiry date" do
expires_at = 5.days.from_now
post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
expect(created_token).not_to be_nil
expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
end
context "scopes" do
it "allows creation of a token with scopes" do
post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] }
expect(created_token).not_to be_nil
expect(created_token.scopes).to eq(['api', 'read_user'])
end
it "allows creation of a token with no scopes" do
post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
expect(created_token).not_to be_nil
expect(created_token.scopes).to eq([])
end
end
end
end
......@@ -5,5 +5,6 @@ FactoryGirl.define do
name { FFaker::Product.brand }
revoked false
expires_at { 5.days.from_now }
scopes ['api']
end
end
class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedAdmin
require 'spec_helper'
step 'there are public deploy keys in system' do
create(:deploy_key, public: true)
create(:another_deploy_key, public: true)
end
RSpec.describe 'admin deploy keys', type: :feature do
let!(:deploy_key) { create(:deploy_key, public: true) }
let!(:another_deploy_key) { create(:another_deploy_key, public: true) }
step 'I should see all public deploy keys' do
DeployKey.are_public.each do |p|
expect(page).to have_content p.title
end
before do
login_as(:admin)
end
step 'I visit admin deploy key page' do
visit admin_deploy_key_path(deploy_key)
it 'show all public deploy keys' do
visit admin_deploy_keys_path
expect(page).to have_content(deploy_key.title)
expect(page).to have_content(another_deploy_key.title)
end
step 'I visit admin deploy keys page' do
it 'creates new deploy key' do
visit admin_deploy_keys_path
end
step 'I click \'New Deploy Key\'' do
click_link 'New Deploy Key'
end
fill_in 'deploy_key_title', with: 'laptop'
fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
click_button 'Create'
step 'I submit new deploy key' do
fill_in "deploy_key_title", with: "laptop"
fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
click_button "Create"
end
step 'I should be on admin deploy keys page' do
expect(current_path).to eq admin_deploy_keys_path
end
step 'I should see newly created deploy key' do
expect(page).to have_content(deploy_key.title)
end
def deploy_key
@deploy_key ||= DeployKey.are_public.first
expect(page).to have_content('laptop')
end
end
require 'spec_helper'
RSpec.describe 'admin manage applications', feature: true do
before do
login_as :admin
end
it do
visit admin_applications_path
click_on 'New Application'
expect(page).to have_content('New application')
fill_in :doorkeeper_application_name, with: 'test'
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
click_on 'Submit'
expect(page).to have_content('Application: test')
expect(page).to have_content('Application Id')
expect(page).to have_content('Secret')
click_on 'Edit'
expect(page).to have_content('Edit application')
fill_in :doorkeeper_application_name, with: 'test_changed'
click_on 'Submit'
expect(page).to have_content('test_changed')
expect(page).to have_content('Application Id')
expect(page).to have_content('Secret')
visit admin_applications_path
page.within '.oauth-applications' do
click_on 'Destroy'
end
expect(page.find('.oauth-applications')).not_to have_content('test_changed')
end
end
require 'spec_helper'
feature 'Groups > Members > Sorting', feature: true do
let(:owner) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:group) { create(:group) }
background do
create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
login_as(owner)
end
scenario 'sorts alphabetically by default' do
visit_members_list(sort: nil)
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
scenario 'sorts by access level ascending' do
visit_members_list(sort: :access_level_asc)
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
scenario 'sorts by access level descending' do
visit_members_list(sort: :access_level_desc)
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
scenario 'sorts by last joined' do
visit_members_list(sort: :last_joined)
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
scenario 'sorts by oldest joined' do
visit_members_list(sort: :oldest_joined)
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
scenario 'sorts by name ascending' do
visit_members_list(sort: :name_asc)
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
scenario 'sorts by name descending' do
visit_members_list(sort: :name_desc)
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
scenario 'sorts by recent sign in' do
visit_members_list(sort: :recent_sign_in)
expect(first_member).to include(owner.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
scenario 'sorts by oldest sign in' do
visit_members_list(sort: :oldest_sign_in)
expect(first_member).to include(developer.name)
expect(second_member).to include(owner.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
end
def visit_members_list(sort:)
visit group_group_members_path(group.to_param, sort: sort)
end
def first_member
page.all('ul.content-list > li').first.text
end
def second_member
page.all('ul.content-list > li').last.text
end
end
require 'spec_helper'
feature 'Merge Request closing issues message', feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue_1) { create(:issue, project: project)}
let(:issue_2) { create(:issue, project: project)}
let(:merge_request) do
create(
:merge_request,
:simple,
source_project: project,
description: merge_request_description
)
end
let(:merge_request_description) { 'Merge Request Description' }
before do
project.team << [user, :master]
login_as user
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
context 'not closing or mentioning any issue' do
it 'does not display closing issue message' do
expect(page).not_to have_css('.mr-widget-footer')
end
end
context 'closing issues but not mentioning any other issue' do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
context 'mentioning issues but not closing them' do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.")
end
end
context 'closing some issues and mentioning, but not closing, others' do
let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.")
end
end
end
require 'spec_helper'
feature 'Clicking toggle commit message link', feature: true, js: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue_1) { create(:issue, project: project)}
let(:issue_2) { create(:issue, project: project)}
let(:merge_request) do
create(
:merge_request,
:simple,
source_project: project,
description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}"
)
end
let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) }
let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) }
let(:default_message) do
[
"Merge branch 'feature' into 'master'",
merge_request.title,
"Closes #{issue_1.to_reference} and #{issue_2.to_reference}",
"See merge request #{merge_request.to_reference}"
].join("\n\n")
end
let(:message_with_description) do
[
"Merge branch 'feature' into 'master'",
merge_request.title,
merge_request.description,
"See merge request #{merge_request.to_reference}"
].join("\n\n")
end
before do
project.team << [user, :master]
login_as user
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(textbox).not_to be_visible
click_link "Modify commit message"
expect(textbox).to be_visible
end
it "toggles commit message between message with description and without description " do
expect(textbox.value).to eq(default_message)
click_link "Include description in commit message"
expect(textbox.value).to eq(message_with_description)
click_link "Don't include description in commit message"
expect(textbox.value).to eq(default_message)
end
it "toggles link between 'Include description' and 'Don't include description'" do
expect(include_link).to be_visible
expect(do_not_include_link).not_to be_visible
click_link "Include description in commit message"
expect(include_link).not_to be_visible
expect(do_not_include_link).to be_visible
click_link "Don't include description in commit message"
expect(include_link).to be_visible
expect(do_not_include_link).not_to be_visible
end
end
require 'spec_helper'
feature 'Member autocomplete', feature: true do
include WaitForAjax
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:participant) { create(:user) }
......@@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do
end
def open_member_suggestions
sleep 1
page.within('.new-note') do
sleep 1
find('#note_note').native.send_keys('@')
find('#note_note').send_keys('@')
end
wait_for_ajax
end
def visit_issue(project, issue)
......
......@@ -27,28 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
describe "token creation" do
it "allows creation of a token" do
visit profile_personal_access_tokens_path
fill_in "Name", with: FFaker::Product.brand
expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
expect(active_personal_access_tokens).to have_text("Never")
end
name = FFaker::Product.brand
it "allows creation of a token with an expiry date" do
visit profile_personal_access_tokens_path
fill_in "Name", with: FFaker::Product.brand
fill_in "Name", with: name
# Set date to 1st of next month
find_field("Expires at").trigger('focus')
find("a[title='Next']").click
click_on "1"
expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
# Scopes
check "api"
check "read_user"
click_on "Create Personal Access Token"
expect(active_personal_access_tokens).to have_text(name)
expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
expect(active_personal_access_tokens).to have_text('api')
expect(active_personal_access_tokens).to have_text('read_user')
end
context "when creation fails" do
......@@ -85,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
disallow_personal_access_token_saves!
visit profile_personal_access_tokens_path
expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
click_on "Revoke"
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
expect(page).to have_content("Could not revoke")
end
......
require 'spec_helper'
feature 'User wants to add a Dockerfile file', feature: true do
include WaitForAjax
before do
user = create(:user)
project = create(:project)
project.team << [user, :master]
login_as user
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile')
end
scenario 'user can see Dockerfile dropdown' do
expect(page).to have_css('.dockerfile-selector')
end
scenario 'user can pick a Dockerfile file from the dropdown', js: true do
find('.js-dockerfile-selector').click
wait_for_ajax
within '.dockerfile-selector' do
find('.dropdown-input-field').set('HTTPd')
find('.dropdown-content li', text: 'HTTPd').click
end
wait_for_ajax
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
end
end
......@@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
it 'does not load on project#show' do
expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
end
it 'loads on new issue page' do
visit new_namespace_project_issue_path(project.namespace, project)
expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({})
end
end
require 'spec_helper'
feature 'Projects > Members > Sorting', feature: true do
let(:master) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:project) { create(:empty_project) }
background do
create(:project_member, :master, user: master, project: project, created_at: 5.days.ago)
create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
login_as(master)
end
scenario 'sorts alphabetically by default' do
visit_members_list(sort: nil)
expect(first_member).to include(master.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
scenario 'sorts by access level ascending' do
visit_members_list(sort: :access_level_asc)
expect(first_member).to include(developer.name)
expect(second_member).to include(master.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
end
scenario 'sorts by access level descending' do
visit_members_list(sort: :access_level_desc)
expect(first_member).to include(master.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
end
scenario 'sorts by last joined' do
visit_members_list(sort: :last_joined)
expect(first_member).to include(developer.name)
expect(second_member).to include(master.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
end
scenario 'sorts by oldest joined' do
visit_members_list(sort: :oldest_joined)
expect(first_member).to include(master.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
end
scenario 'sorts by name ascending' do
visit_members_list(sort: :name_asc)
expect(first_member).to include(master.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
end
scenario 'sorts by name descending' do
visit_members_list(sort: :name_desc)
expect(first_member).to include(developer.name)
expect(second_member).to include(master.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
end
scenario 'sorts by recent sign in' do
visit_members_list(sort: :recent_sign_in)
expect(first_member).to include(master.name)
expect(second_member).to include(developer.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
end
scenario 'sorts by oldest sign in' do
visit_members_list(sort: :oldest_sign_in)
expect(first_member).to include(developer.name)
expect(second_member).to include(master.name)
expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
end
def visit_members_list(sort:)
visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
end
def first_member
page.all('ul.content-list > li').first.text
end
def second_member
page.all('ul.content-list > li').last.text
end
end
......@@ -99,7 +99,7 @@ describe "Pipelines", feature: true, js: true do
context 'when pipeline has manual builds' do
it 'shows the skipped icon and a play action for the manual build' do
page.within('a[data-title="manual build - manual play action"]') do
expect(page).to have_selector('.ci-status-icon-skipped')
expect(page).to have_selector('.ci-status-icon-manual')
expect(page).to have_content('manual')
end
......
......@@ -6,7 +6,7 @@ describe GroupsHelper do
it 'returns an url for the avatar' do
group = create(:group)
group.avatar = File.open(avatar_file_path)
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
expect(group_icon(group.path).to_s).
to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
......
require 'spec_helper'
# Emulates paginator. It returns 2 pages with results
class TestPaginator
def initialize
@current_page = 0
end
def items
@current_page += 1
raise StopIteration if @current_page > 2
["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"]
end
end
describe Bitbucket::Collection do
it "iterates paginator" do
collection = described_class.new(TestPaginator.new)
expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"])
end
end
require 'spec_helper'
describe Bitbucket::Connection do
before do
allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: ''))
end
describe '#get' do
it 'calls OAuth2::AccessToken::get' do
expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true))
connection = described_class.new({})
connection.get('/users')
end
end
describe '#expired?' do
it 'calls connection.expired?' do
expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true)
expect(described_class.new({}).expired?).to be_truthy
end
end
describe '#refresh!' do
it 'calls connection.refresh!' do
response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil)
expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response)
described_class.new({}).refresh!
end
end
end
require 'spec_helper'
describe Bitbucket::Page do
let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } }
before do
# Autoloading hack
Bitbucket::Representation::User.new({})
end
describe '#items' do
it 'returns collection of needed objects' do
page = described_class.new(response, :user)
expect(page.items.first).to be_a(Bitbucket::Representation::User)
expect(page.items.count).to eq(1)
end
end
describe '#attrs' do
it 'returns attributes' do
page = described_class.new(response, :user)
expect(page.attrs.keys).to include(:pagelen, :next)
end
end
describe '#next?' do
it 'returns true' do
page = described_class.new(response, :user)
expect(page.next?).to be_truthy
end
it 'returns false' do
response['next'] = nil
page = described_class.new(response, :user)
expect(page.next?).to be_falsey
end
end
describe '#next' do
it 'returns next attribute' do
page = described_class.new(response, :user)
expect(page.next).to eq('')
end
end
end
require 'spec_helper'
describe Bitbucket::Paginator do
let(:last_page) { double(:page, next?: false, items: ['item_2']) }
let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) }
describe 'items' do
it 'return items and raises StopIteration in the end' do
paginator = described_class.new(nil, nil, nil)
allow(paginator).to receive(:fetch_next_page).and_return(first_page)
expect(paginator.items).to match(['item_1'])
allow(paginator).to receive(:fetch_next_page).and_return(last_page)
expect(paginator.items).to match(['item_2'])
allow(paginator).to receive(:fetch_next_page).and_return(nil)
expect{ paginator.items }.to raise_error(StopIteration)
end
end
end
require 'spec_helper'
describe Bitbucket::Representation::Comment do
describe '#author' do
it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') }
it { expect(described_class.new({}).author).to be_nil }
end
describe '#note' do
it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') }
it { expect(described_class.new({}).note).to be_nil }
end
describe '#created_at' do
it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
end
describe '#updated_at' do
it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) }
it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) }
end
end
require 'spec_helper'
describe Bitbucket::Representation::Issue do
describe '#iid' do
it { expect(described_class.new('id' => 1).iid).to eq(1) }
end
describe '#kind' do
it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') }
end
describe '#milestone' do
it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') }
it { expect(described_class.new({}).milestone).to be_nil }
end
describe '#author' do
it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') }
it { expect(described_class.new({}).author).to be_nil }
end
describe '#description' do
it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') }
it { expect(described_class.new({}).description).to be_nil }
end
describe '#state' do
it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') }
it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') }
it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') }
it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') }
it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') }
it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') }
end
describe '#title' do
it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
end
describe '#created_at' do
it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
end
describe '#updated_at' do
it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) }
end
end
require 'spec_helper'
describe Bitbucket::Representation::PullRequestComment do
describe '#iid' do
it { expect(described_class.new('id' => 1).iid).to eq(1) }
end
describe '#file_path' do
it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') }
end
describe '#old_pos' do
it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) }
end
describe '#new_pos' do
it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) }
end
describe '#parent_id' do
it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) }
it { expect(described_class.new({}).parent_id).to be_nil }
end
describe '#inline?' do
it { expect(described_class.new('inline' => {}).inline?).to be_truthy }
it { expect(described_class.new({}).inline?).to be_falsey }
end
describe '#has_parent?' do
it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy }
it { expect(described_class.new({}).has_parent?).to be_falsey }
end
end
require 'spec_helper'
describe Bitbucket::Representation::PullRequest do
describe '#iid' do
it { expect(described_class.new('id' => 1).iid).to eq(1) }
end
describe '#author' do
it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') }
it { expect(described_class.new({}).author).to be_nil }
end
describe '#description' do
it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') }
it { expect(described_class.new({}).description).to be_nil }
end
describe '#state' do
it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') }
it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') }
it { expect(described_class.new({}).state).to eq('opened') }
end
describe '#title' do
it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
end
describe '#source_branch_name' do
it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') }
it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil }
end
describe '#source_branch_sha' do
it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') }
it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil }
end
describe '#target_branch_name' do
it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') }
it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil }
end
describe '#target_branch_sha' do
it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') }
it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil }
end
end
require 'spec_helper'
describe Bitbucket::Representation::User do
describe '#username' do
it 'returns correct value' do
user = described_class.new('username' => 'Ben')
expect(user.username).to eq('Ben')
end
end
end
......@@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do
project.create_drone_ci_service(active: true)
project.drone_ci_service.update(token: 'token')
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token')
expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
it 'recognizes user lfs tokens' do
user = create(:user)
ip = 'ip'
token = Gitlab::LfsToken.new(user).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
end
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
ip = 'ip'
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
end
context "while using OAuth tokens as passwords" do
it 'succeeds for OAuth tokens with the `api` scope' do
user = create(:user)
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end
it 'recognizes OAuth tokens' do
it 'fails for OAuth tokens with other scopes' do
user = create(:user)
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
ip = 'ip'
token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user")
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
end
context "while using personal access tokens as passwords" do
it 'succeeds for personal access tokens with the `api` scope' do
user = create(:user)
personal_access_token = create(:personal_access_token, user: user, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
end
it 'fails for personal access tokens with other scopes' do
user = create(:user)
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
end
it 'returns double nil for invalid credentials' do
login = 'foo'
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new)
end
end
......
require 'spec_helper'
describe Gitlab::BitbucketImport::Client, lib: true do
include ImportSpecHelper
let(:token) { '123456' }
let(:secret) { 'secret' }
let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
before do
stub_omniauth_provider('bitbucket')
end
it 'all OAuth client options are symbols' do
client.consumer.options.keys.each do |key|
expect(key).to be_kind_of(Symbol)
end
end
context 'issues' do
let(:per_page) { 50 }
let(:count) { 95 }
let(:sample_issues) do
issues = []
count.times do |i|
issues << { local_id: i }
end
issues
end
let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } }
let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } }
let(:project_id) { 'namespace/repo' }
it 'retrieves issues over a number of pages' do
stub_request(:get,
"https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0").
to_return(status: 200,
body: first_sample_data.to_json,
headers: {})
stub_request(:get,
"https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50").
to_return(status: 200,
body: second_sample_data.to_json,
headers: {})
issues = client.issues(project_id)
expect(issues.count).to eq(95)
end
end
context 'project import' do
it 'calls .from_project with no errors' do
project = create(:empty_project)
project.import_url = "ssh://git@bitbucket.org/test/test.git"
project.create_or_update_import_data(credentials:
{ user: "git",
password: nil,
bb_session: { bitbucket_access_token: "test",
bitbucket_access_token_secret: "test" } })
expect { described_class.from_project(project) }.not_to raise_error
end
end
end
......@@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
"closed" # undocumented status
]
end
let(:sample_issues_statuses) do
issues = []
statuses.map.with_index do |status, index|
issues << {
local_id: index,
status: status,
id: index,
state: status,
title: "Issue #{index}",
content: "Some content to issue #{index}"
kind: 'bug',
content: {
raw: "Some content to issue #{index}",
markup: "markdown",
html: "Some content to issue #{index}"
}
}
end
......@@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
end
let(:project_identifier) { 'namespace/repo' }
let(:data) do
{
'bb_session' => {
'bitbucket_access_token' => "123456",
'bitbucket_access_token_secret' => "secret"
'bitbucket_token' => "123456",
'bitbucket_refresh_token' => "secret"
}
}
end
let(:project) do
create(
:project,
......@@ -49,11 +57,13 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
import_data: ProjectImportData.new(credentials: data)
)
end
let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
let(:issues_statuses_sample_data) do
{
count: sample_issues_statuses.count,
issues: sample_issues_statuses
values: sample_issues_statuses
}
end
......@@ -61,26 +71,46 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
before do
stub_request(
:get,
"https://bitbucket.org/api/1.0/repositories/#{project_identifier}"
).to_return(status: 200, body: { has_issues: true }.to_json)
"https://api.bitbucket.org/2.0/repositories/#{project_identifier}"
).to_return(status: 200,
headers: { "Content-Type" => "application/json" },
body: { has_issues: true, full_name: project_identifier }.to_json)
stub_request(
:get,
"https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0"
).to_return(status: 200, body: issues_statuses_sample_data.to_json)
"https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on"
).to_return(status: 200,
headers: { "Content-Type" => "application/json" },
body: issues_statuses_sample_data.to_json)
stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on").
with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
to_return(status: 200,
body: "",
headers: {})
sample_issues_statuses.each_with_index do |issue, index|
stub_request(
:get,
"https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments"
"https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on"
).to_return(
status: 200,
body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json
headers: { "Content-Type" => "application/json" },
body: { author_info: { username: "username" }, utc_created_on: index }.to_json
)
end
stub_request(
:get,
"https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL"
).to_return(status: 200,
headers: { "Content-Type" => "application/json" },
body: {}.to_json)
end
it 'map statuses to open or closed' do
# HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
Bitbucket::Representation::Issue.new({})
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
......
......@@ -2,14 +2,18 @@ require 'spec_helper'
describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
let(:repo) do
{
name: 'Vim',
double(name: 'Vim',
slug: 'vim',
description: 'Test repo',
is_private: true,
owner: "asd"
}.with_indifferent_access
owner: "asd",
full_name: 'Vim repo',
visibility_level: Gitlab::VisibilityLevel::PRIVATE,
clone_url: 'ssh://git@bitbucket.org/asd/vim.git')
end
let(:namespace){ create(:group, owner: user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
......@@ -22,7 +26,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params)
project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params)
project = project_creator.execute
expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
......
require 'spec_helper'
describe Gitlab::Checks::ChangeAccess, lib: true do
let(:project) { create(:project) }
context "exit code checking" do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end
it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
end
end
end
......@@ -32,6 +32,14 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
end
describe '#group' do
it 'does not override status group' do
expect(status).to receive(:group)
subject.group
end
end
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
......
......@@ -18,6 +18,10 @@ describe Gitlab::Ci::Status::Build::Play do
it { expect(subject.icon).to eq 'icon_status_manual' }
end
describe '#group' do
it { expect(subject.group).to eq 'manual' }
end
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
......
......@@ -32,6 +32,14 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
end
describe '#group' do
it 'does not override status group' do
expect(status).to receive(:group)
subject.group
end
end
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
......
......@@ -20,6 +20,10 @@ describe Gitlab::Ci::Status::Build::Stop do
it { expect(subject.icon).to eq 'icon_status_manual' }
end
describe '#group' do
it { expect(subject.group).to eq 'manual' }
end
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
......
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Canceled do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_canceled' }
end
describe '#group' do
it { expect(subject.group).to eq 'canceled' }
end
end
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Created do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_created' }
end
describe '#group' do
it { expect(subject.group).to eq 'created' }
end
end
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Failed do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_failed' }
end
describe '#group' do
it { expect(subject.group).to eq 'failed' }
end
end
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Pending do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_pending' }
end
describe '#group' do
it { expect(subject.group).to eq 'pending' }
end
end
......@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
it { expect(subject.icon).to eq 'icon_status_warning' }
end
describe '#group' do
it { expect(subject.group).to eq 'success_with_warnings' }
end
describe '.matches?' do
context 'when pipeline is successful' do
let(:pipeline) do
......
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Running do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_running' }
end
describe '#group' do
it { expect(subject.group).to eq 'running' }
end
end
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Skipped do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_skipped' }
end
describe '#group' do
it { expect(subject.group).to eq 'skipped' }
end
end
......@@ -16,4 +16,8 @@ describe Gitlab::Ci::Status::Success do
describe '#icon' do
it { expect(subject.icon).to eq 'icon_status_success' }
end
describe '#group' do
it { expect(subject.group).to eq 'success' }
end
end
require 'spec_helper'
describe Gitlab::Git::RevList, lib: true do
let(:project) { create(:project) }
context "validations" do
described_class::ALLOWED_VARIABLES.each do |var|
context var do
it "accepts values starting with the project repo path" do
env = { var => "#{project.repository.path_to_repo}/objects" }
rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
expect(rev_list).to be_valid
end
it "rejects values starting not with the project repo path" do
env = { var => "/some/other/path" }
rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
expect(rev_list).not_to be_valid
end
it "rejects values containing the project repo path but not starting with it" do
env = { var => "/some/other/path/#{project.repository.path_to_repo}" }
rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
expect(rev_list).not_to be_valid
end
end
end
end
context "#execute" do
let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } }
let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) }
it "calls out to `popen` without environment variables if the record is invalid" do
allow(rev_list).to receive(:valid?).and_return(false)
expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args)
rev_list.execute
end
it "calls out to `popen` with environment variables if the record is valid" do
allow(rev_list).to receive(:valid?).and_return(true)
expect(Open3).to receive(:popen3).with(hash_including(env), any_args)
rev_list.execute
end
end
end
require 'spec_helper'
describe Mattermost::Session, type: :request do
let(:user) { create(:user) }
let(:gitlab_url) { "http://gitlab.com" }
let(:mattermost_url) { "http://mattermost.com" }
subject { described_class.new(user) }
# Needed for doorkeeper to function
it { is_expected.to respond_to(:current_resource_owner) }
it { is_expected.to respond_to(:request) }
it { is_expected.to respond_to(:authorization) }
it { is_expected.to respond_to(:strategy) }
before do
described_class.base_uri(mattermost_url)
end
describe '#with session' do
let(:location) { 'http://location.tld' }
let!(:stub) do
WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login").
to_return(headers: { 'location' => location }, status: 307)
end
context 'without oauth uri' do
it 'makes a request to the oauth uri' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
context 'with oauth_uri' do
let!(:doorkeeper) do
Doorkeeper::Application.create(
name: "GitLab Mattermost",
redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete",
scopes: "")
end
context 'without token_uri' do
it 'can not create a session' do
expect do
subject.with_session
end.to raise_error(Mattermost::NoSessionError)
end
end
context 'with token_uri' do
let(:state) { "state" }
let(:params) do
{ response_type: "code",
client_id: doorkeeper.uid,
redirect_uri: "#{mattermost_url}/signup/gitlab/complete",
state: state }
end
let(:location) do
"#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}"
end
before do
WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete").
with(query: hash_including({ 'state' => state })).
to_return do |request|
post "/oauth/token",
client_id: doorkeeper.uid,
client_secret: doorkeeper.secret,
redirect_uri: params[:redirect_uri],
grant_type: 'authorization_code',
code: request.uri.query_values['code']
if response.status == 200
{ headers: { 'token' => 'thisworksnow' }, status: 202 }
end
end
WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout").
to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
end
it 'can setup a session' do
subject.with_session do |session|
end
expect(subject.token).not_to be_nil
end
it 'returns the value of the block' do
result = subject.with_session do |session|
"value"
end
expect(result).to eq("value")
end
end
end
end
end
......@@ -506,6 +506,17 @@ describe Ci::Build, models: true do
it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
end
context 'when build is for a deployment' do
let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
before do
build.environment = 'production'
allow(project).to receive(:deployment_variables).and_return([deployment_variable])
end
it { is_expected.to include(deployment_variable) }
end
context 'returns variables in valid order' do
before do
allow(build).to receive(:predefined_variables) { ['predefined'] }
......
......@@ -252,7 +252,7 @@ describe MergeRequest, models: true do
end
end
describe 'detection of issues to be closed' do
describe '#closes_issues' do
let(:issue0) { create :issue, project: subject.project }
let(:issue1) { create :issue, project: subject.project }
......@@ -280,14 +280,19 @@ describe MergeRequest, models: true do
expect(subject.closes_issues).to be_empty
end
end
describe '#issues_mentioned_but_not_closing' do
it 'detects issues mentioned in description but not closed' do
mentioned_issue = create(:issue, project: subject.project)
subject.project.team << [subject.author, :developer]
subject.description = "Is related to #{mentioned_issue.to_reference}"
it 'detects issues mentioned in the description' do
issue2 = create(:issue, project: subject.project)
subject.description = "Closes #{issue2.to_reference}"
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
expect(subject.closes_issues).to include(issue2)
expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
end
end
......@@ -410,11 +415,17 @@ describe MergeRequest, models: true do
.to match("Remove all technical debt\n\n")
end
it 'includes its description in the body' do
request = build(:merge_request, description: 'By removing all code')
it 'includes its closed issues in the body' do
issue = create(:issue, project: subject.project)
expect(request.merge_commit_message)
.to match("By removing all code\n\n")
subject.project.team << [subject.author, :developer]
subject.description = "This issue Closes #{issue.to_reference}"
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
expect(subject.merge_commit_message)
.to match("Closes #{issue.to_reference}")
end
it 'includes its reference in the body' do
......@@ -429,6 +440,20 @@ describe MergeRequest, models: true do
expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
end
it 'includes its description in the body' do
request = build(:merge_request, description: 'By removing all code')
expect(request.merge_commit_message(include_description: true))
.to match("By removing all code\n\n")
end
it 'does not includes its description in the body' do
request = build(:merge_request, description: 'By removing all code')
expect(request.merge_commit_message)
.not_to match("By removing all code\n\n")
end
end
describe "#reset_merge_when_build_succeeds" do
......
......@@ -123,4 +123,37 @@ describe KubernetesService, models: true do
end
end
end
describe '#predefined_variables' do
before do
subject.api_url = 'https://kube.domain.com'
subject.token = 'token'
subject.namespace = 'my-project'
subject.ca_pem = 'CA PEM DATA'
end
it 'sets KUBE_URL' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
)
end
it 'sets KUBE_TOKEN' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_TOKEN', value: 'token', public: false }
)
end
it 'sets KUBE_NAMESPACE' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
)
end
it 'sets KUBE_CA_PEM' do
expect(subject.predefined_variables).to include(
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
)
end
end
end
......@@ -1697,6 +1697,26 @@ describe Project, models: true do
end
end
describe '#deployment_variables' do
context 'when project has no deployment service' do
let(:project) { create(:empty_project) }
it 'returns an empty array' do
expect(project.deployment_variables).to eq []
end
end
context 'when project has a deployment service' do
let(:project) { create(:kubernetes_project) }
it 'returns variables from this service' do
expect(project.deployment_variables).to include(
{ key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
)
end
end
end
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
......
......@@ -5,7 +5,7 @@ describe API::API, api: true do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
describe "when unauthenticated" do
it "returns authentication success" do
......
require 'spec_helper'
describe API::Helpers, api: true do
include API::APIGuard::HelperMethods
include API::Helpers
include SentryHelper
......@@ -15,24 +16,24 @@ describe API::Helpers, api: true do
def set_env(user_or_token, identifier)
clear_env
clear_param
env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
env[API::Helpers::SUDO_HEADER] = identifier.to_s
end
def set_param(user_or_token, identifier)
clear_env
clear_param
params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
params[API::Helpers::SUDO_PARAM] = identifier.to_s
end
def clear_env
env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
env.delete(API::Helpers::SUDO_HEADER)
end
def clear_param
params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
params.delete(API::Helpers::SUDO_PARAM)
end
......@@ -94,22 +95,28 @@ describe API::Helpers, api: true do
describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
expect(current_user).to be_nil
end
it "returns nil for a user without access" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
it "leaves user as is when sudo not specified" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
expect(current_user).to eq(user)
clear_env
params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
expect(current_user).to eq(user)
end
end
......@@ -117,37 +124,51 @@ describe API::Helpers, api: true do
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
end
it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect(current_user).to be_nil
end
it "returns nil for a user without access" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
it "returns nil for a token without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_access_with_scope('write_user')
expect(current_user).to be_nil
end
it "leaves user as is when sudo not specified" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
clear_env
params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
expect(current_user).to eq(user)
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil
end
end
......
......@@ -230,7 +230,7 @@ describe 'Git HTTP requests', lib: true do
context "when an oauth token is provided" do
before do
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
@token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
@token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
end
it "downloads get status 200" do
......
......@@ -80,10 +80,6 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq')
end
it 'to #autocomplete_sources' do
expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq')
end
describe 'to #show' do
context 'regular name' do
it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') }
......@@ -117,6 +113,21 @@ describe 'project routing' do
end
end
# emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
# members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
# issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
# merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
# labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels
# milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
# commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
describe Projects::AutocompleteSourcesController, 'routing' do
[:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
it "to ##{action}" do
expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
end
# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
# history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history
# project_wikis POST /:project_id/wikis(.:format) projects/wikis#create
......
require 'spec_helper'
describe AccessTokenValidationService, services: true do
describe ".include_any_scope?" do
it "returns true if the required scope is present in the token's scopes" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token).include_any_scope?([:api])).to be(true)
end
it "returns true if more than one of the required scopes is present in the token's scopes" do
token = double("token", scopes: [:api, :read_user, :other_scope])
expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true)
end
it "returns true if the list of required scopes is an exact match for the token's scopes" do
token = double("token", scopes: [:api, :read_user, :other_scope])
expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
end
it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
end
it 'returns true if the list of required scopes is blank' do
token = double("token", scopes: [])
expect(described_class.new(token).include_any_scope?([])).to be(true)
end
it "returns false if there are no scopes in common between the required scopes and the token scopes" do
token = double("token", scopes: [:api, :read_user])
expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false)
end
end
end
FROM httpd:alpine
COPY ./ /usr/local/apache2/htdocs/
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment