Commit 435562ed authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream

parents 54e7f53a 87b01636
......@@ -179,7 +179,7 @@ gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration
gem 'slack-notifier', '~> 1.2.0'
gem 'slack-notifier', '~> 1.5.1'
# Asana integration
gem 'asana', '~> 0.4.0'
......
......@@ -707,7 +707,7 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.2.1)
slack-notifier (1.5.1)
slop (3.6.0)
spinach (0.8.10)
colorize
......@@ -985,7 +985,7 @@ DEPENDENCIES
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
slack-notifier (~> 1.2.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.7.0)
......
......@@ -144,6 +144,11 @@
case 'projects:merge_requests:builds':
new MergedButtons();
break;
case 'projects:merge_requests:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
......@@ -161,6 +166,11 @@
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:commit:builds':
new gl.Pipelines();
break;
......@@ -175,6 +185,11 @@
new TreeView();
}
break;
case 'projects:pipelines:index':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
......
......@@ -18,7 +18,7 @@
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmnetsByState`
* In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
......@@ -34,9 +34,9 @@
* @param {Array} array
* @return {Array}
*/
const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
if (item.children) {
const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
......@@ -76,12 +76,13 @@
helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
};
},
computed: {
filteredEnvironments() {
return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
},
scope() {
......@@ -102,7 +103,7 @@
},
/**
* Fetches all the environmnets and stores them.
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
......@@ -230,6 +231,7 @@
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0"
......@@ -240,6 +242,7 @@
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</tr>
......
......@@ -8,6 +8,7 @@
/*= require ./environment_external_url */
/*= require ./environment_stop */
/*= require ./environment_rollback */
/*= require ./environment_terminal_button */
(() => {
/**
......@@ -33,6 +34,7 @@
'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
'stop-component': window.gl.environmentsList.StopComponent,
'rollback-component': window.gl.environmentsList.RollbackComponent,
'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent,
},
props: {
......@@ -68,6 +70,12 @@
type: String,
required: false,
},
terminalIconSvg: {
type: String,
required: false,
},
},
data() {
......@@ -506,6 +514,14 @@
</stop-component>
</div>
<div v-if="model.terminal_path"
class="inline js-terminal-button-container">
<terminal-button-component
:terminal-icon-svg="terminalIconSvg"
:terminal-path="model.terminal_path">
</terminal-button-component>
</div>
<div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container">
<rollback-component
......
/*= require vue */
/* global Vue */
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
props: {
terminalPath: {
type: String,
default: '',
},
terminalIconSvg: {
type: String,
default: '',
},
},
template: `
<a class="btn terminal-button"
:href="terminalPath">
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a>
`,
});
})();
/* eslint-disable no-new */
/* global Flash */
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
*
* When we click in a pipeline stage, we need to make an API call to get the
* builds list to render in a dropdown.
*
* The container should be the table element.
*
* The stage icon clicked needs to have the following HTML structure:
* <div>
* <button class="dropdown js-builds-dropdown-button"></button>
* <div class="js-builds-dropdown-container"></div>
* </div>
*/
(() => {
class MiniPipelineGraph {
constructor(opts = {}) {
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
this.bindEvents();
}
/**
* Adds and removes the event listener.
*/
bindEvents() {
const dropdownButtonSelector = 'button.js-builds-dropdown-button';
$(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
.on('click', dropdownButtonSelector, this.getBuildsList);
}
/**
* For the clicked stage, renders the given data in the dropdown list.
*
* @param {HTMLElement} stageContainer
* @param {Object} data
*/
renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector(
`${this.dropdownListSelector} .js-builds-dropdown-list`,
);
dropdownContainer.innerHTML = data;
}
/**
* For the clicked stage, gets the list of builds.
*
* @param {Object} e
* @return {Promise}
*/
getBuildsList(e) {
const button = e.currentTarget;
const endpoint = button.dataset.stageEndpoint;
return $.ajax({
dataType: 'json',
type: 'GET',
url: endpoint,
beforeSend: () => {
this.renderBuildsList(button, '');
this.toggleLoading(button);
},
success: (data) => {
this.toggleLoading(button);
this.renderBuildsList(button, data.html);
},
error: () => {
this.toggleLoading(button);
new Flash('An error occurred while fetching the builds.', 'alert');
},
});
}
/**
* Toggles the visibility of the loading icon.
*
* @param {HTMLElement} stageContainer
* @return {type}
*/
toggleLoading(stageContainer) {
stageContainer.parentElement.querySelector(
`${this.dropdownListSelector} .js-builds-dropdown-loading`,
).classList.toggle('hidden');
}
}
window.gl = window.gl || {};
window.gl.MiniPipelineGraph = MiniPipelineGraph;
})();
......@@ -356,7 +356,7 @@
icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
nameText = this.text(x + 25, y + 10, commit.author.name);
idText = this.text(x, y + 35, commit.id);
messageText = this.text(x, y + 50, commit.message);
messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n "));
textSet = this.set(icon, nameText, idText, messageText).attr({
"text-anchor": "start",
font: "12px Monaco, monospace"
......@@ -368,6 +368,7 @@
idText.attr({
fill: "#AAA"
});
messageText.node.style["white-space"] = "pre";
this.textWrap(messageText, boxWidth - 50);
rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
fill: "#FFF",
......@@ -404,16 +405,21 @@
s.push("\n");
x = 0;
}
x += word.length * letterWidth;
if (word === "\n") {
s.push("\n");
x = 0;
} else {
s.push(word + " ");
x += word.length * letterWidth;
}
}
t.attr({
text: s.join("")
text: s.join("").trim()
});
b = t.getBBox();
h = Math.abs(b.y2) - Math.abs(b.y) + 1;
h = Math.abs(b.y2) + 1;
return t.attr({
y: b.y + h
y: h
});
};
......
/* global Terminal */
(() => {
class GLTerminal {
constructor(options) {
this.options = options || {};
this.options.cursorBlink = options.cursorBlink || true;
this.options.screenKeys = options.screenKeys || true;
this.container = document.querySelector(options.selector);
this.setSocketUrl();
this.createTerminal();
$(window).off('resize.terminal').on('resize.terminal', () => {
this.terminal.fit();
});
}
setSocketUrl() {
const { protocol, hostname, port } = window.location;
const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://';
const path = this.container.dataset.projectPath;
this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`;
}
createTerminal() {
this.terminal = new Terminal(this.options);
this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
this.socket.binaryType = 'arraybuffer';
this.terminal.open(this.container);
this.socket.onopen = () => { this.runTerminal(); };
this.socket.onerror = () => { this.handleSocketFailure(); };
}
runTerminal() {
const decoder = new TextDecoder('utf-8');
const encoder = new TextEncoder('utf-8');
this.terminal.on('data', (data) => {
this.socket.send(encoder.encode(data));
});
this.socket.addEventListener('message', (ev) => {
this.terminal.write(decoder.decode(ev.data));
});
this.isTerminalInitialized = true;
this.terminal.fit();
}
handleSocketFailure() {
this.terminal.write('\r\nConnection failure');
}
}
window.gl = window.gl || {};
gl.Terminal = GLTerminal;
})();
//= require xterm/xterm.js
//= require xterm/fit.js
//= require ./terminal.js
$(() => new gl.Terminal({ selector: '#terminal' }));
......@@ -89,7 +89,8 @@
U2FAuthenticate.prototype.renderError = function(error) {
this.renderTemplate('error', {
error_message: error.message()
error_message: error.message(),
error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
......
......@@ -9,7 +9,6 @@
this.errorCode = errorCode;
this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:';
console.error("U2F Error Code: " + this.errorCode);
}
U2FError.prototype.message = function() {
......
......@@ -76,7 +76,8 @@
U2FRegister.prototype.renderError = function(error) {
this.renderTemplate('error', {
error_message: error.message()
error_message: error.message(),
error_code: error.errorCode
});
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
};
......
......@@ -64,7 +64,7 @@
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 68px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 138px; }
......
......@@ -230,6 +230,13 @@
}
}
.btn-terminal {
svg {
height: 14px;
width: 18px;
}
}
.btn-lg {
padding: 12px 20px;
}
......
......@@ -96,6 +96,10 @@ label {
code {
line-height: 1.8;
}
img {
margin-right: $gl-padding;
}
}
@media(max-width: $screen-xs-max) {
......
......@@ -33,10 +33,12 @@ body {
}
.alert-wrapper {
margin-bottom: $gl-padding;
.alert {
margin-bottom: 0;
&:last-child {
margin-bottom: $gl-padding;
}
}
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
......
......@@ -22,17 +22,22 @@
.table.ci-table {
min-width: 1200px;
table-layout: fixed;
.pipeline-id {
color: $black;
}
.branch-commit {
width: 30%;
.branch-name {
max-width: 195px;
.pipeline-date,
.pipeline-status {
width: 10%;
}
.pipeline-info,
.pipeline-commit,
.pipeline-actions,
.pipeline-stages {
width: 20%;
}
}
}
......@@ -106,7 +111,7 @@
.branch-name {
font-weight: bold;
max-width: 150px;
max-width: 120px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
......@@ -132,7 +137,7 @@
.commit-title {
margin-top: 4px;
max-width: 300px;
max-width: 225px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
......@@ -192,10 +197,6 @@
border-bottom: 2px solid $border-color;
}
}
a {
display: block;
}
}
}
......@@ -462,6 +463,25 @@
white-space: normal;
color: $gl-text-color-light;
.dropdown-menu-toggle {
background-color: transparent;
border: none;
padding: 0;
color: $gl-text-color-light;
&:focus {
outline: none;
}
&:hover {
color: $gl-text-color;
.dropdown-counter-badge {
color: $gl-text-color;
}
}
}
> .build-content {
display: inline-block;
padding: 8px 10px 9px;
......@@ -527,7 +547,7 @@
content: '';
position: absolute;
top: 48%;
right: -49px;
right: -48px;
border-top: 2px solid $border-color;
width: 48px;
height: 1px;
......@@ -574,49 +594,17 @@
}
}
}
}
.ci-status-text {
max-width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
display: inline-block;
position: relative;
font-weight: 100;
}
.dropdown-menu-toggle {
background-color: transparent;
border: none;
padding: 0;
color: $gl-text-color-light;
white-space: normal;
overflow: visible;
&:focus {
outline: none;
}
&:hover {
color: $gl-text-color;
.dropdown-counter-badge {
color: $gl-text-color;
}
}
}
.dropdown-counter-badge {
.dropdown-counter-badge {
float: right;
clear: right;
color: $border-color;
font-weight: 100;
font-size: 15px;
margin-right: 2px;
}
}
.grouped-pipeline-dropdown {
.grouped-pipeline-dropdown {
padding: 0;
width: 191px;
left: auto;
......@@ -638,6 +626,7 @@
margin: 5px 0;
li {
padding-top: 2px;
margin: 0 5px;
padding-left: 0;
padding-bottom: 0;
......@@ -645,29 +634,63 @@
line-height: 1.2;
}
}
}
.dropdown-build {
color: $gl-text-color-light;
.ci-status-text {
max-width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
display: inline-block;
position: relative;
font-weight: 100;
}
.build-content {
width: 100%;
// Action Icons
.ci-action-icon-container .ci-action-icon-wrapper {
i {
color: $border-color;
border-radius: 100%;
border: 1px solid $border-color;
padding: 5px 6px;
font-size: 13px;
background: $white-light;
height: 30px;
width: 30px;
&::before {
position: relative;
top: 3px;
left: 3px;
}
&:hover {
color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-bg;
}
}
.ci-play-icon {
padding: 5px 5px 5px 7px;
}
}
.dropdown-build {
color: $gl-text-color-light;
.ci-action-icon-container {
padding: 0;
font-size: 11px;
position: absolute;
right: 4px;
float: right;
margin-top: 4px;
display: inline-block;
position: relative;
i {
width: 25px;
height: 25px;
font-size: 11px;
margin-top: 0;
&::before {
top: 1px;
left: 1px;
}
}
}
......@@ -677,6 +700,18 @@
color: $gl-text-color;
}
.ci-action-icon-container {
i {
width: 25px;
height: 25px;
&::before {
top: 1px;
left: 1px;
}
}
}
.stage {
max-width: 100px;
width: 100px;
......@@ -689,40 +724,169 @@
.ci-status-text {
max-width: 95px;
padding-bottom: 3px;
position: relative;
top: 3px;
}
}
/**
* Builds dropdown in mini pipeline
*/
.mini-pipeline-graph {
.builds-dropdown {
background-color: transparent;
border: none;
padding: 0;
color: $gl-text-color-light;
border: none;
margin: 0;
}
.builds-dropdown-loading {
margin: 10px auto;
width: 18px;
}
}
// Action Icons
.ci-action-icon-container .ci-action-icon-wrapper {
i {
color: $border-color;
border-radius: 100%;
border: 1px solid $border-color;
padding: 5px 6px;
font-size: 13px;
background: $white-light;
height: 30px;
width: 30px;
.grouped-pipeline-dropdown {
right: -172px;
top: 23px;
min-height: 50px;
a {
color: $gl-text-color-light;
}
}
.arrow-up {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 2px;
border-width: 0 5px 6px;
}
&::before {
border-width: 0 5px 5px;
border-bottom-color: $border-color;
}
&::after {
margin-top: 1px;
border-bottom-color: $white-light;
}
}
}
/**
* Icons in mini pipeline graph
*/
.mini-pipeline-graph-icon-container .ci-status-icon {
display: inline-block;
border: 1px solid;
border-radius: 20px;
margin-right: 1px;
width: 20px;
height: 20px;
position: relative;
top: 3px;
left: 3px;
z-index: 2;
transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
svg {
top: -1px;
}
}
&:hover {
color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-bg;
.builds-dropdown {
&:focus {
outline: none;
margin-right: -8px;
.ci-status-icon {
width: 28px;
padding: 0 8px 0 0;
transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
+ .dropdown-caret {
display: inline-block;
}
}
}
.ci-play-icon {
padding: 5px 5px 5px 7px;
&:focus,
&:active {
.ci-status-icon-success {
background-color: rgba($gl-success, .1);
}
.ci-status-icon-failed {
background-color: rgba($gl-danger, .1);
}
.ci-status-icon-pending,
.ci-status-icon-success_with_warnings {
background-color: rgba($gl-warning, .1);
}
.ci-status-icon-running {
background-color: rgba($blue-normal, .1);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
background-color: rgba($gl-gray, .1);
}
.ci-status-icon-created,
.ci-status-icon-skipped {
background-color: rgba($gray-darkest, .1);
}
}
.mini-pipeline-graph-icon-container {
.ci-status-icon:hover,
.ci-status-icon:focus {
width: 28px;
padding: 0 8px 0 0;
+ .dropdown-caret {
display: inline-block;
}
}
.dropdown-caret {
font-size: 11px;
position: relative;
top: 3px;
left: -11px;
margin-right: -6px;
display: none;
z-index: 2;
}
}
}
.terminal-icon {
margin-left: 3px;
}
.terminal-container {
.content-block {
border-bottom: none;
}
#terminal {
margin-top: 10px;
min-height: 450px;
box-sizing: border-box;
> div {
min-height: 450px;
}
}
}
......@@ -93,7 +93,6 @@
.group-avatar {
float: none;
margin: 0 auto;
border: none;
&.identicon {
border-radius: 50%;
......
......@@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
......@@ -249,6 +249,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('github')
end
def gitea_import_enabled?
current_application_settings.import_sources.include?('gitea')
end
def github_import_configured?
Gitlab::OAuth::Provider.enabled?(:github)
end
......
class Import::GiteaController < Import::GithubController
def new
if session[access_token_key].present? && session[host_key].present?
redirect_to status_import_url
end
end
def personal_access_token
session[host_key] = params[host_key]
super
end
def status
@gitea_host_url = session[host_key]
super
end
private
def host_key
:"#{provider}_host_url"
end
# Overriden methods
def provider
:gitea
end
# Gitea is not yet an OAuth provider
# See https://github.com/go-gitea/gitea/issues/27
def logged_in_with_provider?
false
end
def provider_auth
if session[access_token_key].blank? || session[host_key].blank?
redirect_to new_import_gitea_url,
alert: 'You need to specify both an Access Token and a Host URL.'
end
end
def client_options
{ host: session[host_key], api_version: 'v1' }
end
end
class Import::GithubController < Import::BaseController
before_action :verify_github_import_enabled
before_action :github_auth, only: [:status, :jobs, :create]
before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :jobs, :create]
rescue_from Octokit::Unauthorized, with: :github_unauthorized
helper_method :logged_in_with_github?
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
def new
if logged_in_with_github?
go_to_github_for_permissions
elsif session[:github_access_token]
redirect_to status_import_github_url
if logged_in_with_provider?
go_to_provider_for_permissions
elsif session[access_token_key]
redirect_to status_import_url
end
end
def callback
session[:github_access_token] = client.get_token(params[:code])
redirect_to status_import_github_url
session[access_token_key] = client.get_token(params[:code])
redirect_to status_import_url
end
def personal_access_token
session[:github_access_token] = params[:personal_access_token]
redirect_to status_import_github_url
session[access_token_key] = params[:personal_access_token]
redirect_to status_import_url
end
def status
@repos = client.repos
@already_added_projects = current_user.created_projects.where(import_type: "github")
@already_added_projects = current_user.created_projects.where(import_type: provider)
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject!{ |repo| already_added_projects_names.include? repo.full_name }
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
def jobs
jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status])
jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
render json: jobs
end
......@@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController
namespace_path = params[:target_namespace].presence || current_user.namespace_path
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if current_user.can?(:create_projects, @target_namespace)
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute
if can?(current_user, :create_projects, @target_namespace)
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else
render 'unauthorized'
end
......@@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController
private
def client
@client ||= Gitlab::GithubImport::Client.new(session[:github_access_token])
@client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
end
def verify_import_enabled
render_404 unless import_enabled?
end
def go_to_provider_for_permissions
redirect_to client.authorize_url(callback_import_url)
end
def verify_github_import_enabled
render_404 unless github_import_enabled?
def import_enabled?
__send__("#{provider}_import_enabled?")
end
def github_auth
if session[:github_access_token].blank?
go_to_github_for_permissions
def new_import_url
public_send("new_import_#{provider}_url")
end
def status_import_url
public_send("status_import_#{provider}_url")
end
def go_to_github_for_permissions
redirect_to client.authorize_url(callback_import_github_url)
def callback_import_url
public_send("callback_import_#{provider}_url")
end
def github_unauthorized
session[:github_access_token] = nil
redirect_to new_import_github_url,
alert: 'Access denied to your GitHub account.'
def provider_unauthorized
session[access_token_key] = nil
redirect_to new_import_url,
alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account."
end
def logged_in_with_github?
current_user.identities.exists?(provider: 'github')
def access_token_key
:"#{provider}_access_token"
end
def access_params
{ github_access_token: session[:github_access_token] }
{ github_access_token: session[access_token_key] }
end
# The following methods are overriden in subclasses
def provider
:github
end
def logged_in_with_provider?
current_user.identities.exists?(provider: provider)
end
def provider_auth
if session[access_token_key].blank?
go_to_provider_for_permissions
end
end
def client_options
{}
end
end
......@@ -4,7 +4,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
before_action :environment, only: [:show, :edit, :update, :stop]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
@scope = params[:scope]
......@@ -14,7 +16,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.html
format.json do
render json: EnvironmentSerializer
.new(project: @project)
.new(project: @project, user: current_user)
.represent(@environments)
end
end
......@@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end
def terminal
# Currently, this acts as a hint to load the terminal details into the cache
# if they aren't there already. In the future, users will need these details
# to choose between terminals to connect to.
@terminals = environment.terminals
end
# GET .../terminal.ws : implemented in gitlab-workhorse
def terminal_websocket_authorize
# Just return the first terminal for now. If the list is in the process of
# being looked up, this may result in a 404 response, so the frontend
# should retry those errors
terminal = environment.terminals.try(:first)
if terminal
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
else
render text: 'Not found', status: 404
end
end
private
def verify_api_request!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
def environment_params
params.require(:environment).permit(:name, :external_url)
end
......
......@@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
@pipelines = @pipelines.includes(project: :namespace)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder.new(project).execute.count
......@@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def stage
@stage = pipeline.stage(params[:stage])
return not_found unless @stage
respond_to do |format|
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
end
end
def retry
pipeline.retry_failed(current_user)
......
......@@ -128,50 +128,11 @@ module CommitsHelper
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
if can_collaborate_with_project?
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(@project.namespace, @project,
namespace_key: current_user.namespace.id,
continue: continue_params)
btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request"
if can_collaborate_with_project?
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(@project.namespace, @project,
namespace_key: current_user.namespace.id,
continue: continue_params)
btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
protected
......@@ -211,6 +172,28 @@ module CommitsHelper
end
end
def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
if can_collaborate_with_project?
link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(@project.namespace, @project,
namespace_key: current_user.namespace.id,
continue: continue_params)
link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
end
def view_file_btn(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
......
......@@ -4,8 +4,10 @@ module ImportHelper
"#{namespace}/#{name}"
end
def github_project_link(path_with_namespace)
link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace)
link_to path_with_namespace, url, target: '_blank'
end
private
......@@ -20,4 +22,8 @@ module ImportHelper
provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
@github_url = provider.fetch('url', 'https://github.com') if provider
end
def gitea_project_url(path_with_namespace)
"#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
end
end
......@@ -116,6 +116,11 @@ module Ci
where.not(duration: nil).sum(:duration)
end
def stage(name)
stage = Ci::Stage.new(self, name: name)
stage unless stage.statuses_count.zero?
end
def stages_count
statuses.select(:stage).distinct.count
end
......
......@@ -18,6 +18,10 @@ module Ci
name
end
def statuses_count
@statuses_count ||= statuses.count
end
def status
@status ||= statuses.latest.status
end
......
module Milestoneish
def closed_items_count(user)
issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
memoize_per_user(user, :closed_items_count) do
(count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size
end
end
def total_items_count(user)
issues_visible_to_user(user).size + merge_requests.size
memoize_per_user(user, :total_items_count) do
issues_count = count_issues_by_state(user).values.sum
issues_count + merge_requests.size
end
end
def complete?(user)
......@@ -30,7 +35,10 @@ module Milestoneish
end
def issues_visible_to_user(user)
IssuesFinder.new(user).execute.where(id: issues)
memoize_per_user(user, :issues_visible_to_user) do
params = try(:project_id) ? { project_id: project_id } : {}
IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids)
end
end
def upcoming?
......@@ -50,4 +58,18 @@ module Milestoneish
def expired?
due_date && due_date.past?
end
private
def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count
end
end
def memoize_per_user(user, method_name)
@memoized ||= {}
@memoized[method_name] ||= {}
@memoized[method_name][user.try!(:id)] ||= yield
end
end
# The ReactiveCaching concern is used to fetch some data in the background and
# store it in the Rails cache, keeping it up-to-date for as long as it is being
# requested. If the data hasn't been requested for +reactive_cache_lifetime+,
# it stop being refreshed, and then be removed.
#
# Example of use:
#
# class Foo < ActiveRecord::Base
# include ReactiveCaching
#
# self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
#
# after_save :clear_reactive_cache!
#
# def calculate_reactive_cache
# # Expensive operation here. The return value of this method is cached
# end
#
# def result
# with_reactive_cache do |data|
# # ...
# end
# end
# end
#
# In this example, the first time `#result` is called, it will return `nil`.
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes.
#
# Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
# Calculations are never run concurrently.
#
# Calling `#result` while a value is in the cache will call the block given to
# `#with_reactive_cache`, yielding the cached value. It will also extend the
# lifetime by `reactive_cache_lifetime`.
#
# Once the lifetime has expired, no more background jobs will be enqueued and
# calling `#result` will again return `nil` - starting the process all over
# again
module ReactiveCaching
extend ActiveSupport::Concern
included do
class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_key
class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_refresh_interval
# defaults
self.reactive_cache_lease_timeout = 2.minutes
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
def calculate_reactive_cache
raise NotImplementedError
end
def with_reactive_cache(&blk)
within_reactive_cache_lifetime do
data = Rails.cache.read(full_reactive_cache_key)
yield data if data.present?
end
ensure
Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime)
ReactiveCachingWorker.perform_async(self.class, id)
end
def clear_reactive_cache!
Rails.cache.delete(full_reactive_cache_key)
end
def exclusively_update_reactive_cache!
locking_reactive_cache do
within_reactive_cache_lifetime do
enqueuing_update do
value = calculate_reactive_cache
Rails.cache.write(full_reactive_cache_key, value)
end
end
end
end
private
def full_reactive_cache_key(*qualifiers)
prefix = self.class.reactive_cache_key
prefix = prefix.call(self) if prefix.respond_to?(:call)
([prefix].flatten + qualifiers).join(':')
end
def locking_reactive_cache
lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout)
uuid = lease.try_obtain
yield if uuid
ensure
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid)
end
def within_reactive_cache_lifetime
yield if Rails.cache.read(full_reactive_cache_key('alive'))
end
def enqueuing_update
yield
ensure
ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id)
end
end
end
......@@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base
end
end
def has_terminals?
project.deployment_service.present? && available? && last_deployment.present?
end
def terminals
project.deployment_service.terminals(self) if has_terminals?
end
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
......
......@@ -24,12 +24,16 @@ class GlobalMilestone
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
def milestoneish_ids
milestones.select(:id)
end
def safe_title
@title.to_slug.normalize.to_s
end
def projects
@projects ||= Project.for_milestones(milestones.select(:id))
@projects ||= Project.for_milestones(milestoneish_ids)
end
def state
......@@ -49,11 +53,11 @@ class GlobalMilestone
end
def issues
@issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
@issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
end
def merge_requests
@merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
@merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
end
def participants
......
......@@ -47,6 +47,8 @@ class Issue < ActiveRecord::Base
scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
......
......@@ -130,6 +130,10 @@ class Milestone < ActiveRecord::Base
self.title
end
def milestoneish_ids
id
end
def can_be_closed?
active? && issues.opened.count.zero?
end
......
......@@ -72,8 +72,7 @@ class Project < ActiveRecord::Base
has_one :push_rule, dependent: :destroy
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, dependent: :destroy
has_many :chat_services
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
# Project services
has_one :campfire_service, dependent: :destroy
......@@ -93,6 +92,7 @@ class Project < ActiveRecord::Base
has_one :slack_service, dependent: :destroy
has_one :jenkins_service, dependent: :destroy
has_one :jenkins_deprecated_service, dependent: :destroy
has_one :slack_slash_commands_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
......@@ -629,6 +629,10 @@ class Project < ActiveRecord::Base
import_type == 'gitlab_project'
end
def gitea_import?
import_type == 'gitea'
end
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit
......
......@@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base
validates :project, presence: true
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
def self.insert_authorizations(rows, per_batch = 1000)
rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple|
tuple.map { |value| connection.quote(value) }
end
connection.execute <<-EOF.strip_heredoc
INSERT INTO project_authorizations (user_id, project_id, access_level)
VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
EOF
end
end
end
# Base class for Chat services
# This class is not meant to be used directly, but only to inherit from.
class ChatService < Service
# This class is not meant to be used directly, but only to inherrit from.
class ChatSlashCommandsService < Service
default_value_for :category, 'chat'
has_many :chat_names, foreign_key: :service_id
prop_accessor :token
has_many :chat_names, foreign_key: :service_id, dependent: :destroy
def valid_token?(token)
self.respond_to?(:token) &&
......@@ -15,7 +17,40 @@ class ChatService < Service
[]
end
def can_test?
false
end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
]
end
def trigger(params)
raise NotImplementedError
return unless valid_token?(params[:token])
user = find_chat_user(params)
unless user
url = authorize_chat_name_url(params)
return presenter.authorize_chat_name(url)
end
Gitlab::ChatCommands::Command.new(project, user,
params).execute
end
private
def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute
end
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end
......@@ -12,4 +12,22 @@ class DeploymentService < Service
def predefined_variables
[]
end
# Environments may have a number of terminals. Should return an array of
# hashes describing them, e.g.:
#
# [{
# :selectors => {"a" => "b", "foo" => "bar"},
# :url => "wss://external.example.com/exec",
# :headers => {"Authorization" => "Token xxx"},
# :subprotocols => ["foo"],
# :ca_pem => "----BEGIN CERTIFICATE...", # optional
# :created_at => Time.now.utc
# }]
#
# Selectors should be a set of values that uniquely identify a particular
# terminal
def terminals(environment)
raise NotImplementedError
end
end
class KubernetesService < DeploymentService
include Gitlab::Kubernetes
include ReactiveCaching
self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
# Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name
prop_accessor :namespace
......@@ -25,6 +30,8 @@ class KubernetesService < DeploymentService
length: 1..63
end
after_save :clear_reactive_cache!
def initialize_properties
if properties.nil?
self.properties = {}
......@@ -41,7 +48,8 @@ class KubernetesService < DeploymentService
end
def help
''
'To enable terminal access to Kubernetes environments, label your ' \
'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end
def to_param
......@@ -75,9 +83,9 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kubeclient
kubeclient.discover
kubeclient = build_kubeclient!
kubeclient.discover
{ success: kubeclient.discovered, result: "Checked API discovery endpoint" }
rescue => err
{ success: false, result: err }
......@@ -93,20 +101,48 @@ class KubernetesService < DeploymentService
variables
end
private
# Constructs a list of terminals from the reactive cache
#
# Returns nil if the cache is empty, in which case you should try again a
# short time later
def terminals(environment)
with_reactive_cache do |data|
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
end
end
def build_kubeclient(api_path = '/api', api_version = 'v1')
return nil unless api_url && namespace && token
# Caches all pods in the namespace so other calls don't need to block on
# network access.
def calculate_reactive_cache
return unless active? && project && !project.pending_delete?
url = URI.parse(api_url)
url.path = url.path[0..-2] if url.path[-1] == "/"
url.path += api_path
kubeclient = build_kubeclient!
# Store as hashes, rather than as third-party types
pods = begin
kubeclient.get_pods(namespace: namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
[]
end
# We may want to cache extra things in the future
{ pods: pods }
end
private
def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token
::Kubeclient::Client.new(
url,
join_api_url(api_path),
api_version,
ssl_options: kubeclient_ssl_options,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
)
end
......@@ -125,4 +161,13 @@ class KubernetesService < DeploymentService
def kubeclient_auth_options
{ bearer_token: token }
end
def join_api_url(*parts)
url = URI.parse(api_url)
prefix = url.path.sub(%r{/+\z}, '')
url.path = [ prefix, *parts ].join("/")
url.to_s
end
end
class MattermostSlashCommandsService < ChatService
class MattermostSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
prop_accessor :token
......@@ -18,32 +18,4 @@ class MattermostSlashCommandsService < ChatService
def to_param
'mattermost_slash_commands'
end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
]
end
def trigger(params)
return nil unless valid_token?(params[:token])
user = find_chat_user(params)
unless user
url = authorize_chat_name_url(params)
return Mattermost::Presenter.authorize_chat_name(url)
end
Gitlab::ChatCommands::Command.new(project, user, params).execute
end
private
def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute
end
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
end
class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
'Slack Command'
end
def description
"Perform common operations on GitLab in Slack"
end
def to_param
'slack_slash_commands'
end
def trigger(params)
# Format messages to be Slack-compatible
super.tap do |result|
result[:text] = format(result[:text])
end
end
private
def format(text)
Slack::Notifier::LinkFormatter.format(text) if text
end
end
......@@ -218,11 +218,13 @@ class Service < ActiveRecord::Base
jira
kubernetes
mattermost_slash_commands
mattermost
pipelines_email
pivotaltracker
pushover
redmine
mattermost
slack_slash_commands
slack
teamcity
]
......
......@@ -328,6 +328,7 @@ class User < ActiveRecord::Base
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end
<<<<<<< HEAD
def build_user(attrs = {})
User.new(attrs)
end
......@@ -353,6 +354,8 @@ class User < ActiveRecord::Base
username
end
=======
>>>>>>> 87b01636e4baf3ef58d83139129956e7eac39d1f
def reference_prefix
'@'
end
......@@ -481,24 +484,18 @@ class User < ActiveRecord::Base
end
def refresh_authorized_projects
transaction do
project_authorizations.delete_all
# project_authorizations_union can return multiple records for the same
# project/user with different access_level so we take row with the maximum
# access_level
project_authorizations.connection.execute <<-SQL
INSERT INTO project_authorizations (user_id, project_id, access_level)
SELECT user_id, project_id, MAX(access_level) AS access_level
FROM (#{project_authorizations_union.to_sql}) sub
GROUP BY user_id, project_id
SQL
Users::RefreshAuthorizedProjectsService.new(self).execute
end
def remove_project_authorizations(project_ids)
project_authorizations.where(id: project_ids).delete_all
end
def set_authorized_projects_column
unless authorized_projects_populated
update_column(:authorized_projects_populated, true)
end
end
end
def authorized_projects(min_access_level = nil)
refresh_authorized_projects unless authorized_projects_populated
......@@ -951,18 +948,6 @@ class User < ActiveRecord::Base
private
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
groups_projects.select_for_project_authorization,
projects.select_for_project_authorization,
groups.joins(:shared_projects).select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
end
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
......
......@@ -23,5 +23,13 @@ class EnvironmentEntity < Grape::Entity
environment)
end
expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment|
can?(request.user, :admin_environment, environment.project) &&
terminal_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
end
expose :created_at, :updated_at
end
......@@ -8,4 +8,8 @@ module RequestAwareEntity
def request
@options.fetch(:request)
end
def can?(object, action, subject)
Ability.allowed?(object, action, subject)
end
end
......@@ -4,15 +4,6 @@ module Projects
class Error < StandardError; end
ALLOWED_TYPES = [
'bitbucket',
'fogbugz',
'gitlab',
'github',
'google_code',
'gitlab_project'
]
def execute
add_repository_to_project unless project.gitlab_project_import?
......@@ -64,14 +55,11 @@ module Projects
end
def has_importer?
ALLOWED_TYPES.include?(project.import_type)
Gitlab::ImportSources.importer_names.include?(project.import_type)
end
def importer
return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project)
Gitlab::ImportSources.importer(project.import_type).new(project)
end
def unknown_url?
......
......@@ -197,7 +197,7 @@ module SystemNoteService
end
def remove_merge_request_wip(noteable, project, author)
body = 'unmarked as a Work In Progress'
body = 'unmarked as a **Work In Progress**'
create_note(noteable: noteable, project: project, author: author, note: body)
end
......
module Users
# Service for refreshing the authorized projects of a user.
#
# This particular service class can not be used to update data for the same
# user concurrently. Doing so could lead to an incorrect state. To ensure this
# doesn't happen a caller must synchronize access (e.g. using
# `Gitlab::ExclusiveLease`).
#
# Usage:
#
# user = User.find_by(username: 'alice')
# service = Users::RefreshAuthorizedProjectsService.new(some_user)
# service.execute
class RefreshAuthorizedProjectsService
attr_reader :user
LEASE_TIMEOUT = 1.minute.to_i
# user - The User for which to refresh the authorized projects.
def initialize(user)
@user = user
# We need an up to date User object that has access to all relations that
# may have been created earlier. The only way to ensure this is to reload
# the User object.
user.reload
end
# This method returns the updated User object.
def execute
current = current_authorizations_per_project
fresh = fresh_access_levels_per_project
remove = current.each_with_object([]) do |(project_id, row), array|
# rows not in the new list or with a different access level should be
# removed.
if !fresh[project_id] || fresh[project_id] != row.access_level
array << row.id
end
end
add = fresh.each_with_object([]) do |(project_id, level), array|
# rows not in the old list or with a different access level should be
# added.
if !current[project_id] || current[project_id].access_level != level
array << [user.id, project_id, level]
end
end
update_with_lease(remove, add)
end
# Updates the list of authorizations using an exclusive lease.
def update_with_lease(remove = [], add = [])
lease_key = "refresh_authorized_projects:#{user.id}"
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
until uuid = lease.try_obtain
# Keep trying until we obtain the lease. If we don't do so we may end up
# not updating the list of authorized projects properly. To prevent
# hammering Redis too much we'll wait for a bit between retries.
sleep(1)
end
begin
update_authorizations(remove, add)
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
end
# Updates the list of authorizations for the current user.
#
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = [])
return if remove.empty? && add.empty?
User.transaction do
user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty?
user.set_authorized_projects_column
end
# Since we batch insert authorization rows, Rails' associations may get
# out of sync. As such we force a reload of the User object.
user.reload
end
def fresh_access_levels_per_project
fresh_authorizations.each_with_object({}) do |row, hash|
hash[row.project_id] = row.access_level
end
end
def current_authorizations_per_project
current_authorizations.each_with_object({}) do |row, hash|
hash[row.project_id] = row
end
end
def current_authorizations
user.project_authorizations.select(:id, :project_id, :access_level)
end
def fresh_authorizations
ProjectAuthorization.
unscoped.
select('project_id, MAX(access_level) AS access_level').
from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
group(:project_id)
end
private
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
user.groups_projects.select_for_project_authorization,
user.projects.select_for_project_authorization,
user.groups.joins(:shared_projects).select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
end
end
end
......@@ -3,18 +3,18 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status.group}"
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
= link_to status.details_path, class: 'build-content' do
= link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do
%span{ class: klass }= custom_icon(status.icon)
.ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
.ci-status-text= subject.name
- else
.build-content
.build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } }
%span{ class: klass }= custom_icon(status.icon)
.ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name
.ci-status-text= subject.name
- if status.has_action?
= link_to status.action_path, method: status.action_method,
title: status.action_title, class: 'ci-action-icon-container' do
= link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
%i.ci-action-icon-wrapper
= icon(status.action_icon, class: status.action_class)
- provider = local_assigns.fetch(:provider)
- provider_title = Gitlab::ImportSources.title(provider)
%p.light
Select projects you want to import.
%hr
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th= "From #{provider_title}"
%th To GitLab
%th Status
%tbody
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
= provider_project_link(provider, project.import_source)
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
%i.fa.fa-check
done
- elsif project.import_status == 'started'
%i.fa.fa-spinner.fa-spin
started
- else
= project.human_import_status_name
- @repos.each do |repo|
%tr{id: "repo_#{repo.id}"}
%td
= provider_project_link(provider, repo.full_name)
%td.import-target
%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.login, path: repo.owner.login) } : {}
= 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
Import
= icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } }
- page_title "Gitea Import"
- header_title "Projects", root_path
%h3.page-title
= custom_icon('go_logo')
Import Projects from Gitea
%p
To get started, please enter your Gitea Host URL and a
= succeed '.' do
= link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token'
= form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do
.form-group
= label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label'
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
.form-group
= label_tag :personal_access_token, 'Personal Access Token', class: 'control-label'
.col-sm-4
= text_field_tag :personal_access_token, nil, class: 'form-control'
.form-actions
= submit_tag 'List Your Gitea Repositories', class: 'btn btn-create'
- page_title "Gitea Import"
- header_title "Projects", root_path
%h3.page-title
= custom_icon('go_logo')
Import Projects from Gitea
= render 'import/githubish_status', provider: 'gitea'
- page_title "GitHub import"
- page_title "GitHub Import"
- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-github
Import projects from GitHub
= icon 'github', text: 'Import Projects from GitHub'
%p.light
Select projects you want to import.
%hr
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From GitHub
%th To GitLab
%th Status
%tbody
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
= github_project_link(project.import_source)
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
%i.fa.fa-check
done
- elsif project.import_status == 'started'
%i.fa.fa-spinner.fa-spin
started
- else
= project.human_import_status_name
- @repos.each do |repo|
%tr{id: "repo_#{repo.id}"}
%td
= github_project_link(repo.full_name)
%td.import-target
%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.login, path: repo.owner.login) } : {}
= 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
Import
= icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } }
= render 'import/githubish_status', provider: 'github'
......@@ -43,10 +43,25 @@
%td.stage-cell
- pipeline.stages.each do |stage|
- if stage.status
- tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}"
.stage-container
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do
= ci_icon_for_status(stage.status)
- detailed_status = stage.detailed_status(current_user)
- icon_status = "#{detailed_status.icon}_borderless"
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.mini-pipeline-graph
.dropdown.inline.build-content
%button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}}
%span.has-tooltip{ class: status_klass }
%span.mini-pipeline-graph-icon-container
%span{ class: status_klass }= custom_icon(icon_status)
= icon('caret-down', class: 'dropdown-caret')
.js-builds-dropdown-container
.dropdown-menu.grouped-pipeline-dropdown
.arrow-up
.js-builds-dropdown-list
.js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
%td
- if pipeline.duration
......@@ -66,7 +81,7 @@
.btn-group.inline
- if actions.any?
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
%a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
......@@ -77,7 +92,7 @@
%span= build.name.humanize
- if artifacts.present?
.btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
%a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
......
......@@ -4,12 +4,12 @@
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.ci-table
%tbody
%th Status
%th Pipeline
%th Commit
%th Stages
%th
%th
%table.table.ci-table.js-pipeline-table
%thead
%th.pipeline-status Status
%th.pipeline-info Pipeline
%th.pipeline-commit Commit
%th.pipeline-stages Stages
%th.pipeline-date
%th.pipeline-actions
= render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
= link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do
= icon('terminal')
......@@ -4,10 +4,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag("environments/environments_bundle.js")
.commit-icon-svg.hidden
= custom_icon("icon_commit")
.play-icon-svg.hidden
= custom_icon("icon_play")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
......@@ -19,4 +15,5 @@
"help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play")}}
......@@ -8,6 +8,7 @@
%h3.page-title= @environment.name.capitalize
.col-md-3
.nav-controls
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
......
- @no_container = true
- page_title "Terminal for environment", @environment.name
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
= page_specific_javascript_tag("terminal/terminal_bundle.js")
%div{class: container_class}
.top-area
.row
.col-sm-6
%h3.page-title
Terminal for environment
= @environment.name
.col-sm-6
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{class: container_class}
#terminal{data:{project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws"}}
......@@ -5,10 +5,10 @@
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.clearfix.merged-buttons
- if can_remove_source_branch
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
= revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
= revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning")
- if mr_can_be_cherry_picked
= cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
= cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
......@@ -68,6 +68,11 @@
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
......
%ul
- @stage.statuses.each do |status|
%li.dropdown-build
= render 'ci/status/graph_badge', subject: status
......@@ -42,14 +42,14 @@
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.ci-table
%table.table.ci-table.js-pipeline-table
%thead
%th Status
%th Pipeline
%th Commit
%th Stages
%th
%th.hidden-xs
%th.pipeline-status Status
%th.pipeline-info Pipeline
%th.pipeline-commit Commit
%th.pipeline-stages Stages
%th.pipeline-date
%th.pipeline-actions.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true
= paginate @pipelines, theme: 'gitlab'
- 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
project by entering slash commands in Slack.
%br
See list of available commands in Slack after setting up this service,
by entering
%code /&lt;command&gt; help
%br
%br
To setup this service:
%ul.list-unstyled
%li
1.
= link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
in your Slack team with these options:
%hr
.help-form
.form-group
= label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
.form-group
= label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#url')
.form-group
= label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST
.form-group
= label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#customize_name')
.form-group
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= image_tag(asset_url('gitlab_logo.png'), width: 36, height: 36)
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description')
.form-group
= label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_usage_hint')
.form-group
= label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#descriptive_label')
%hr
%ul.list-unstyled
%li
2. Paste the
%strong Token
into the field below
%li
3. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Slack!
......@@ -8,7 +8,7 @@
= render 'shared/empty_states/icons/issues.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- if has_button
- if has_button && current_user
%h4
The Issue Tracker is a good place to add things that need to be improved or solved in a project!
%p
......
<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"><g fill-rule="evenodd" transform="translate(0 1)"><path d="m14 15.01h1v-8.02c0-3.862-3.134-6.991-7-6.991-3.858 0-7 3.13-7 6.991v8.02h1v-8.02c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02m-10.52-13.354c-.366-.402-.894-.655-1.48-.655-1.105 0-2 .895-2 2 0 .868.552 1.606 1.325 1.883.102-.321.226-.631.371-.93-.403-.129-.695-.507-.695-.953 0-.552.448-1 1-1 .306 0 .58.138.764.354.222-.25.461-.483.717-.699m9.04-.002c.366-.401.893-.653 1.479-.653 1.105 0 2 .895 2 2 0 .867-.552 1.606-1.324 1.883-.101-.321-.225-.632-.37-.931.403-.129.694-.507.694-.952 0-.552-.448-1-1-1-.305 0-.579.137-.762.353-.222-.25-.461-.483-.717-.699"/><path d="m5.726 7.04h1.557v.124c0 .283-.033.534-.1.752-.065.202-.175.391-.33.566-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571-.376-.382-.564-.841-.564-1.377 0-.547.191-1.01.574-1.391.382-.382.848-.574 1.396-.574.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367.387-.381.853-.571 1.396-.571.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379-.389.379-.858.569-1.408.569-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01-.267-.273-.597-.41-.991-.41-.392 0-.723.137-.993.41-.27.27-.405.604-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5 0-.276-.448-.5-1-.5-.552 0-1 .224-1 .5 0 .276.448.5 1 .5"/></g></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg>
<svg width="19" height="14" viewBox="0 0 19 14" xmlns="http://www.w3.org/2000/svg"><rect fill="#848484" x="7.2" y="9.25" width="6.46" height="1.5" rx=".5"/><path d="M5.851 7.016L3.81 9.103a.503.503 0 0 0 .017.709l.35.334c.207.198.524.191.717-.006l2.687-2.748a.493.493 0 0 0 .137-.376.493.493 0 0 0-.137-.376L4.894 3.892a.507.507 0 0 0-.717-.006l-.35.334a.503.503 0 0 0-.017.709L5.85 7.016z"/><path d="M1.25 11.497c0 .691.562 1.253 1.253 1.253h13.994c.694 0 1.253-.56 1.253-1.253V2.503c0-.691-.562-1.253-1.253-1.253H2.503c-.694 0-1.253.56-1.253 1.253v8.994zM2.503 0h13.994A2.504 2.504 0 0 1 19 2.503v8.994A2.501 2.501 0 0 1 16.497 14H2.503A2.504 2.504 0 0 1 0 11.497V2.503A2.501 2.501 0 0 1 2.503 0z"/></svg>
......@@ -21,7 +21,7 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues
= render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
= render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
......
......@@ -13,7 +13,7 @@
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
%p <%= error_message %>
%p <%= error_message %> (error code: <%= error_code %>)
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
......
......@@ -23,7 +23,7 @@
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
%span <%= error_message %>
%span <%= error_message %> (error code: <%= error_code %>)
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
......
......@@ -2,8 +2,6 @@ class AuthorizedProjectsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
LEASE_TIMEOUT = 1.minute.to_i
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end
......@@ -11,24 +9,6 @@ class AuthorizedProjectsWorker
def perform(user_id)
user = User.find_by(id: user_id)
refresh(user) if user
end
def refresh(user)
lease_key = "refresh_authorized_projects:#{user.id}"
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
until uuid = lease.try_obtain
# Keep trying until we obtain the lease. If we don't do so we may end up
# not updating the list of authorized projects properly. To prevent
# hammering Redis too much we'll wait for a bit between retries.
sleep(1)
end
begin
user.refresh_authorized_projects
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
user.refresh_authorized_projects if user
end
end
class ReactiveCachingWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(class_name, id)
klass = begin
Kernel.const_get(class_name)
rescue NameError
nil
end
return unless klass
klass.find_by(id: id).try(:exclusively_update_reactive_cache!)
end
end
......@@ -84,12 +84,15 @@ class ChangelogEntry
end
end
private
def contents
YAML.dump(
yaml_content = YAML.dump(
'title' => title,
'merge_request' => options.merge_request,
'author' => options.author
)
remove_trailing_whitespace(yaml_content)
end
def write
......@@ -101,8 +104,6 @@ class ChangelogEntry
exec("git commit --amend")
end
private
def fail_with(message)
$stderr.puts "\e[31merror\e[0m #{message}"
exit 1
......@@ -160,6 +161,10 @@ class ChangelogEntry
def branch_name
@branch_name ||= %x{git symbolic-ref --short HEAD}.strip
end
def remove_trailing_whitespace(yaml_content)
yaml_content.gsub(/ +$/, '')
end
end
if $0 == __FILE__
......
---
title: Adds Direct link from pipeline list to builds
merge_request: 8097
author:
---
title: New Gitea importer
merge_request: 8116
author:
---
title: Hides new issue button for non loggedin user
merge_request: 8175
author:
---
title: remove build_user
merge_request: 8162
author: Arsenev Vladislav
---
title: Allow public access to some Project API endpoints
title: Allow unauthenticated access to some Project API GET endpoints
merge_request: 7843
author:
---
title: Allow unauthenticated access to Repositories Files API GET endpoints
merge_request:
author:
---
title: Allow unauthenticated access to Repositories API GET endpoints
merge_request: 8148
author:
---
title: Fix project import label priorities error
merge_request:
author:
---
title: Fix N+1 queries on milestone show pages
merge_request: 8185
author:
---
title: Milestoneish SQL performance partially improved and memoized
merge_request: 8146
author:
---
title: Add online terminal support for Kubernetes
merge_request: 7690
author:
---
title: Remove trailing whitespace when generating changelog entry
merge_request: 7948
author:
---
title: Display error code for U2F errors
merge_request: 7305
author: winniehell
---
title: Refactor presenters ChatCommands
merge_request: 7846
author:
......@@ -93,6 +93,7 @@ module Gitlab
config.assets.precompile << "lib/vue_resource.js"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
......@@ -107,6 +108,7 @@ module Gitlab
config.assets.precompile << "environments/environments_bundle.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "terminal/terminal_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
......
......@@ -261,7 +261,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea]
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
......
......@@ -6,6 +6,12 @@ namespace :import do
get :jobs
end
resource :gitea, only: [:create, :new], controller: :gitea do
post :personal_access_token
get :status
get :jobs
end
resource :gitlab, only: [:create], controller: :gitlab do
get :status
get :callback
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment