Commit 3c61b13e authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into zj-mattermost-slash-config

parents dec1e90e 2bc3084d
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
* The environments array is a recursive tree structure and we need to filter * The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments. * 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. * functions work together.
* The first one works as the filter that verifies if the given environment matches * The first one works as the filter that verifies if the given environment matches
* the given state. * the given state.
...@@ -34,9 +34,9 @@ ...@@ -34,9 +34,9 @@
* @param {Array} array * @param {Array} array
* @return {Array} * @return {Array}
*/ */
const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => { const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
if (item.children) { if (item.children) {
const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean); const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
if (filteredChildren.length) { if (filteredChildren.length) {
item.children = filteredChildren; item.children = filteredChildren;
return item; return item;
...@@ -76,12 +76,13 @@ ...@@ -76,12 +76,13 @@
helpPagePath: environmentsData.helpPagePath, helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg, commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg, playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
}; };
}, },
computed: { computed: {
filteredEnvironments() { filteredEnvironments() {
return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments); return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
}, },
scope() { scope() {
...@@ -102,7 +103,7 @@ ...@@ -102,7 +103,7 @@
}, },
/** /**
* Fetches all the environmnets and stores them. * Fetches all the environments and stores them.
* Toggles loading property. * Toggles loading property.
*/ */
created() { created() {
...@@ -230,6 +231,7 @@ ...@@ -230,6 +231,7 @@
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg" :play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr> :commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0" <tr v-if="model.isOpen && model.children && model.children.length > 0"
...@@ -240,6 +242,7 @@ ...@@ -240,6 +242,7 @@
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg" :play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"> :commit-icon-svg="commitIconSvg">
</tr> </tr>
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
/*= require ./environment_external_url */ /*= require ./environment_external_url */
/*= require ./environment_stop */ /*= require ./environment_stop */
/*= require ./environment_rollback */ /*= require ./environment_rollback */
/*= require ./environment_terminal_button */
(() => { (() => {
/** /**
...@@ -33,6 +34,7 @@ ...@@ -33,6 +34,7 @@
'external-url-component': window.gl.environmentsList.ExternalUrlComponent, 'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
'stop-component': window.gl.environmentsList.StopComponent, 'stop-component': window.gl.environmentsList.StopComponent,
'rollback-component': window.gl.environmentsList.RollbackComponent, 'rollback-component': window.gl.environmentsList.RollbackComponent,
'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent,
}, },
props: { props: {
...@@ -68,6 +70,12 @@ ...@@ -68,6 +70,12 @@
type: String, type: String,
required: false, required: false,
}, },
terminalIconSvg: {
type: String,
required: false,
},
}, },
data() { data() {
...@@ -506,6 +514,14 @@ ...@@ -506,6 +514,14 @@
</stop-component> </stop-component>
</div> </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" <div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container"> class="inline js-rollback-component-container">
<rollback-component <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>
`,
});
})();
/* 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' }));
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
&.s32 { font-size: 20px; line-height: 30px; } &.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; } &.s40 { font-size: 16px; line-height: 38px; }
&.s60 { font-size: 32px; line-height: 58px; } &.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; } &.s90 { font-size: 36px; line-height: 88px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
&.s140 { font-size: 72px; line-height: 138px; } &.s140 { font-size: 72px; line-height: 138px; }
......
...@@ -230,6 +230,13 @@ ...@@ -230,6 +230,13 @@
} }
} }
.btn-terminal {
svg {
height: 14px;
width: 18px;
}
}
.btn-lg { .btn-lg {
padding: 12px 20px; padding: 12px 20px;
} }
......
...@@ -726,3 +726,23 @@ ...@@ -726,3 +726,23 @@
padding: 5px 5px 5px 7px; padding: 5px 5px 5px 7px;
} }
} }
.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 @@ ...@@ -93,7 +93,6 @@
.group-avatar { .group-avatar {
float: none; float: none;
margin: 0 auto; margin: 0 auto;
border: none;
&.identicon { &.identicon {
border-radius: 50%; border-radius: 50%;
......
...@@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base ...@@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
helper_method :can?, :current_application_settings 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| rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception) log_exception(exception)
...@@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base ...@@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('github') current_application_settings.import_sources.include?('github')
end end
def gitea_import_enabled?
current_application_settings.import_sources.include?('gitea')
end
def github_import_configured? def github_import_configured?
Gitlab::OAuth::Provider.enabled?(:github) Gitlab::OAuth::Provider.enabled?(:github)
end 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 class Import::GithubController < Import::BaseController
before_action :verify_github_import_enabled before_action :verify_import_enabled
before_action :github_auth, only: [:status, :jobs, :create] before_action :provider_auth, only: [:status, :jobs, :create]
rescue_from Octokit::Unauthorized, with: :github_unauthorized rescue_from Octokit::Unauthorized, with: :provider_unauthorized
helper_method :logged_in_with_github?
def new def new
if logged_in_with_github? if logged_in_with_provider?
go_to_github_for_permissions go_to_provider_for_permissions
elsif session[:github_access_token] elsif session[access_token_key]
redirect_to status_import_github_url redirect_to status_import_url
end end
end end
def callback def callback
session[:github_access_token] = client.get_token(params[:code]) session[access_token_key] = client.get_token(params[:code])
redirect_to status_import_github_url redirect_to status_import_url
end end
def personal_access_token def personal_access_token
session[:github_access_token] = params[:personal_access_token] session[access_token_key] = params[:personal_access_token]
redirect_to status_import_github_url redirect_to status_import_url
end end
def status def status
@repos = client.repos @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) 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 end
def jobs 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 render json: jobs
end end
...@@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController ...@@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController
namespace_path = params[:target_namespace].presence || current_user.namespace_path namespace_path = params[:target_namespace].presence || current_user.namespace_path
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if current_user.can?(:create_projects, @target_namespace) if can?(current_user, :create_projects, @target_namespace)
@project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else else
render 'unauthorized' render 'unauthorized'
end end
...@@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController ...@@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController
private private
def client def client
@client ||= Gitlab::GithubImport::Client.new(session[:github_access_token]) @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
end end
def verify_github_import_enabled def verify_import_enabled
render_404 unless github_import_enabled? render_404 unless import_enabled?
end end
def github_auth def go_to_provider_for_permissions
if session[:github_access_token].blank? redirect_to client.authorize_url(callback_import_url)
go_to_github_for_permissions
end
end end
def go_to_github_for_permissions def import_enabled?
redirect_to client.authorize_url(callback_import_github_url) __send__("#{provider}_import_enabled?")
end end
def github_unauthorized def new_import_url
session[:github_access_token] = nil public_send("new_import_#{provider}_url")
redirect_to new_import_github_url,
alert: 'Access denied to your GitHub account.'
end end
def logged_in_with_github? def status_import_url
current_user.identities.exists?(provider: 'github') public_send("status_import_#{provider}_url")
end
def callback_import_url
public_send("callback_import_#{provider}_url")
end
def provider_unauthorized
session[access_token_key] = nil
redirect_to new_import_url,
alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account."
end
def access_token_key
:"#{provider}_access_token"
end end
def access_params 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
end end
...@@ -4,17 +4,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -4,17 +4,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update] 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 def index
@scope = params[:scope] @scope = params[:scope]
@environments = project.environments @environments = project.environments
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: EnvironmentSerializer render json: EnvironmentSerializer
.new(project: @project) .new(project: @project, user: current_user)
.represent(@environments) .represent(@environments)
end end
end end
...@@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end 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 private
def verify_api_request!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
def environment_params def environment_params
params.require(:environment).permit(:name, :external_url) params.require(:environment).permit(:name, :external_url)
end end
......
...@@ -4,8 +4,10 @@ module ImportHelper ...@@ -4,8 +4,10 @@ module ImportHelper
"#{namespace}/#{name}" "#{namespace}/#{name}"
end end
def github_project_link(path_with_namespace) def provider_project_link(provider, path_with_namespace)
link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' url = __send__("#{provider}_project_url", path_with_namespace)
link_to path_with_namespace, url, target: '_blank'
end end
private private
...@@ -20,4 +22,8 @@ module ImportHelper ...@@ -20,4 +22,8 @@ module ImportHelper
provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
@github_url = provider.fetch('url', 'https://github.com') if provider @github_url = provider.fetch('url', 'https://github.com') if provider
end end
def gitea_project_url(path_with_namespace)
"#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
end
end end
module Milestoneish module Milestoneish
def closed_items_count(user) 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 end
def total_items_count(user) 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 end
def complete?(user) def complete?(user)
...@@ -30,7 +35,10 @@ module Milestoneish ...@@ -30,7 +35,10 @@ module Milestoneish
end end
def issues_visible_to_user(user) 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 end
def upcoming? def upcoming?
...@@ -50,4 +58,18 @@ module Milestoneish ...@@ -50,4 +58,18 @@ module Milestoneish
def expired? def expired?
due_date && due_date.past? due_date && due_date.past?
end 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 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 ...@@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base
end end
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 # 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 # or other third-party contexts, so provide a slugified version. A slug has
# the following properties: # the following properties:
......
...@@ -24,12 +24,16 @@ class GlobalMilestone ...@@ -24,12 +24,16 @@ class GlobalMilestone
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end end
def milestoneish_ids
milestones.select(:id)
end
def safe_title def safe_title
@title.to_slug.normalize.to_s @title.to_slug.normalize.to_s
end end
def projects def projects
@projects ||= Project.for_milestones(milestones.select(:id)) @projects ||= Project.for_milestones(milestoneish_ids)
end end
def state def state
...@@ -49,11 +53,11 @@ class GlobalMilestone ...@@ -49,11 +53,11 @@ class GlobalMilestone
end end
def issues def issues
@issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
end end
def merge_requests 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 end
def participants def participants
......
...@@ -129,6 +129,10 @@ class Milestone < ActiveRecord::Base ...@@ -129,6 +129,10 @@ class Milestone < ActiveRecord::Base
self.title self.title
end end
def milestoneish_ids
id
end
def can_be_closed? def can_be_closed?
active? && issues.opened.count.zero? active? && issues.opened.count.zero?
end end
......
...@@ -533,6 +533,10 @@ class Project < ActiveRecord::Base ...@@ -533,6 +533,10 @@ class Project < ActiveRecord::Base
import_type == 'gitlab_project' import_type == 'gitlab_project'
end end
def gitea_import?
import_type == 'gitea'
end
def check_limit def check_limit
unless creator.can_create_project? or namespace.kind == 'group' unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit projects_limit = creator.projects_limit
......
...@@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base ...@@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base
validates :project, presence: true validates :project, presence: true
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, 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 end
...@@ -12,4 +12,22 @@ class DeploymentService < Service ...@@ -12,4 +12,22 @@ class DeploymentService < Service
def predefined_variables def predefined_variables
[] []
end 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 end
class KubernetesService < DeploymentService 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 # Namespace defaults to the project path, but can be overridden in case that
# is an invalid or inappropriate name # is an invalid or inappropriate name
prop_accessor :namespace prop_accessor :namespace
...@@ -25,6 +30,8 @@ class KubernetesService < DeploymentService ...@@ -25,6 +30,8 @@ class KubernetesService < DeploymentService
length: 1..63 length: 1..63
end end
after_save :clear_reactive_cache!
def initialize_properties def initialize_properties
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
...@@ -41,7 +48,8 @@ class KubernetesService < DeploymentService ...@@ -41,7 +48,8 @@ class KubernetesService < DeploymentService
end end
def help def help
'' 'To enable terminal access to Kubernetes environments, label your ' \
'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end end
def to_param def to_param
...@@ -75,9 +83,9 @@ class KubernetesService < DeploymentService ...@@ -75,9 +83,9 @@ class KubernetesService < DeploymentService
# Check we can connect to the Kubernetes API # Check we can connect to the Kubernetes API
def test(*args) def test(*args)
kubeclient = build_kubeclient kubeclient = build_kubeclient!
kubeclient.discover
kubeclient.discover
{ success: kubeclient.discovered, result: "Checked API discovery endpoint" } { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
rescue => err rescue => err
{ success: false, result: err } { success: false, result: err }
...@@ -93,20 +101,48 @@ class KubernetesService < DeploymentService ...@@ -93,20 +101,48 @@ class KubernetesService < DeploymentService
variables variables
end 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') # Caches all pods in the namespace so other calls don't need to block on
return nil unless api_url && namespace && token # network access.
def calculate_reactive_cache
return unless active? && project && !project.pending_delete?
url = URI.parse(api_url) kubeclient = build_kubeclient!
url.path = url.path[0..-2] if url.path[-1] == "/"
url.path += api_path # 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( ::Kubeclient::Client.new(
url, join_api_url(api_path),
api_version, api_version,
ssl_options: kubeclient_ssl_options,
auth_options: kubeclient_auth_options, auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy'] http_proxy_uri: ENV['http_proxy']
) )
end end
...@@ -125,4 +161,13 @@ class KubernetesService < DeploymentService ...@@ -125,4 +161,13 @@ class KubernetesService < DeploymentService
def kubeclient_auth_options def kubeclient_auth_options
{ bearer_token: token } { bearer_token: token }
end 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 end
...@@ -311,10 +311,6 @@ class User < ActiveRecord::Base ...@@ -311,10 +311,6 @@ class User < ActiveRecord::Base
find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end end
def build_user(attrs = {})
User.new(attrs)
end
def reference_prefix def reference_prefix
'@' '@'
end end
...@@ -443,22 +439,16 @@ class User < ActiveRecord::Base ...@@ -443,22 +439,16 @@ class User < ActiveRecord::Base
end end
def refresh_authorized_projects def refresh_authorized_projects
transaction do Users::RefreshAuthorizedProjectsService.new(self).execute
project_authorizations.delete_all end
# project_authorizations_union can return multiple records for the same def remove_project_authorizations(project_ids)
# project/user with different access_level so we take row with the maximum project_authorizations.where(id: project_ids).delete_all
# access_level end
project_authorizations.connection.execute <<-SQL
INSERT INTO project_authorizations (user_id, project_id, access_level) def set_authorized_projects_column
SELECT user_id, project_id, MAX(access_level) AS access_level unless authorized_projects_populated
FROM (#{project_authorizations_union.to_sql}) sub update_column(:authorized_projects_populated, true)
GROUP BY user_id, project_id
SQL
unless authorized_projects_populated
update_column(:authorized_projects_populated, true)
end
end end
end end
...@@ -905,18 +895,6 @@ class User < ActiveRecord::Base ...@@ -905,18 +895,6 @@ class User < ActiveRecord::Base
private 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 def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope) groups = groups_projects.where(members: scope)
......
...@@ -23,5 +23,13 @@ class EnvironmentEntity < Grape::Entity ...@@ -23,5 +23,13 @@ class EnvironmentEntity < Grape::Entity
environment) environment)
end 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 expose :created_at, :updated_at
end end
...@@ -8,4 +8,8 @@ module RequestAwareEntity ...@@ -8,4 +8,8 @@ module RequestAwareEntity
def request def request
@options.fetch(:request) @options.fetch(:request)
end end
def can?(object, action, subject)
Ability.allowed?(object, action, subject)
end
end end
...@@ -4,15 +4,6 @@ module Projects ...@@ -4,15 +4,6 @@ module Projects
class Error < StandardError; end class Error < StandardError; end
ALLOWED_TYPES = [
'bitbucket',
'fogbugz',
'gitlab',
'github',
'google_code',
'gitlab_project'
]
def execute def execute
add_repository_to_project unless project.gitlab_project_import? add_repository_to_project unless project.gitlab_project_import?
...@@ -64,14 +55,11 @@ module Projects ...@@ -64,14 +55,11 @@ module Projects
end end
def has_importer? def has_importer?
ALLOWED_TYPES.include?(project.import_type) Gitlab::ImportSources.importer_names.include?(project.import_type)
end end
def importer def importer
return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? Gitlab::ImportSources.importer(project.import_type).new(project)
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project)
end end
def unknown_url? def unknown_url?
......
...@@ -146,7 +146,7 @@ module SystemNoteService ...@@ -146,7 +146,7 @@ module SystemNoteService
end end
def remove_merge_request_wip(noteable, project, author) 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) create_note(noteable: noteable, project: project, author: author, note: body)
end 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
- 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 - header_title "Projects", root_path
%h3.page-title %h3.page-title
%i.fa.fa-github = icon 'github', text: 'Import Projects from GitHub'
Import projects from GitHub
%p.light = render 'import/githubish_status', provider: 'github'
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}" } }
- 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 @@ ...@@ -4,10 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag("environments/environments_bundle.js") = 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, #environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
...@@ -19,4 +15,5 @@ ...@@ -19,4 +15,5 @@
"help-page-path" => help_page_path("ci/environments"), "help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class, "css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"), "commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play")}} "play-icon-svg" => custom_icon("icon_play")}}
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
%h3.page-title= @environment.name.capitalize %h3.page-title= @environment.name.capitalize
.col-md-3 .col-md-3
.nav-controls .nav-controls
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment = render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = 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"}}
...@@ -68,6 +68,11 @@ ...@@ -68,6 +68,11 @@
- if fogbugz_import_enabled? - if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz') = 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 %div
- if git_import_enabled? - if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do = link_to "#", class: 'btn js-toggle-button import_git' do
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= render 'shared/empty_states/icons/issues.svg' = render 'shared/empty_states/icons/issues.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" } .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content .text-content
- if has_button - if has_button && current_user
%h4 %h4
The Issue Tracker is a good place to add things that need to be improved or solved in a project! The Issue Tracker is a good place to add things that need to be improved or solved in a project!
%p %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="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>
...@@ -2,8 +2,6 @@ class AuthorizedProjectsWorker ...@@ -2,8 +2,6 @@ class AuthorizedProjectsWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
LEASE_TIMEOUT = 1.minute.to_i
def self.bulk_perform_async(args_list) def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end end
...@@ -11,24 +9,6 @@ class AuthorizedProjectsWorker ...@@ -11,24 +9,6 @@ class AuthorizedProjectsWorker
def perform(user_id) def perform(user_id)
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
refresh(user) if user user.refresh_authorized_projects 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
end end
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 ...@@ -84,12 +84,15 @@ class ChangelogEntry
end end
end end
private
def contents def contents
YAML.dump( yaml_content = YAML.dump(
'title' => title, 'title' => title,
'merge_request' => options.merge_request, 'merge_request' => options.merge_request,
'author' => options.author 'author' => options.author
) )
remove_trailing_whitespace(yaml_content)
end end
def write def write
...@@ -101,8 +104,6 @@ class ChangelogEntry ...@@ -101,8 +104,6 @@ class ChangelogEntry
exec("git commit --amend") exec("git commit --amend")
end end
private
def fail_with(message) def fail_with(message)
$stderr.puts "\e[31merror\e[0m #{message}" $stderr.puts "\e[31merror\e[0m #{message}"
exit 1 exit 1
...@@ -160,6 +161,10 @@ class ChangelogEntry ...@@ -160,6 +161,10 @@ class ChangelogEntry
def branch_name def branch_name
@branch_name ||= %x{git symbolic-ref --short HEAD}.strip @branch_name ||= %x{git symbolic-ref --short HEAD}.strip
end end
def remove_trailing_whitespace(yaml_content)
yaml_content.gsub(/ +$/, '')
end
end end
if $0 == __FILE__ if $0 == __FILE__
......
---
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 merge_request: 7843
author: 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: 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:
...@@ -89,6 +89,7 @@ module Gitlab ...@@ -89,6 +89,7 @@ module Gitlab
config.assets.precompile << "mailers/*.css" config.assets.precompile << "mailers/*.css"
config.assets.precompile << "katex.css" config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js" config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js" config.assets.precompile << "network/network_bundle.js"
...@@ -102,6 +103,7 @@ module Gitlab ...@@ -102,6 +103,7 @@ module Gitlab
config.assets.precompile << "environments/environments_bundle.js" config.assets.precompile << "environments/environments_bundle.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_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/utils/*.js"
config.assets.precompile << "lib/*.js" config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js" config.assets.precompile << "u2f.js"
......
...@@ -213,7 +213,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin ...@@ -213,7 +213,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['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.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['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['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
......
...@@ -6,6 +6,12 @@ namespace :import do ...@@ -6,6 +6,12 @@ namespace :import do
get :jobs get :jobs
end 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 resource :gitlab, only: [:create], controller: :gitlab do
get :status get :status
get :callback get :callback
......
...@@ -150,6 +150,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -150,6 +150,8 @@ constraints(ProjectUrlConstrainer.new) do
resources :environments, except: [:destroy] do resources :environments, except: [:destroy] do
member do member do
post :stop post :stop
get :terminal
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end end
end end
......
...@@ -46,5 +46,6 @@ ...@@ -46,5 +46,6 @@
- [repository_check, 1] - [repository_check, 1]
- [system_hook, 1] - [system_hook, 1]
- [git_garbage_collect, 1] - [git_garbage_collect, 1]
- [reactive_caching, 1]
- [cronjob, 1] - [cronjob, 1]
- [default, 1] - [default, 1]
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
- [Online terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab.
- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system. - [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Environment Variables](administration/environment_variables.md) to configure GitLab.
......
...@@ -10,11 +10,11 @@ you need to use with GitLab. ...@@ -10,11 +10,11 @@ you need to use with GitLab.
## Basic ports ## Basic ports
| LB Port | Backend Port | Protocol | | LB Port | Backend Port | Protocol |
| ------- | ------------ | -------- | | ------- | ------------ | --------------- |
| 80 | 80 | HTTP | | 80 | 80 | HTTP [^1] |
| 443 | 443 | HTTPS [^1] | | 443 | 443 | HTTPS [^1] [^2] |
| 22 | 22 | TCP | | 22 | 22 | TCP |
## GitLab Pages Ports ## GitLab Pages Ports
...@@ -25,8 +25,8 @@ GitLab Pages requires a separate VIP. Configure DNS to point the ...@@ -25,8 +25,8 @@ GitLab Pages requires a separate VIP. Configure DNS to point the
| LB Port | Backend Port | Protocol | | LB Port | Backend Port | Protocol |
| ------- | ------------ | -------- | | ------- | ------------ | -------- |
| 80 | Varies [^2] | HTTP | | 80 | Varies [^3] | HTTP |
| 443 | Varies [^2] | TCP [^3] | | 443 | Varies [^3] | TCP [^4] |
## Alternate SSH Port ## Alternate SSH Port
...@@ -50,13 +50,19 @@ Read more on high-availability configuration: ...@@ -50,13 +50,19 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md) 1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the GitLab application servers](gitlab.md)
[^1]: When using HTTPS protocol for port 443, you will need to add an SSL [^1]: [Terminal support](../../ci/environments.md#terminal-support) requires
your load balancer to correctly handle WebSocket connections. When using
HTTP or HTTPS proxying, this means your load balancer must be configured
to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
[online terminal](../integration/terminal.md) integration guide for
more details.
[^2]: When using HTTPS protocol for port 443, you will need to add an SSL
certificate to the load balancers. If you wish to terminate SSL at the certificate to the load balancers. If you wish to terminate SSL at the
GitLab application server instead, use TCP protocol. GitLab application server instead, use TCP protocol.
[^2]: The backend port for GitLab Pages depends on the [^3]: The backend port for GitLab Pages depends on the
`gitlab_pages['external_http']` and `gitlab_pages['external_https']` `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
setting. See [GitLab Pages documentation][gitlab-pages] for more details. setting. See [GitLab Pages documentation][gitlab-pages] for more details.
[^3]: Port 443 for GitLab Pages should always use the TCP protocol. Users can [^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can
configure custom domains with custom SSL, which would not be possible configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer. if SSL was terminated at the load balancer.
......
# Online terminals
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
in GitLab 8.15. Only project masters and owners can access online terminals.
With the introduction of the [Kubernetes](../../project_services/kubernetes.md)
project service, GitLab gained the ability to store and use credentials for a
Kubernetes cluster. One of the things it uses these credentials for is providing
access to [online terminals](../../ci/environments.html#online-terminals)
for environments.
## How it works
A detailed overview of the architecture of online terminals and how they work
can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md).
In brief:
* GitLab relies on the user to provide their own Kubernetes credentials, and to
appropriately label the pods they create when deploying.
* When a user navigates to the terminal page for an environment, they are served
a JavaScript application that opens a WebSocket connection back to GitLab.
* The WebSocket is handled in [Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse),
rather than the Rails application server.
* Workhorse queries Rails for connection details and user permissions; Rails
queries Kubernetes for them in the background, using [Sidekiq](../troubleshooting/sidekiq.md)
* Workhorse acts as a proxy server between the user's browser and the Kubernetes
API, passing WebSocket frames between the two.
* Workhorse regularly polls Rails, terminating the WebSocket connection if the
user no longer has permission to access the terminal, or if the connection
details have changed.
## Enabling and disabling terminal support
As online terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of
Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers
through to the next one in the chain. If you installed Gitlab using Omnibus, or
from source, starting with GitLab 8.15, this should be done by the default
configuration, so there's no need for you to do anything.
However, if you run a [load balancer](../high_availability/load_balancer.md) in
front of GitLab, you may need to make some changes to your configuration. These
guides document the necessary steps for a selection of popular reverse proxies:
* [Apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html)
* [NGINX](https://www.nginx.com/blog/websocket-nginx/)
* [HAProxy](http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/)
* [Varnish](https://www.varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html)
Workhorse won't let WebSocket requests through to non-WebSocket endpoints, so
it's safe to enable support for these headers globally. If you'd rather had a
narrower set of rules, you can restrict it to URLs ending with `/terminal.ws`
(although this may still have a few false positives).
If you installed from source, or have made any configuration changes to your
Omnibus installation before upgrading to 8.15, you may need to make some
changes to your configuration. See the [8.14 to 8.15 upgrade](../../update/8.14-to-8.15.md#nginx-configuration)
document for more details.
If you'd like to disable online terminal support in GitLab, just stop passing
the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse
proxy in the chain. For most users, this will be the NGINX server bundled with
Omnibus Gitlab, in which case, you need to:
* Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file
* Ensure the whole block is uncommented, and then comment out or remove the
`Connection` and `Upgrade` lines.
For your own load balancer, just reverse the configuration changes recommended
by the above guides.
When these headers are not passed through, Workhorse will return a
`400 Bad Request` response to users attempting to use an online terminal. In
turn, they will receive a `Connection failed` message.
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
## List repository tree ## List repository tree
Get a list of repository files and directories in a project. Get a list of repository files and directories in a project. This endpoint can
be accessed without authentication if the repository is publicly accessible.
``` ```
GET /projects/:id/repository/tree GET /projects/:id/repository/tree
...@@ -71,7 +72,8 @@ Parameters: ...@@ -71,7 +72,8 @@ Parameters:
## Raw file content ## Raw file content
Get the raw file contents for a file by commit SHA and path. Get the raw file contents for a file by commit SHA and path. This endpoint can
be accessed without authentication if the repository is publicly accessible.
``` ```
GET /projects/:id/repository/blobs/:sha GET /projects/:id/repository/blobs/:sha
...@@ -85,7 +87,8 @@ Parameters: ...@@ -85,7 +87,8 @@ Parameters:
## Raw blob content ## Raw blob content
Get the raw file contents for a blob by blob SHA. Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
without authentication if the repository is publicly accessible.
``` ```
GET /projects/:id/repository/raw_blobs/:sha GET /projects/:id/repository/raw_blobs/:sha
...@@ -98,7 +101,8 @@ Parameters: ...@@ -98,7 +101,8 @@ Parameters:
## Get file archive ## Get file archive
Get an archive of the repository Get an archive of the repository. This endpoint can be accessed without
authentication if the repository is publicly accessible.
``` ```
GET /projects/:id/repository/archive GET /projects/:id/repository/archive
...@@ -111,6 +115,9 @@ Parameters: ...@@ -111,6 +115,9 @@ Parameters:
## Compare branches, tags or commits ## Compare branches, tags or commits
This endpoint can be accessed without authentication if the repository is
publicly accessible.
``` ```
GET /projects/:id/repository/compare GET /projects/:id/repository/compare
``` ```
...@@ -163,7 +170,8 @@ Response: ...@@ -163,7 +170,8 @@ Response:
## Contributors ## Contributors
Get repository contributors list Get repository contributors list. This endpoint can be accessed without
authentication if the repository is publicly accessible.
``` ```
GET /projects/:id/repository/contributors GET /projects/:id/repository/contributors
......
...@@ -6,7 +6,9 @@ ...@@ -6,7 +6,9 @@
## Get file from repository ## Get file from repository
Allows you to receive information about file in repository like name, size, content. Note that file content is Base64 encoded. Allows you to receive information about file in repository like name, size,
content. Note that file content is Base64 encoded. This endpoint can be accessed
without authentication if the repository is publicly accessible.
``` ```
GET /projects/:id/repository/files GET /projects/:id/repository/files
......
...@@ -25,7 +25,9 @@ Environments are like tags for your CI jobs, describing where code gets deployed ...@@ -25,7 +25,9 @@ Environments are like tags for your CI jobs, describing where code gets deployed
Deployments are created when [jobs] deploy versions of code to environments, Deployments are created when [jobs] deploy versions of code to environments,
so every environment can have one or more deployments. GitLab keeps track of so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your your deployments, so you always know what is currently being deployed on your
servers. servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
enabled for your project, you can use it to assist with your deployments, and
can even access a terminal for your environment from within GitLab!
To better understand how environments and deployments work, let's consider an To better understand how environments and deployments work, let's consider an
example. We assume that you have already created a project in GitLab and set up example. We assume that you have already created a project in GitLab and set up
...@@ -233,6 +235,46 @@ Remember that if your environment's name is `production` (all lowercase), then ...@@ -233,6 +235,46 @@ Remember that if your environment's name is `production` (all lowercase), then
it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
Double the benefit! Double the benefit!
## Terminal support
>**Note:**
Terminal support was added in GitLab 8.15 and is only available to project
masters and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service documentation.
Once enabled, your environments will gain a "terminal" button:
![Terminal button on environment index](img/environments_terminal_button_on_index.png)
You can also access the terminal button from the page for a specific environment:
![Terminal button for an environment](img/environments_terminal_button_on_show.png)
Wherever you find it, clicking the button will take you to a separate page to
establish the terminal session:
![Terminal page](img/environments_terminal_page.png)
This works just like any other terminal - you'll be in the container created
by your deployment, so you can run shell commands and get responses in real
time, check the logs, try out configuration or code tweaks, etc. You can open
multiple terminals to the same environment - they each get their own shell
session - and even a multiplexer like `screen` or `tmux`!
>**Note:**
Container-based deployments often lack basic tools (like an editor), and may
be stopped or restarted at any time. If this happens, you will lose all your
changes! Treat this as a debugging tool, not a comprehensive online IDE. You
can use [Koding](../administration/integration/koding.md) for online
development.
---
While this is fine for deploying to some stable environments like staging or While this is fine for deploying to some stable environments like staging or
production, what happens for branches? So far we haven't defined anything production, what happens for branches? So far we haven't defined anything
regarding deployments for branches other than `master`. Dynamic environments regarding deployments for branches other than `master`. Dynamic environments
...@@ -524,6 +566,7 @@ Below are some links you may find interesting: ...@@ -524,6 +566,7 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md [Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs [jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md [yaml]: yaml/README.md
[kubernetes-service]: ../project_services/kubernetes.md]
[environments]: #environments [environments]: #environments
[deployments]: #deployments [deployments]: #deployments
[permissions]: ../user/permissions.md [permissions]: ../user/permissions.md
......
...@@ -36,6 +36,37 @@ Clicking on a pipeline will show the builds that were run for that pipeline. ...@@ -36,6 +36,37 @@ Clicking on a pipeline will show the builds that were run for that pipeline.
Clicking on an individual build will show you its build trace, and allow you to Clicking on an individual build will show you its build trace, and allow you to
cancel the build, retry it, or erase the build trace. cancel the build, retry it, or erase the build trace.
## How the pipeline duration is calculated
Total running time for a given pipeline would exclude retries and pending
(queue) time. We could reduce this problem down to finding the union of
periods.
So each job would be represented as a `Period`, which consists of
`Period#first` as when the job started and `Period#last` as when the
job was finished. A simple example here would be:
* A (1, 3)
* B (2, 4)
* C (6, 7)
Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
C begins from 6, and ends to 7. Visually it could be viewed as:
```
0 1 2 3 4 5 6 7
AAAAAAA
BBBBBBB
CCCC
```
The union of A, B, and C would be (1, 4) and (6, 7), therefore the
total running time should be:
```
(4 - 1) + (7 - 6) => 4
```
## Badges ## Badges
Build status and test coverage report badges are available. You can find their Build status and test coverage report badges are available. You can find their
......
...@@ -47,3 +47,17 @@ GitLab CI build environment: ...@@ -47,3 +47,17 @@ GitLab CI build environment:
- `KUBE_TOKEN` - `KUBE_TOKEN`
- `KUBE_NAMESPACE` - `KUBE_NAMESPACE`
- `KUBE_CA_PEM` - only if a custom CA bundle was specified - `KUBE_CA_PEM` - only if a custom CA bundle was specified
## Terminal support
>**NOTE:**
Added in GitLab 8.15. You must be the project owner or have `master` permissions
to use terminals. Support is currently limited to the first container in the
first pod of your environment.
When enabled, the Kubernetes service adds online [terminal support](../ci/environments.md#terminal-support)
to your environments. This is based on the `exec` functionality found in
Docker and Kubernetes, so you get a new shell session within your existing
containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with
`app=$CI_ENVIRONMENT_SLUG`.
...@@ -33,6 +33,7 @@ The following table depicts the various user permission levels in a project. ...@@ -33,6 +33,7 @@ The following table depicts the various user permission levels in a project.
| See a container registry | | ✓ | ✓ | ✓ | ✓ | | See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ |
| Use environment terminals | | | | ✓ | ✓ |
| Stop environments | | | ✓ | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ | | Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
1. [GitHub](import_projects_from_github.md) 1. [GitHub](import_projects_from_github.md)
1. [GitLab.com](import_projects_from_gitlab_com.md) 1. [GitLab.com](import_projects_from_gitlab_com.md)
1. [FogBugz](import_projects_from_fogbugz.md) 1. [FogBugz](import_projects_from_fogbugz.md)
1. [Gitea](import_projects_from_gitea.md)
1. [SVN](migrating_from_svn.md) 1. [SVN](migrating_from_svn.md)
In addition to the specific migration documentation above, you can import any In addition to the specific migration documentation above, you can import any
...@@ -14,4 +15,3 @@ repository is too large the import can timeout. ...@@ -14,4 +15,3 @@ repository is too large the import can timeout.
You can copy your repos by changing the remote and pushing to the new server; You can copy your repos by changing the remote and pushing to the new server;
but issues and merge requests can't be imported. but issues and merge requests can't be imported.
# Import your project from Gitea to GitLab
Import your projects from Gitea to GitLab with minimal effort.
## Overview
>**Note:**
As of Gitea `v1.0.0`, issue & pull-request comments cannot be imported! This is
a [known issue][issue-401] that should be fixed in a near-future.
- At its current state, Gitea importer can import:
- the repository description (GitLab 8.15+)
- the Git repository data (GitLab 8.15+)
- the issues (GitLab 8.15+)
- the pull requests (GitLab 8.15+)
- the milestones (GitLab 8.15+)
- the labels (GitLab 8.15+)
- Repository public access is retained. If a repository is private in Gitea
it will be created as private in GitLab as well.
## How it works
Since Gitea is currently not an OAuth provider, author/assignee cannot be mapped
to users in your GitLab's instance. This means that 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 Gitea author is kept.
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.
## Importing your Gitea repositories
The importer page is visible when you create a new project.
![New project page on GitLab](img/import_projects_from_new_project_page.png)
Click on the **Gitea** link and the import authorization process will start.
![New Gitea project import](img/import_projects_from_gitea_new_import.png)
### Authorize access to your repositories using a personal access token
With this method, you will perform a one-off authorization with Gitea to grant
GitLab access your repositories:
1. Go to <https://you-gitea-instance/user/settings/applications> (replace
`you-gitea-instance` with the host of your Gitea instance).
1. Click **Generate New Token**.
1. Enter a token description.
1. Click **Generate Token**.
1. Copy the token hash.
1. Go back to GitLab and provide the token to the Gitea importer.
1. Hit the **List Your Gitea Repositories** button and wait while GitLab reads
your repositories' information. Once done, you'll be taken to the importer
page to select the repositories to import.
### Select which repositories to import
After you've authorized access to your Gitea repositories, you will be
redirected to the Gitea importer page.
From there, you can see the import statuses of your Gitea repositories.
- Those that are being imported will show a _started_ status,
- those already successfully imported will be green with a _done_ status,
- whereas those that are not yet imported will have an **Import** button on the
right side of the table.
If you want, you can import all your Gitea projects in one go by hitting
**Import all projects** in the upper left corner.
![Gitea importer page](img/import_projects_from_github_importer.png)
---
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
[issue-401]: https://github.com/go-gitea/gitea/issues/401
...@@ -6,8 +6,9 @@ Import your projects from GitHub to GitLab with minimal effort. ...@@ -6,8 +6,9 @@ Import your projects from GitHub to GitLab with minimal effort.
>**Note:** >**Note:**
If you are an administrator you can enable the [GitHub integration][gh-import] If you are an administrator you can enable the [GitHub integration][gh-import]
in your GitLab instance sitewide. This configuration is optional, users will be in your GitLab instance sitewide. This configuration is optional, users will
able import their GitHub repositories with a [personal access token][gh-token]. still be able to import their GitHub repositories with a
[personal access token][gh-token].
- At its current state, GitHub importer can import: - At its current state, GitHub importer can import:
- the repository description (GitLab 7.7+) - the repository description (GitLab 7.7+)
...@@ -85,7 +86,7 @@ authorization with GitHub to grant GitLab access your repositories: ...@@ -85,7 +86,7 @@ authorization with GitHub to grant GitLab access your repositories:
1. Click **Generate token**. 1. Click **Generate token**.
1. Copy the token hash. 1. Copy the token hash.
1. Go back to GitLab and provide the token to the GitHub importer. 1. Go back to GitLab and provide the token to the GitHub importer.
1. Hit the **List your GitHub repositories** button and wait while GitLab reads 1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads
your repositories' information. Once done, you'll be taken to the importer your repositories' information. Once done, you'll be taken to the importer
page to select the repositories to import. page to select the repositories to import.
...@@ -112,7 +113,6 @@ You can also choose a different name for the project and a different namespace, ...@@ -112,7 +113,6 @@ You can also choose a different name for the project and a different namespace,
if you have the privileges to do so. if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration" [gh-import]: ../../integration/github.md "GitHub integration"
[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration [gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token [gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
[social sign-in]: ../../profile/account/social_sign_in.md [social sign-in]: ../../profile/account/social_sign_in.md
@admin
Feature: Admin Projects
Background:
Given I sign in as an admin
And there are projects in system
Scenario: I should see non-archived projects in the list
Given archived project "Archive"
When I visit admin projects page
Then I should see all non-archived projects
And I should not see project "Archive"
@javascript
Scenario: I should see all projects in the list
Given archived project "Archive"
When I visit admin projects page
And I select "Show archived projects"
Then I should see all projects
And I should see "archived" label
Scenario: Projects show
When I visit admin projects page
And I click on first project
Then I should see project details
@javascript
Scenario: Transfer project
Given group 'Web'
And I visit admin project page
When I transfer project to group 'Web'
Then I should see project transfered
@javascript
Scenario: Signed in admin should be able to add himself to a project
Given "John Doe" owns private project "Enterprise"
When I visit project "Enterprise" members page
When I select current user as "Developer"
Then I should see current user as "Developer"
@javascript
Scenario: Signed in admin should be able to remove himself from a project
Given "John Doe" owns private project "Enterprise"
And current user is developer of project "Enterprise"
When I visit project "Enterprise" members page
Then I should see current user as "Developer"
When I click on the "Remove User From Project" button for current user
Then I should not see current user as "Developer"
class Spinach::Features::AdminProjects < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedAdmin
include SharedProject
include SharedUser
include Select2Helper
step 'I should see all non-archived projects' do
Project.non_archived.each do |p|
expect(page).to have_content p.name_with_namespace
end
end
step 'I should see all projects' do
Project.all.each do |p|
expect(page).to have_content p.name_with_namespace
end
end
step 'I select "Show archived projects"' do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects'
end
step 'I should see "archived" label' do
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
step 'I click on first project' do
click_link Project.first.name_with_namespace
end
step 'I should see project details' do
project = Project.first
expect(current_path).to eq admin_namespace_project_path(project.namespace, project)
expect(page).to have_content(project.name_with_namespace)
expect(page).to have_content(project.creator.name)
end
step 'I visit admin project page' do
visit admin_namespace_project_path(project.namespace, project)
end
step 'I transfer project to group \'Web\'' do
allow_any_instance_of(Projects::TransferService).
to receive(:move_uploads_to_new_namespace).and_return(true)
click_button 'Search for Namespace'
click_link 'group: web'
click_button 'Transfer'
end
step 'group \'Web\'' do
create(:group, name: 'Web')
end
step 'I should see project transfered' do
expect(page).to have_content 'Web / ' + project.name
expect(page).to have_content 'Namespace: Web'
end
step 'I visit project "Enterprise" members page' do
project = Project.find_by!(name: "Enterprise")
visit namespace_project_project_members_path(project.namespace, project)
end
step 'I select current user as "Developer"' do
page.within ".users-project-form" do
select2(current_user.id, from: "#user_ids", multiple: true)
select "Developer", from: "access_level"
end
click_button "Add to project"
end
step 'I should see current user as "Developer"' do
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
end
step 'current user is developer of project "Enterprise"' do
project = Project.find_by!(name: "Enterprise")
project.team << [current_user, :developer]
end
step 'I click on the "Remove User From Project" button for current user' do
find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
# poltergeist always confirms popups.
end
step 'I should not see current_user as "Developer"' do
expect(page).not_to have_selector(:css, '.content-list')
end
def project
@project ||= Project.first
end
def group
Group.find_by(name: 'Web')
end
end
module API module API
# Projects API # Projects API
class Files < Grape::API class Files < Grape::API
before { authenticate! }
helpers do helpers do
def commit_params(attrs) def commit_params(attrs)
{ {
......
...@@ -2,7 +2,6 @@ require 'mime/types' ...@@ -2,7 +2,6 @@ require 'mime/types'
module API module API
class Repositories < Grape::API class Repositories < Grape::API
before { authenticate! }
before { authorize! :download_code, user_project } before { authorize! :download_code, user_project }
params do params do
...@@ -79,8 +78,6 @@ module API ...@@ -79,8 +78,6 @@ module API
optional :format, type: String, desc: 'The archive format' optional :format, type: String, desc: 'The archive format'
end end
get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
authorize! :download_code, user_project
begin begin
send_git_archive user_project.repository, ref: params[:sha], format: params[:format] send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
rescue rescue
...@@ -96,7 +93,6 @@ module API ...@@ -96,7 +93,6 @@ module API
requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
end end
get ':id/repository/compare' do get ':id/repository/compare' do
authorize! :download_code, user_project
compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
present compare, with: Entities::Compare present compare, with: Entities::Compare
end end
...@@ -105,8 +101,6 @@ module API ...@@ -105,8 +101,6 @@ module API
success Entities::Contributor success Entities::Contributor
end end
get ':id/repository/contributors' do get ':id/repository/contributors' do
authorize! :download_code, user_project
begin begin
present user_project.repository.contributors, present user_project.repository.contributors,
with: Entities::Contributor with: Entities::Contributor
......
...@@ -94,7 +94,7 @@ module API ...@@ -94,7 +94,7 @@ module API
identity_attrs = params.slice(:provider, :extern_uid) identity_attrs = params.slice(:provider, :extern_uid)
confirm = params.delete(:confirm) confirm = params.delete(:confirm)
user = User.build_user(declared_params(include_missing: false)) user = User.new(declared_params(include_missing: false))
user.skip_confirmation! unless confirm user.skip_confirmation! unless confirm
if identity_attrs.any? if identity_attrs.any?
......
...@@ -45,7 +45,7 @@ module Gitlab ...@@ -45,7 +45,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project], import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
......
...@@ -15,6 +15,10 @@ module Gitlab ...@@ -15,6 +15,10 @@ module Gitlab
end end
end end
def url
raw_data.url || ''
end
private private
def gitlab_user_id(github_id) def gitlab_user_id(github_id)
......
...@@ -4,10 +4,12 @@ module Gitlab ...@@ -4,10 +4,12 @@ module Gitlab
GITHUB_SAFE_REMAINING_REQUESTS = 100 GITHUB_SAFE_REMAINING_REQUESTS = 100
GITHUB_SAFE_SLEEP_TIME = 500 GITHUB_SAFE_SLEEP_TIME = 500
attr_reader :access_token attr_reader :access_token, :host, :api_version
def initialize(access_token) def initialize(access_token, host: nil, api_version: 'v3')
@access_token = access_token @access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '')
@api_version = api_version
if access_token if access_token
::Octokit.auto_paginate = false ::Octokit.auto_paginate = false
...@@ -17,7 +19,7 @@ module Gitlab ...@@ -17,7 +19,7 @@ module Gitlab
def api def api
@api ||= ::Octokit::Client.new( @api ||= ::Octokit::Client.new(
access_token: access_token, access_token: access_token,
api_endpoint: github_options[:site], api_endpoint: api_endpoint,
# If there is no config, we're connecting to github.com and we # If there is no config, we're connecting to github.com and we
# should verify ssl. # should verify ssl.
connection_options: { connection_options: {
...@@ -64,6 +66,14 @@ module Gitlab ...@@ -64,6 +66,14 @@ module Gitlab
private private
def api_endpoint
if host.present? && api_version.present?
"#{host}/api/#{api_version}"
else
github_options[:site]
end
end
def config def config
Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" } Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
end end
......
...@@ -3,7 +3,7 @@ module Gitlab ...@@ -3,7 +3,7 @@ module Gitlab
class Importer class Importer
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
attr_reader :client, :errors, :project, :repo, :repo_url attr_reader :errors, :project, :repo, :repo_url
def initialize(project) def initialize(project)
@project = project @project = project
...@@ -11,12 +11,27 @@ module Gitlab ...@@ -11,12 +11,27 @@ module Gitlab
@repo_url = project.import_url @repo_url = project.import_url
@errors = [] @errors = []
@labels = {} @labels = {}
end
def client
return @client if defined?(@client)
unless credentials
raise Projects::ImportService::Error,
"Unable to find project import data credentials for project ID: #{@project.id}"
end
if credentials opts = {}
@client = Client.new(credentials[:user]) # Gitea plan to be GitHub compliant
else if project.gitea_import?
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" uri = URI.parse(project.import_url)
host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '')
opts = {
host: host,
api_version: 'v1'
}
end end
@client = Client.new(credentials[:user], opts)
end end
def execute def execute
...@@ -35,7 +50,13 @@ module Gitlab ...@@ -35,7 +50,13 @@ module Gitlab
import_comments(:issues) import_comments(:issues)
import_comments(:pull_requests) import_comments(:pull_requests)
import_wiki import_wiki
import_releases
# Gitea doesn't have a Release API yet
# See https://github.com/go-gitea/gitea/issues/330
unless project.gitea_import?
import_releases
end
handle_errors handle_errors
true true
...@@ -44,7 +65,9 @@ module Gitlab ...@@ -44,7 +65,9 @@ module Gitlab
private private
def credentials def credentials
@credentials ||= project.import_data.credentials if project.import_data return @credentials if defined?(@credentials)
@credentials = project.import_data ? project.import_data.credentials : nil
end end
def handle_errors def handle_errors
...@@ -60,9 +83,10 @@ module Gitlab ...@@ -60,9 +83,10 @@ module Gitlab
fetch_resources(:labels, repo, per_page: 100) do |labels| fetch_resources(:labels, repo, per_page: 100) do |labels|
labels.each do |raw| labels.each do |raw|
begin begin
LabelFormatter.new(project, raw).create! gh_label = LabelFormatter.new(project, raw)
gh_label.create!
rescue => e rescue => e
errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
end end
end end
end end
...@@ -74,9 +98,10 @@ module Gitlab ...@@ -74,9 +98,10 @@ module Gitlab
fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones| fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw| milestones.each do |raw|
begin begin
MilestoneFormatter.new(project, raw).create! gh_milestone = MilestoneFormatter.new(project, raw)
gh_milestone.create!
rescue => e rescue => e
errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
end end
end end
end end
...@@ -97,7 +122,7 @@ module Gitlab ...@@ -97,7 +122,7 @@ module Gitlab
apply_labels(issuable, raw) apply_labels(issuable, raw)
rescue => e rescue => e
errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message }
end end
end end
end end
...@@ -106,18 +131,23 @@ module Gitlab ...@@ -106,18 +131,23 @@ module Gitlab
def import_pull_requests def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw| pull_requests.each do |raw|
pull_request = PullRequestFormatter.new(project, raw) gh_pull_request = PullRequestFormatter.new(project, raw)
next unless pull_request.valid? next unless gh_pull_request.valid?
begin begin
restore_source_branch(pull_request) unless pull_request.source_branch_exists? restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists?
restore_target_branch(pull_request) unless pull_request.target_branch_exists? restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists?
merge_request = gh_pull_request.create!
pull_request.create! # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage
if project.gitea_import?
apply_labels(merge_request, raw)
end
rescue => e rescue => e
errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message } errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message }
ensure ensure
clean_up_restored_branches(pull_request) clean_up_restored_branches(gh_pull_request)
end end
end end
end end
...@@ -233,7 +263,7 @@ module Gitlab ...@@ -233,7 +263,7 @@ module Gitlab
gh_release = ReleaseFormatter.new(project, raw) gh_release = ReleaseFormatter.new(project, raw)
gh_release.create! if gh_release.valid? gh_release.create! if gh_release.valid?
rescue => e rescue => e
errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
end end
end end
end end
......
module Gitlab
module GithubImport
class IssuableFormatter < BaseFormatter
def project_association
raise NotImplementedError
end
def number
raw_data.number
end
def find_condition
{ iid: number }
end
private
def state
raw_data.state == 'closed' ? 'closed' : 'opened'
end
def assigned?
raw_data.assignee.present?
end
def assignee_id
if assigned?
gitlab_user_id(raw_data.assignee.id)
end
end
def author
raw_data.user.login
end
def author_id
gitlab_author_id || project.creator_id
end
def body
raw_data.body || ""
end
def description
if gitlab_author_id
body
else
formatter.author_line(author) + body
end
end
def milestone
if raw_data.milestone.present?
milestone = MilestoneFormatter.new(project, raw_data.milestone)
project.milestones.find_by(milestone.find_condition)
end
end
end
end
end
module Gitlab module Gitlab
module GithubImport module GithubImport
class IssueFormatter < BaseFormatter class IssueFormatter < IssuableFormatter
def attributes def attributes
{ {
iid: number, iid: number,
...@@ -24,59 +24,9 @@ module Gitlab ...@@ -24,59 +24,9 @@ module Gitlab
:issues :issues
end end
def find_condition
{ iid: number }
end
def number
raw_data.number
end
def pull_request? def pull_request?
raw_data.pull_request.present? raw_data.pull_request.present?
end end
private
def assigned?
raw_data.assignee.present?
end
def assignee_id
if assigned?
gitlab_user_id(raw_data.assignee.id)
end
end
def author
raw_data.user.login
end
def author_id
gitlab_author_id || project.creator_id
end
def body
raw_data.body || ""
end
def description
if gitlab_author_id
body
else
formatter.author_line(author) + body
end
end
def milestone
if raw_data.milestone.present?
project.milestones.find_by(iid: raw_data.milestone.number)
end
end
def state
raw_data.state == 'closed' ? 'closed' : 'opened'
end
end end
end end
end end
...@@ -3,7 +3,7 @@ module Gitlab ...@@ -3,7 +3,7 @@ module Gitlab
class MilestoneFormatter < BaseFormatter class MilestoneFormatter < BaseFormatter
def attributes def attributes
{ {
iid: raw_data.number, iid: number,
project: project, project: project,
title: raw_data.title, title: raw_data.title,
description: raw_data.description, description: raw_data.description,
...@@ -19,7 +19,15 @@ module Gitlab ...@@ -19,7 +19,15 @@ module Gitlab
end end
def find_condition def find_condition
{ iid: raw_data.number } { iid: number }
end
def number
if project.gitea_import?
raw_data.id
else
raw_data.number
end
end end
private private
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class ProjectCreator class ProjectCreator
attr_reader :repo, :name, :namespace, :current_user, :session_data attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data) def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@repo = repo @repo = repo
@name = name @name = name
@namespace = namespace @namespace = namespace
@current_user = current_user @current_user = current_user
@session_data = session_data @session_data = session_data
@type = type
end end
def execute def execute
...@@ -19,7 +20,7 @@ module Gitlab ...@@ -19,7 +20,7 @@ module Gitlab
description: repo.description, description: repo.description,
namespace_id: namespace.id, namespace_id: namespace.id,
visibility_level: visibility_level, visibility_level: visibility_level,
import_type: "github", import_type: type,
import_source: repo.full_name, import_source: repo.full_name,
import_url: import_url, import_url: import_url,
skip_wiki: skip_wiki skip_wiki: skip_wiki
...@@ -29,7 +30,7 @@ module Gitlab ...@@ -29,7 +30,7 @@ module Gitlab
private private
def import_url def import_url
repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@") repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@")
end end
def visibility_level def visibility_level
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class PullRequestFormatter < BaseFormatter class PullRequestFormatter < IssuableFormatter
delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
...@@ -28,14 +28,6 @@ module Gitlab ...@@ -28,14 +28,6 @@ module Gitlab
:merge_requests :merge_requests
end end
def find_condition
{ iid: number }
end
def number
raw_data.number
end
def valid? def valid?
source_branch.valid? && target_branch.valid? source_branch.valid? && target_branch.valid?
end end
...@@ -60,57 +52,15 @@ module Gitlab ...@@ -60,57 +52,15 @@ module Gitlab
end end
end end
def url
raw_data.url
end
private private
def assigned? def state
raw_data.assignee.present? if raw_data.state == 'closed' && raw_data.merged_at.present?
end 'merged'
def assignee_id
if assigned?
gitlab_user_id(raw_data.assignee.id)
end
end
def author
raw_data.user.login
end
def author_id
gitlab_author_id || project.creator_id
end
def body
raw_data.body || ""
end
def description
if gitlab_author_id
body
else else
formatter.author_line(author) + body super
end end
end end
def milestone
if raw_data.milestone.present?
project.milestones.find_by(iid: raw_data.milestone.number)
end
end
def state
@state ||= if raw_data.state == 'closed' && raw_data.merged_at.present?
'merged'
elsif raw_data.state == 'closed'
'closed'
else
'opened'
end
end
end end
end end
end end
...@@ -7,21 +7,38 @@ module Gitlab ...@@ -7,21 +7,38 @@ module Gitlab
module ImportSources module ImportSources
extend CurrentSettings extend CurrentSettings
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
ImportSource.new('github', 'GitHub', Gitlab::GithubImport::Importer),
ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer)
].freeze
class << self class << self
def options
@options ||= Hash[ImportTable.map { |importer| [importer.title, importer.name] }]
end
def values def values
options.values @values ||= ImportTable.map(&:name)
end end
def options def importer_names
{ @importer_names ||= ImportTable.select(&:importer).map(&:name)
'GitHub' => 'github', end
'Bitbucket' => 'bitbucket',
'GitLab.com' => 'gitlab', def importer(name)
'Google Code' => 'google_code', ImportTable.find { |import_source| import_source.name == name }.importer
'FogBugz' => 'fogbugz', end
'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project' def title(name)
} options.key(name)
end end
end end
end end
......
module Gitlab
# Helper methods to do with Kubernetes network services & resources
module Kubernetes
# This is the comand that is run to start a terminal session. Kubernetes
# expects `command=foo&command=bar, not `command[]=foo&command[]=bar`
EXEC_COMMAND = URI.encode_www_form(
['sh', '-c', 'bash || sh'].map { |value| ['command', value] }
)
# Filters an array of pods (as returned by the kubernetes API) by their labels
def filter_pods(pods, labels = {})
pods.select do |pod|
metadata = pod.fetch("metadata", {})
pod_labels = metadata.fetch("labels", nil)
next unless pod_labels
labels.all? { |k, v| pod_labels[k.to_s] == v }
end
end
# Converts a pod (as returned by the kubernetes API) into a terminal
def terminals_for_pod(api_url, namespace, pod)
metadata = pod.fetch("metadata", {})
status = pod.fetch("status", {})
spec = pod.fetch("spec", {})
containers = spec["containers"]
pod_name = metadata["name"]
phase = status["phase"]
return unless containers.present? && pod_name.present? && phase == "Running"
created_at = DateTime.parse(metadata["creationTimestamp"]) rescue nil
containers.map do |container|
{
selectors: { pod: pod_name, container: container["name"] },
url: container_exec_url(api_url, namespace, pod_name, container["name"]),
subprotocols: ['channel.k8s.io'],
headers: Hash.new { |h, k| h[k] = [] },
created_at: created_at,
}
end
end
def add_terminal_auth(terminal, token, ca_pem = nil)
terminal[:headers]['Authorization'] << "Bearer #{token}"
terminal[:ca_pem] = ca_pem if ca_pem.present?
terminal
end
def container_exec_url(api_url, namespace, pod_name, container_name)
url = URI.parse(api_url)
url.path = [
url.path.sub(%r{/+\z}, ''),
'api', 'v1',
'namespaces', ERB::Util.url_encode(namespace),
'pods', ERB::Util.url_encode(pod_name),
'exec'
].join('/')
url.query = {
container: container_name,
tty: true,
stdin: true,
stdout: true,
stderr: true,
}.to_query + '&' + EXEC_COMMAND
case url.scheme
when 'http'
url.scheme = 'ws'
when 'https'
url.scheme = 'wss'
end
url.to_s
end
end
end
...@@ -95,6 +95,19 @@ module Gitlab ...@@ -95,6 +95,19 @@ module Gitlab
] ]
end end
def terminal_websocket(terminal)
details = {
'Terminal' => {
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
'Header' => terminal[:headers]
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
details
end
def version def version
path = Rails.root.join(VERSION_FILE) path = Rails.root.join(VERSION_FILE)
path.readable? ? path.read.chomp : 'unknown' path.readable? ? path.read.chomp : 'unknown'
......
require 'spec_helper'
describe Import::GiteaController do
include ImportSpecHelper
let(:provider) { :gitea }
let(:host_url) { 'https://try.gitea.io' }
include_context 'a GitHub-ish import controller'
def assign_host_url
session[:gitea_host_url] = host_url
end
describe "GET new" do
it_behaves_like 'a GitHub-ish import controller: GET new' do
before do
assign_host_url
end
end
end
describe "POST personal_access_token" do
it_behaves_like 'a GitHub-ish import controller: POST personal_access_token'
end
describe "GET status" do
it_behaves_like 'a GitHub-ish import controller: GET status' do
before do
assign_host_url
end
let(:extra_assign_expectations) { { gitea_host_url: host_url } }
end
end
describe 'POST create' do
it_behaves_like 'a GitHub-ish import controller: POST create' do
before do
assign_host_url
end
end
end
end
...@@ -3,34 +3,18 @@ require 'spec_helper' ...@@ -3,34 +3,18 @@ require 'spec_helper'
describe Import::GithubController do describe Import::GithubController do
include ImportSpecHelper include ImportSpecHelper
let(:user) { create(:user) } let(:provider) { :github }
let(:token) { "asdasd12345" }
let(:access_params) { { github_access_token: token } }
def assign_session_token include_context 'a GitHub-ish import controller'
session[:github_access_token] = token
end
before do
sign_in(user)
allow(controller).to receive(:github_import_enabled?).and_return(true)
end
describe "GET new" do describe "GET new" do
it "redirects to GitHub for an access token if logged in with GitHub" do it_behaves_like 'a GitHub-ish import controller: GET new'
allow(controller).to receive(:logged_in_with_github?).and_return(true)
expect(controller).to receive(:go_to_github_for_permissions)
get :new it "redirects to GitHub for an access token if logged in with GitHub" do
end allow(controller).to receive(:logged_in_with_provider?).and_return(true)
expect(controller).to receive(:go_to_provider_for_permissions)
it "redirects to status if we already have a token" do
assign_session_token
allow(controller).to receive(:logged_in_with_github?).and_return(false)
get :new get :new
expect(controller).to redirect_to(status_import_github_url)
end end
end end
...@@ -51,196 +35,14 @@ describe Import::GithubController do ...@@ -51,196 +35,14 @@ describe Import::GithubController do
end end
describe "POST personal_access_token" do describe "POST personal_access_token" do
it "updates access token" do it_behaves_like 'a GitHub-ish import controller: POST personal_access_token'
token = "asdfasdf9876"
allow_any_instance_of(Gitlab::GithubImport::Client).
to receive(:user).and_return(true)
post :personal_access_token, personal_access_token: token
expect(session[:github_access_token]).to eq(token)
expect(controller).to redirect_to(status_import_github_url)
end
end end
describe "GET status" do describe "GET status" do
before do it_behaves_like 'a GitHub-ish import controller: GET status'
@repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim')
@org = OpenStruct.new(login: 'company')
@org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo')
assign_session_token
end
it "assigns variables" do
@project = create(:project, import_type: 'github', creator_id: user.id)
stub_client(repos: [@repo, @org_repo], orgs: [@org], org_repos: [@org_repo])
get :status
expect(assigns(:already_added_projects)).to eq([@project])
expect(assigns(:repos)).to eq([@repo, @org_repo])
end
it "does not show already added project" do
@project = create(:project, import_type: 'github', creator_id: user.id, import_source: 'asd/vim')
stub_client(repos: [@repo], orgs: [])
get :status
expect(assigns(:already_added_projects)).to eq([@project])
expect(assigns(:repos)).to eq([])
end
it "handles an invalid access token" do
allow_any_instance_of(Gitlab::GithubImport::Client).
to receive(:repos).and_raise(Octokit::Unauthorized)
get :status
expect(session[:github_access_token]).to eq(nil)
expect(controller).to redirect_to(new_import_github_url)
expect(flash[:alert]).to eq('Access denied to your GitHub account.')
end
end end
describe "POST create" do describe "POST create" do
let(:github_username) { user.username } it_behaves_like 'a GitHub-ish import controller: POST create'
let(:github_user) { OpenStruct.new(login: github_username) }
let(:github_repo) do
OpenStruct.new(
name: 'vim',
full_name: "#{github_username}/vim",
owner: OpenStruct.new(login: github_username)
)
end
before do
stub_client(user: github_user, repo: github_repo)
assign_session_token
end
context "when the repository owner is the GitHub user" do
context "when the GitHub user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
end
end
context "when the GitHub user and GitLab user's usernames don't match" do
let(:github_username) { "someone_else" }
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
end
end
end
context "when the repository owner is not the GitHub user" do
let(:other_username) { "someone_else" }
before do
github_repo.owner = OpenStruct.new(login: other_username)
assign_session_token
end
context "when a namespace with the GitHub user's username already exists" do
let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, github_repo.name, existing_namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
end
end
context "when the namespace is not owned by the GitLab user" do
before do
existing_namespace.owner = create(:user)
existing_namespace.save
end
it "creates a project using user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
end
end
end
context "when a namespace with the GitHub user's username doesn't exist" do
context "when current user can create namespaces" do
it "creates the namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).and_return(double(execute: true))
expect { post :create, target_namespace: github_repo.name, format: :js }.to change(Namespace, :count).by(1)
end
it "takes the new namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, github_repo.name, an_instance_of(Group), user, access_params).
and_return(double(execute: true))
post :create, target_namespace: github_repo.name, format: :js
end
end
context "when current user can't create namespaces" do
before do
user.update_attribute(:can_create_group, false)
end
it "doesn't create the namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).and_return(double(execute: true))
expect { post :create, format: :js }.not_to change(Namespace, :count)
end
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, github_repo.name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, format: :js
end
end
end
context 'user has chosen a namespace and name for the project' do
let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, test_name, test_namespace, user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
end
it 'takes the selected name and default namespace' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(github_repo, test_name, user.namespace, user, access_params).
and_return(double(execute: true))
post :create, { new_name: test_name, format: :js }
end
end
end
end end
end end
...@@ -71,6 +71,75 @@ describe Projects::EnvironmentsController do ...@@ -71,6 +71,75 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'GET #terminal' do
context 'with valid id' do
it 'responds with a status code 200' do
get :terminal, environment_params
expect(response).to have_http_status(200)
end
it 'loads the terminals for the enviroment' do
expect_any_instance_of(Environment).to receive(:terminals)
get :terminal, environment_params
end
end
context 'with invalid id' do
it 'responds with a status code 404' do
get :terminal, environment_params(id: 666)
expect(response).to have_http_status(404)
end
end
end
describe 'GET #terminal_websocket_authorize' do
context 'with valid workhorse signature' do
before do
allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
end
context 'and valid id' do
it 'returns the first terminal for the environment' do
expect_any_instance_of(Environment).
to receive(:terminals).
and_return([:fake_terminal])
expect(Gitlab::Workhorse).
to receive(:terminal_websocket).
with(:fake_terminal).
and_return(workhorse: :response)
get :terminal_websocket_authorize, environment_params
expect(response).to have_http_status(200)
expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(response.body).to eq('{"workhorse":"response"}')
end
end
context 'and invalid id' do
it 'returns 404' do
get :terminal_websocket_authorize, environment_params(id: 666)
expect(response).to have_http_status(404)
end
end
end
context 'with invalid workhorse signature' do
it 'aborts with an exception' do
allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError)
expect { get :terminal_websocket_authorize, environment_params }.to raise_error(JWT::DecodeError)
# controller tests don't set the response status correctly. It's enough
# to check that the action raised an exception
end
end
end
def environment_params(opts = {}) def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, opts.reverse_merge(namespace_id: project.namespace,
project_id: project, project_id: project,
......
...@@ -42,6 +42,12 @@ FactoryGirl.define do ...@@ -42,6 +42,12 @@ FactoryGirl.define do
end end
end end
trait :test_repo do
after :create do |project|
TestEnv.copy_repo(project)
end
end
# Nest Project Feature attributes # Nest Project Feature attributes
transient do transient do
wiki_access_level ProjectFeature::ENABLED wiki_access_level ProjectFeature::ENABLED
...@@ -91,9 +97,7 @@ FactoryGirl.define do ...@@ -91,9 +97,7 @@ FactoryGirl.define do
factory :project, parent: :empty_project do factory :project, parent: :empty_project do
path { 'gitlabhq' } path { 'gitlabhq' }
after :create do |project| test_repo
TestEnv.copy_repo(project)
end
end end
factory :forked_project_with_submodules, parent: :empty_project do factory :forked_project_with_submodules, parent: :empty_project do
...@@ -140,7 +144,7 @@ FactoryGirl.define do ...@@ -140,7 +144,7 @@ FactoryGirl.define do
active: true, active: true,
properties: { properties: {
namespace: project.path, namespace: project.path,
api_url: 'https://kubernetes.example.com/api', api_url: 'https://kubernetes.example.com',
token: 'a' * 40, token: 'a' * 40,
} }
) )
......
require 'spec_helper' require 'spec_helper'
describe "Admin::Projects", feature: true do describe "Admin::Projects", feature: true do
before do include Select2Helper
@project = create(:project)
let(:user) { create :user }
let!(:project) { create(:project) }
let!(:current_user) do
login_as :admin login_as :admin
end end
describe "GET /admin/projects" do describe "GET /admin/projects" do
let!(:archived_project) { create :project, :public, archived: true }
before do before do
visit admin_projects_path visit admin_projects_path
end end
...@@ -15,20 +20,98 @@ describe "Admin::Projects", feature: true do ...@@ -15,20 +20,98 @@ describe "Admin::Projects", feature: true do
expect(current_path).to eq(admin_projects_path) expect(current_path).to eq(admin_projects_path)
end end
it "has projects list" do it 'renders projects list without archived project' do
expect(page).to have_content(@project.name) expect(page).to have_content(project.name)
expect(page).not_to have_content(archived_project.name)
end
it 'renders all projects', js: true do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects'
expect(page).to have_content(project.name)
expect(page).to have_content(archived_project.name)
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end end
end end
describe "GET /admin/projects/:id" do describe "GET /admin/projects/:namespace_id/:id" do
before do before do
visit admin_projects_path visit admin_projects_path
click_link "#{@project.name}" click_link "#{project.name}"
end
it do
expect(current_path).to eq admin_namespace_project_path(project.namespace, project)
end end
it "has project info" do it "has project info" do
expect(page).to have_content(@project.path) expect(page).to have_content(project.path)
expect(page).to have_content(@project.name) expect(page).to have_content(project.name)
expect(page).to have_content(project.name_with_namespace)
expect(page).to have_content(project.creator.name)
end
end
describe 'transfer project' do
before do
create(:group, name: 'Web')
allow_any_instance_of(Projects::TransferService).
to receive(:move_uploads_to_new_namespace).and_return(true)
end
it 'transfers project to group web', js: true do
visit admin_namespace_project_path(project.namespace, project)
click_button 'Search for Namespace'
click_link 'group: web'
click_button 'Transfer'
expect(page).to have_content("Web / #{project.name}")
expect(page).to have_content('Namespace: Web')
end
end
describe 'add admin himself to a project' do
before do
project.team << [user, :master]
end
it 'adds admin a to a project as developer', js: true do
visit namespace_project_project_members_path(project.namespace, project)
page.within '.users-project-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
click_button 'Add to project'
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
end
end
describe 'admin remove himself from a project' do
before do
project.team << [user, :master]
project.team << [current_user, :developer]
end
it 'removes admin from the project' do
visit namespace_project_project_members_path(project.namespace, project)
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
expect(page).not_to have_selector(:css, '.content-list')
end end
end end
end end
...@@ -38,6 +38,10 @@ feature 'Environment', :feature do ...@@ -38,6 +38,10 @@ feature 'Environment', :feature do
scenario 'does not show a re-deploy button for deployment without build' do scenario 'does not show a re-deploy button for deployment without build' do
expect(page).not_to have_link('Re-deploy') expect(page).not_to have_link('Re-deploy')
end end
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end end
context 'with related deployable present' do context 'with related deployable present' do
...@@ -60,6 +64,10 @@ feature 'Environment', :feature do ...@@ -60,6 +64,10 @@ feature 'Environment', :feature do
expect(page).not_to have_link('Stop') expect(page).not_to have_link('Stop')
end end
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
context 'with manual action' do context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
...@@ -84,6 +92,26 @@ feature 'Environment', :feature do ...@@ -84,6 +92,26 @@ feature 'Environment', :feature do
end end
end end
context 'with terminal' do
let(:project) { create(:kubernetes_project, :test_repo) }
context 'for project master' do
let(:role) { :master }
scenario 'it shows the terminal button' do
expect(page).to have_terminal_button
end
end
context 'for developer' do
let(:role) { :developer }
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end
end
context 'with stop action' do context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
...@@ -158,4 +186,8 @@ feature 'Environment', :feature do ...@@ -158,4 +186,8 @@ feature 'Environment', :feature do
environment.project, environment.project,
environment) environment)
end end
def have_terminal_button
have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
end
end end
...@@ -113,6 +113,10 @@ feature 'Environments page', :feature, :js do ...@@ -113,6 +113,10 @@ feature 'Environments page', :feature, :js do
expect(page).not_to have_css('external-url') expect(page).not_to have_css('external-url')
end end
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
context 'with external_url' do context 'with external_url' do
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
given(:build) { create(:ci_build, pipeline: pipeline) } given(:build) { create(:ci_build, pipeline: pipeline) }
...@@ -145,6 +149,26 @@ feature 'Environments page', :feature, :js do ...@@ -145,6 +149,26 @@ feature 'Environments page', :feature, :js do
end end
end end
end end
context 'with terminal' do
let(:project) { create(:kubernetes_project, :test_repo) }
context 'for project master' do
let(:role) { :master }
scenario 'it shows the terminal button' do
expect(page).to have_terminal_button
end
end
context 'for developer' do
let(:role) { :developer }
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end
end
end end
end end
end end
...@@ -195,6 +219,10 @@ feature 'Environments page', :feature, :js do ...@@ -195,6 +219,10 @@ feature 'Environments page', :feature, :js do
end end
end end
def have_terminal_button
have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment))
end
def visit_environments(project) def visit_environments(project)
visit namespace_project_environments_path(project.namespace, project) visit namespace_project_environments_path(project.namespace, project)
end end
......
...@@ -24,7 +24,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: ...@@ -24,7 +24,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
click_on 'Add to project' click_on 'Add to project'
end end
page.within '.project_member:first-child' do page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days') expect(page).to have_content('Expires in 4 days')
end end
end end
...@@ -35,7 +35,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: ...@@ -35,7 +35,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
visit namespace_project_project_members_path(project.namespace, project) visit namespace_project_project_members_path(project.namespace, project)
page.within '.project_member:first-child' do page.within "#project_member_#{new_member.project_members.first.id}" do
find('.js-access-expiration-date').set '2016-08-09' find('.js-access-expiration-date').set '2016-08-09'
wait_for_ajax wait_for_ajax
expect(page).to have_content('Expires in 3 days') expect(page).to have_content('Expires in 3 days')
......
...@@ -25,24 +25,37 @@ describe ImportHelper do ...@@ -25,24 +25,37 @@ describe ImportHelper do
end end
end end
describe '#github_project_link' do describe '#provider_project_link' do
context 'when provider does not specify a custom URL' do context 'when provider is "github"' do
it 'uses default GitHub URL' do context 'when provider does not specify a custom URL' do
allow(Gitlab.config.omniauth).to receive(:providers). it 'uses default GitHub URL' do
allow(Gitlab.config.omniauth).to receive(:providers).
and_return([Settingslogic.new('name' => 'github')]) and_return([Settingslogic.new('name' => 'github')])
expect(helper.github_project_link('octocat/Hello-World')). expect(helper.provider_project_link('github', 'octocat/Hello-World')).
to include('href="https://github.com/octocat/Hello-World"') to include('href="https://github.com/octocat/Hello-World"')
end
end end
end
context 'when provider specify a custom URL' do context 'when provider specify a custom URL' do
it 'uses custom URL' do it 'uses custom URL' do
allow(Gitlab.config.omniauth).to receive(:providers). allow(Gitlab.config.omniauth).to receive(:providers).
and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')]) and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')])
expect(helper.github_project_link('octocat/Hello-World')). expect(helper.provider_project_link('github', 'octocat/Hello-World')).
to include('href="https://github.company.com/octocat/Hello-World"') to include('href="https://github.company.com/octocat/Hello-World"')
end
end
end
context 'when provider is "gitea"' do
before do
assign(:gitea_host_url, 'https://try.gitea.io/')
end
it 'uses given host' do
expect(helper.provider_project_link('gitea', 'octocat/Hello-World')).
to include('href="https://try.gitea.io/octocat/Hello-World"')
end end
end end
end end
......
...@@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do ...@@ -45,20 +45,46 @@ describe Gitlab::GithubImport::Client, lib: true do
end end
end end
context 'when provider does not specity an API endpoint' do describe '#api_endpoint' do
it 'uses GitHub root API endpoint' do context 'when provider does not specity an API endpoint' do
expect(client.api.api_endpoint).to eq 'https://api.github.com/' it 'uses GitHub root API endpoint' do
expect(client.api.api_endpoint).to eq 'https://api.github.com/'
end
end end
end
context 'when provider specify a custom API endpoint' do context 'when provider specify a custom API endpoint' do
before do before do
github_provider['args']['client_options']['site'] = 'https://github.company.com/' github_provider['args']['client_options']['site'] = 'https://github.company.com/'
end
it 'uses the custom API endpoint' do
expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
expect(client.api.api_endpoint).to eq 'https://github.company.com/'
end
end
context 'when given a host' do
subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') }
it 'builds a endpoint with the given host and the default API version' do
expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
end
end end
it 'uses the custom API endpoint' do context 'when given an API version' do
expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) subject(:client) { described_class.new(token, api_version: 'v3') }
expect(client.api.api_endpoint).to eq 'https://github.company.com/'
it 'does not use the API version without a host' do
expect(client.api.api_endpoint).to eq 'https://api.github.com/'
end
end
context 'when given a host and version' do
subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') }
it 'builds a endpoint with the given options' do
expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GithubImport::Importer, lib: true do describe Gitlab::GithubImport::Importer, lib: true do
describe '#execute' do shared_examples 'Gitlab::GithubImport::Importer#execute' do
let(:expected_not_called) { [] }
before do before do
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) allow(project).to receive(:import_data).and_return(double.as_null_object)
end end
context 'when an error occurs' do it 'calls import methods' do
let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) } importer = described_class.new(project)
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
let(:label1) do
double(
name: 'Bug',
color: 'ff0000',
url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
)
end
let(:label2) do expected_called = [
double( :import_labels, :import_milestones, :import_pull_requests, :import_issues,
name: nil, :import_wiki, :import_releases, :handle_errors
color: 'ff0000', ]
url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
)
end
let(:milestone) do expected_called -= expected_not_called
double(
number: 1347,
state: 'open',
title: '1.0',
description: 'Version 1.0',
due_on: nil,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1'
)
end
let(:issue1) do aggregate_failures do
double( expected_called.each do |method_name|
number: 1347, expect(importer).to receive(method_name)
milestone: nil, end
state: 'open',
title: 'Found a bug',
body: "I'm having a problem with this.",
assignee: nil,
user: octocat,
comments: 0,
pull_request: nil,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
labels: [double(name: 'Label #1')],
)
end
let(:issue2) do expect(importer).to receive(:import_comments).with(:issues)
double( expect(importer).to receive(:import_comments).with(:pull_requests)
number: 1348,
milestone: nil,
state: 'open',
title: nil,
body: "I'm having a problem with this.",
assignee: nil,
user: octocat,
comments: 0,
pull_request: nil,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348',
labels: [double(name: 'Label #2')],
)
end
let(:pull_request) do expected_not_called.each do |method_name|
double( expect(importer).not_to receive(method_name)
number: 1347, end
milestone: nil,
state: 'open',
title: 'New feature',
body: 'Please pull these awesome changes',
head: source_branch,
base: target_branch,
assignee: nil,
user: octocat,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
)
end end
let(:release1) do importer.execute
double( end
tag_name: 'v1.0.0', end
name: 'First release',
body: 'Release v1.0.0',
draft: false,
created_at: created_at,
updated_at: updated_at,
url: 'https://api.github.com/repos/octocat/Hello-World/releases/1'
)
end
let(:release2) do shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do
double( before do
tag_name: 'v2.0.0', allow(project).to receive(:import_data).and_return(double.as_null_object)
name: 'Second release',
body: nil,
draft: false,
created_at: created_at,
updated_at: updated_at,
url: 'https://api.github.com/repos/octocat/Hello-World/releases/2'
)
end
before do allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
allow(project).to receive(:import_data).and_return(double.as_null_object)
allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request]) allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([]) allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([]) allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
end allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
end
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:label1) do
double(
name: 'Bug',
color: 'ff0000',
url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
)
end
let(:label2) do
double(
name: nil,
color: 'ff0000',
url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
)
end
let(:milestone) do
double(
id: 1347, # For Gitea
number: 1347,
state: 'open',
title: '1.0',
description: 'Version 1.0',
due_on: nil,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/milestones/1"
)
end
it 'returns true' do let(:issue1) do
expect(described_class.new(project).execute).to eq true double(
number: 1347,
milestone: nil,
state: 'open',
title: 'Found a bug',
body: "I'm having a problem with this.",
assignee: nil,
user: octocat,
comments: 0,
pull_request: nil,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/issues/1347",
labels: [double(name: 'Label #1')]
)
end
let(:issue2) do
double(
number: 1348,
milestone: nil,
state: 'open',
title: nil,
body: "I'm having a problem with this.",
assignee: nil,
user: octocat,
comments: 0,
pull_request: nil,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/issues/1348",
labels: [double(name: 'Label #2')]
)
end
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
let(:pull_request) do
double(
number: 1347,
milestone: nil,
state: 'open',
title: 'New feature',
body: 'Please pull these awesome changes',
head: source_branch,
base: target_branch,
assignee: nil,
user: octocat,
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
labels: [double(name: 'Label #2')]
)
end
let(:release1) do
double(
tag_name: 'v1.0.0',
name: 'First release',
body: 'Release v1.0.0',
draft: false,
created_at: created_at,
updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/1"
)
end
let(:release2) do
double(
tag_name: 'v2.0.0',
name: 'Second release',
body: nil,
draft: false,
created_at: created_at,
updated_at: updated_at,
url: "#{api_root}/repos/octocat/Hello-World/releases/2"
)
end
it 'returns true' do
expect(described_class.new(project).execute).to eq true
end
it 'does not raise an error' do
expect { described_class.new(project).execute }.not_to raise_error
end
it 'stores error messages' do
error = {
message: 'The remote data could not be fully imported.',
errors: [
{ type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
{ type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
{ type: :wiki, errors: "Gitlab::Shell::Error" }
]
}
unless project.gitea_import?
error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" }
end end
it 'does not raise an error' do described_class.new(project).execute
expect { described_class.new(project).execute }.not_to raise_error
expect(project.import_error).to eq error.to_json
end
end
let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) }
let(:credentials) { { user: 'joe' } }
context 'when importing a GitHub project' do
let(:api_root) { 'https://api.github.com' }
let(:repo_root) { 'https://github.com' }
it_behaves_like 'Gitlab::GithubImport::Importer#execute'
it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
describe '#client' do
it 'instantiates a Client' do
allow(project).to receive(:import_data).and_return(double(credentials: credentials))
expect(Gitlab::GithubImport::Client).to receive(:new).with(
credentials[:user],
{}
)
described_class.new(project).client
end end
end
end
it 'stores error messages' do context 'when importing a Gitea project' do
error = { let(:api_root) { 'https://try.gitea.io/api/v1' }
message: 'The remote data could not be fully imported.', let(:repo_root) { 'https://try.gitea.io' }
errors: [ before do
{ type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" }, project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" }, end
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
]
}
described_class.new(project).execute it_behaves_like 'Gitlab::GithubImport::Importer#execute' do
let(:expected_not_called) { [:import_releases] }
end
it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
describe '#client' do
it 'instantiates a Client' do
allow(project).to receive(:import_data).and_return(double(credentials: credentials))
expect(Gitlab::GithubImport::Client).to receive(:new).with(
credentials[:user],
{ host: "#{repo_root}:443/foo", api_version: 'v1' }
)
expect(project.import_error).to eq error.to_json described_class.new(project).client
end end
end end
end end
......
require 'spec_helper'
describe Gitlab::GithubImport::IssuableFormatter, lib: true do
let(:raw_data) do
double(number: 42)
end
let(:project) { double(import_type: 'github') }
let(:issuable_formatter) { described_class.new(project, raw_data) }
describe '#project_association' do
it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) }
end
describe '#number' do
it { expect(issuable_formatter.number).to eq(42) }
end
describe '#find_condition' do
it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) }
end
end
...@@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -23,9 +23,9 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
} }
end end
subject(:issue) { described_class.new(project, raw_data)} subject(:issue) { described_class.new(project, raw_data) }
describe '#attributes' do shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
context 'when issue is open' do context 'when issue is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) } let(:raw_data) { double(base_data.merge(state: 'open')) }
...@@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -83,7 +83,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end end
context 'when it has a milestone' do context 'when it has a milestone' do
let(:milestone) { double(number: 45) } let(:milestone) { double(id: 42, number: 42) }
let(:raw_data) { double(base_data.merge(milestone: milestone)) } let(:raw_data) { double(base_data.merge(milestone: milestone)) }
it 'returns nil when milestone does not exist' do it 'returns nil when milestone does not exist' do
...@@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -91,7 +91,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end end
it 'returns milestone when it exists' do it 'returns milestone when it exists' do
milestone = create(:milestone, project: project, iid: 45) milestone = create(:milestone, project: project, iid: 42)
expect(issue.attributes.fetch(:milestone)).to eq milestone expect(issue.attributes.fetch(:milestone)).to eq milestone
end end
...@@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -118,6 +118,28 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end end
end end
shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do
let(:raw_data) { double(base_data.merge(number: 1347)) }
it 'returns issue number' do
expect(issue.number).to eq 1347
end
end
context 'when importing a GitHub project' do
it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
end
context 'when importing a Gitea project' do
before do
project.update(import_type: 'gitea')
end
it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
end
describe '#has_comments?' do describe '#has_comments?' do
context 'when number of comments is greater than zero' do context 'when number of comments is greater than zero' do
let(:raw_data) { double(base_data.merge(comments: 1)) } let(:raw_data) { double(base_data.merge(comments: 1)) }
...@@ -136,14 +158,6 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -136,14 +158,6 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end end
end end
describe '#number' do
let(:raw_data) { double(base_data.merge(number: 1347)) }
it 'returns pull request number' do
expect(issue.number).to eq 1347
end
end
describe '#pull_request?' do describe '#pull_request?' do
context 'when mention a pull request' do context 'when mention a pull request' do
let(:raw_data) { double(base_data.merge(pull_request: double)) } let(:raw_data) { double(base_data.merge(pull_request: double)) }
......
...@@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -6,7 +6,6 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:base_data) do let(:base_data) do
{ {
number: 1347,
state: 'open', state: 'open',
title: '1.0', title: '1.0',
description: 'Version 1.0', description: 'Version 1.0',
...@@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -16,12 +15,15 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
closed_at: nil closed_at: nil
} }
end end
let(:iid_attr) { :number }
subject(:formatter) { described_class.new(project, raw_data)} subject(:formatter) { described_class.new(project, raw_data) }
shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do
let(:data) { base_data.merge(iid_attr => 1347) }
describe '#attributes' do
context 'when milestone is open' do context 'when milestone is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) } let(:raw_data) { double(data.merge(state: 'open')) }
it 'returns formatted attributes' do it 'returns formatted attributes' do
expected = { expected = {
...@@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -40,7 +42,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
end end
context 'when milestone is closed' do context 'when milestone is closed' do
let(:raw_data) { double(base_data.merge(state: 'closed')) } let(:raw_data) { double(data.merge(state: 'closed')) }
it 'returns formatted attributes' do it 'returns formatted attributes' do
expected = { expected = {
...@@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -60,7 +62,7 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
context 'when milestone has a due date' do context 'when milestone has a due date' do
let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') } let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') }
let(:raw_data) { double(base_data.merge(due_on: due_date)) } let(:raw_data) { double(data.merge(due_on: due_date)) }
it 'returns formatted attributes' do it 'returns formatted attributes' do
expected = { expected = {
...@@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do ...@@ -78,4 +80,17 @@ describe Gitlab::GithubImport::MilestoneFormatter, lib: true do
end end
end end
end end
context 'when importing a GitHub project' do
it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
end
context 'when importing a Gitea project' do
let(:iid_attr) { :id }
before do
project.update(import_type: 'gitea')
end
it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
end
end end
...@@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -32,9 +32,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
} }
end end
subject(:pull_request) { described_class.new(project, raw_data)} subject(:pull_request) { described_class.new(project, raw_data) }
describe '#attributes' do shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
context 'when pull request is open' do context 'when pull request is open' do
let(:raw_data) { double(base_data.merge(state: 'open')) } let(:raw_data) { double(base_data.merge(state: 'open')) }
...@@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -149,7 +149,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
context 'when it has a milestone' do context 'when it has a milestone' do
let(:milestone) { double(number: 45) } let(:milestone) { double(id: 42, number: 42) }
let(:raw_data) { double(base_data.merge(milestone: milestone)) } let(:raw_data) { double(base_data.merge(milestone: milestone)) }
it 'returns nil when milestone does not exist' do it 'returns nil when milestone does not exist' do
...@@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -157,22 +157,22 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
it 'returns milestone when it exists' do it 'returns milestone when it exists' do
milestone = create(:milestone, project: project, iid: 45) milestone = create(:milestone, project: project, iid: 42)
expect(pull_request.attributes.fetch(:milestone)).to eq milestone expect(pull_request.attributes.fetch(:milestone)).to eq milestone
end end
end end
end end
describe '#number' do shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do
let(:raw_data) { double(base_data.merge(number: 1347)) } let(:raw_data) { double(base_data) }
it 'returns pull request number' do it 'returns pull request number' do
expect(pull_request.number).to eq 1347 expect(pull_request.number).to eq 1347
end end
end end
describe '#source_branch_name' do shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do
context 'when source branch exists' do context 'when source branch exists' do
let(:raw_data) { double(base_data) } let(:raw_data) { double(base_data) }
...@@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -190,7 +190,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
end end
describe '#target_branch_name' do shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
context 'when source branch exists' do context 'when source branch exists' do
let(:raw_data) { double(base_data) } let(:raw_data) { double(base_data) }
...@@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -208,6 +208,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end end
end end
context 'when importing a GitHub project' do
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
end
context 'when importing a Gitea project' do
before do
project.update(import_type: 'gitea')
end
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
end
describe '#valid?' do describe '#valid?' do
context 'when source, and target repos are not a fork' do context 'when source, and target repos are not a fork' do
let(:raw_data) { double(base_data) } let(:raw_data) { double(base_data) }
......
require 'spec_helper'
describe Gitlab::ImportSources do
describe '.options' do
it 'returns a hash' do
expected =
{
'GitHub' => 'github',
'Bitbucket' => 'bitbucket',
'GitLab.com' => 'gitlab',
'Google Code' => 'google_code',
'FogBugz' => 'fogbugz',
'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project',
'Gitea' => 'gitea'
}
expect(described_class.options).to eq(expected)
end
end
describe '.values' do
it 'returns an array' do
expected =
[
'github',
'bitbucket',
'gitlab',
'google_code',
'fogbugz',
'git',
'gitlab_project',
'gitea'
]
expect(described_class.values).to eq(expected)
end
end
describe '.importer_names' do
it 'returns an array of importer names' do
expected =
[
'github',
'bitbucket',
'gitlab',
'google_code',
'fogbugz',
'gitlab_project',
'gitea'
]
expect(described_class.importer_names).to eq(expected)
end
end
describe '.importer' do
import_sources = {
'github' => Gitlab::GithubImport::Importer,
'bitbucket' => Gitlab::BitbucketImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
'google_code' => Gitlab::GoogleCodeImport::Importer,
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
'gitea' => Gitlab::GithubImport::Importer
}
import_sources.each do |name, klass|
it "returns #{klass} when given #{name}" do
expect(described_class.importer(name)).to eq(klass)
end
end
end
describe '.title' do
import_sources = {
'github' => 'GitHub',
'bitbucket' => 'Bitbucket',
'gitlab' => 'GitLab.com',
'google_code' => 'Google Code',
'fogbugz' => 'FogBugz',
'git' => 'Repo by URL',
'gitlab_project' => 'GitLab export',
'gitea' => 'Gitea'
}
import_sources.each do |name, title|
it "returns #{title} when given #{name}" do
expect(described_class.title(name)).to eq(title)
end
end
end
end
require 'spec_helper'
describe Gitlab::Kubernetes do
include described_class
describe '#container_exec_url' do
let(:api_url) { 'https://example.com' }
let(:namespace) { 'default' }
let(:pod_name) { 'pod1' }
let(:container_name) { 'container1' }
subject(:result) { URI::parse(container_exec_url(api_url, namespace, pod_name, container_name)) }
it { expect(result.scheme).to eq('wss') }
it { expect(result.host).to eq('example.com') }
it { expect(result.path).to eq('/api/v1/namespaces/default/pods/pod1/exec') }
it { expect(result.query).to eq('container=container1&stderr=true&stdin=true&stdout=true&tty=true&command=sh&command=-c&command=bash+%7C%7C+sh') }
context 'with a HTTP API URL' do
let(:api_url) { 'http://example.com' }
it { expect(result.scheme).to eq('ws') }
end
context 'with a path prefix in the API URL' do
let(:api_url) { 'https://example.com/prefix/' }
it { expect(result.path).to eq('/prefix/api/v1/namespaces/default/pods/pod1/exec') }
end
context 'with arguments that need urlencoding' do
let(:namespace) { 'default namespace' }
let(:pod_name) { 'pod 1' }
let(:container_name) { 'container 1' }
it { expect(result.path).to eq('/api/v1/namespaces/default%20namespace/pods/pod%201/exec') }
it { expect(result.query).to match(/\Acontainer=container\+1&/) }
end
end
end
...@@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do ...@@ -37,6 +37,42 @@ describe Gitlab::Workhorse, lib: true do
end end
end end
describe '.terminal_websocket' do
def terminal(ca_pem: nil)
out = {
subprotocols: ['foo'],
url: 'wss://example.com/terminal.ws',
headers: { 'Authorization' => ['Token x'] }
}
out[:ca_pem] = ca_pem if ca_pem
out
end
def workhorse(ca_pem: nil)
out = {
'Terminal' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
'Header' => { 'Authorization' => ['Token x'] }
}
}
out['Terminal']['CAPem'] = ca_pem if ca_pem
out
end
context 'without ca_pem' do
subject { Gitlab::Workhorse.terminal_websocket(terminal) }
it { is_expected.to eq(workhorse) }
end
context 'with ca_pem' do
subject { Gitlab::Workhorse.terminal_websocket(terminal(ca_pem: "foo")) }
it { is_expected.to eq(workhorse(ca_pem: "foo")) }
end
end
describe '.send_git_diff' do describe '.send_git_diff' do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") } let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) } subject { described_class.send_git_patch(repository, diff_refs) }
......
require 'spec_helper'
describe ReactiveCaching, caching: true do
include ReactiveCachingHelpers
class CacheTest
include ReactiveCaching
self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
self.reactive_cache_lifetime = 5.minutes
self.reactive_cache_refresh_interval = 15.seconds
attr_reader :id
def initialize(id, &blk)
@id = id
@calculator = blk
end
def calculate_reactive_cache
@calculator.call
end
def result
with_reactive_cache do |data|
data / 2
end
end
end
let(:now) { Time.now.utc }
around(:each) do |example|
Timecop.freeze(now) { example.run }
end
let(:calculation) { -> { 2 + 2 } }
let(:cache_key) { "foo:666" }
let(:instance) { CacheTest.new(666, &calculation) }
describe '#with_reactive_cache' do
before { stub_reactive_cache }
subject(:go!) { instance.result }
context 'when cache is empty' do
it { is_expected.to be_nil }
it 'queues a background worker' do
expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
go!
end
it 'updates the cache lifespan' do
go!
expect(reactive_cache_alive?(instance)).to be_truthy
end
end
context 'when the cache is full' do
before { stub_reactive_cache(instance, 4) }
it { is_expected.to eq(2) }
context 'and expired' do
before { invalidate_reactive_cache(instance) }
it { is_expected.to be_nil }
end
end
end
describe '#clear_reactive_cache!' do
before do
stub_reactive_cache(instance, 4)
instance.clear_reactive_cache!
end
it { expect(instance.result).to be_nil }
end
describe '#exclusively_update_reactive_cache!' do
subject(:go!) { instance.exclusively_update_reactive_cache! }
context 'when the lease is free and lifetime is not exceeded' do
before { stub_reactive_cache(instance, "preexisting") }
it 'takes and releases the lease' do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000")
expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000")
go!
end
it 'caches the result of #calculate_reactive_cache' do
go!
expect(read_reactive_cache(instance)).to eq(calculation.call)
end
it "enqueues a repeat worker" do
expect_reactive_cache_update_queued(instance)
go!
end
context 'and #calculate_reactive_cache raises an exception' do
before { stub_reactive_cache(instance, "preexisting") }
let(:calculation) { -> { raise "foo"} }
it 'leaves the cache untouched' do
expect { go! }.to raise_error("foo")
expect(read_reactive_cache(instance)).to eq("preexisting")
end
it 'enqueues a repeat worker' do
expect_reactive_cache_update_queued(instance)
expect { go! }.to raise_error("foo")
end
end
end
context 'when lifetime is exceeded' do
it 'skips the calculation' do
expect(instance).to receive(:calculate_reactive_cache).never
go!
end
end
context 'when the lease is already taken' do
before do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil)
end
it 'skips the calculation' do
expect(instance).to receive(:calculate_reactive_cache).never
go!
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Environment, models: true do describe Environment, models: true do
subject(:environment) { create(:environment) } let(:project) { create(:empty_project) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) } it { is_expected.to have_many(:deployments) }
...@@ -31,6 +32,8 @@ describe Environment, models: true do ...@@ -31,6 +32,8 @@ describe Environment, models: true do
end end
describe '#includes_commit?' do describe '#includes_commit?' do
let(:project) { create(:project) }
context 'without a last deployment' do context 'without a last deployment' do
it "returns false" do it "returns false" do
expect(environment.includes_commit?('HEAD')).to be false expect(environment.includes_commit?('HEAD')).to be false
...@@ -38,9 +41,6 @@ describe Environment, models: true do ...@@ -38,9 +41,6 @@ describe Environment, models: true do
end end
context 'with a last deployment' do context 'with a last deployment' do
let(:project) { create(:project) }
let(:environment) { create(:environment, project: project) }
let!(:deployment) do let!(:deployment) do
create(:deployment, environment: environment, sha: project.commit('master').id) create(:deployment, environment: environment, sha: project.commit('master').id)
end end
...@@ -65,7 +65,6 @@ describe Environment, models: true do ...@@ -65,7 +65,6 @@ describe Environment, models: true do
describe '#first_deployment_for' do describe '#first_deployment_for' do
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) }
let(:head_commit) { project.commit } let(:head_commit) { project.commit }
...@@ -196,6 +195,57 @@ describe Environment, models: true do ...@@ -196,6 +195,57 @@ describe Environment, models: true do
end end
end end
describe '#has_terminals?' do
subject { environment.has_terminals? }
context 'when the enviroment is available' do
context 'with a deployment service' do
let(:project) { create(:kubernetes_project) }
context 'and a deployment' do
let!(:deployment) { create(:deployment, environment: environment) }
it { is_expected.to be_truthy }
end
context 'but no deployments' do
it { is_expected.to be_falsy }
end
end
context 'without a deployment service' do
it { is_expected.to be_falsy }
end
end
context 'when the environment is unavailable' do
let(:project) { create(:kubernetes_project) }
before { environment.stop }
it { is_expected.to be_falsy }
end
end
describe '#terminals' do
let(:project) { create(:kubernetes_project) }
subject { environment.terminals }
context 'when the environment has terminals' do
before { allow(environment).to receive(:has_terminals?).and_return(true) }
it 'returns the terminals from the deployment service' do
expect(project.deployment_service).
to receive(:terminals).with(environment).
and_return(:fake_terminals)
is_expected.to eq(:fake_terminals)
end
end
context 'when the environment does not have terminals' do
before { allow(environment).to receive(:has_terminals?).and_return(false) }
it { is_expected.to eq(nil) }
end
end
describe '#slug' do describe '#slug' do
it "is automatically generated" do it "is automatically generated" do
expect(environment.slug).not_to be_nil expect(environment.slug).not_to be_nil
......
require 'spec_helper'
describe ProjectAuthorization do
let(:user) { create(:user) }
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
describe '.insert_authorizations' do
it 'inserts the authorizations' do
described_class.
insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]])
expect(user.project_authorizations.count).to eq(1)
end
it 'inserts rows in batches' do
described_class.insert_authorizations([
[user.id, project1.id, Gitlab::Access::MASTER],
[user.id, project2.id, Gitlab::Access::MASTER],
], 1)
expect(user.project_authorizations.count).to eq(2)
end
end
end
require 'spec_helper' require 'spec_helper'
describe KubernetesService, models: true do describe KubernetesService, models: true, caching: true do
let(:project) { create(:empty_project) } include KubernetesHelpers
include ReactiveCachingHelpers
let(:project) { create(:kubernetes_project) }
let(:service) { project.kubernetes_service }
# We use Kubeclient to interactive with the Kubernetes API. It will
# GET /api/v1 for a list of resources the API supports. This must be stubbed
# in addition to any other HTTP requests we expect it to perform.
let(:discovery_url) { service.api_url + '/api/v1' }
let(:discovery_response) { { body: kube_discovery_body.to_json } }
let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" }
let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } }
def stub_kubeclient_discover
WebMock.stub_request(:get, discovery_url).to_return(discovery_response)
end
def stub_kubeclient_pods
stub_kubeclient_discover
WebMock.stub_request(:get, pods_url).to_return(pods_response)
end
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
...@@ -65,22 +87,15 @@ describe KubernetesService, models: true do ...@@ -65,22 +87,15 @@ describe KubernetesService, models: true do
end end
describe '#test' do describe '#test' do
let(:project) { create(:kubernetes_project) } before do
let(:service) { project.kubernetes_service } stub_kubeclient_discover
let(:discovery_url) { service.api_url + '/api/v1' } end
# JSON response body from Kubernetes GET /api/v1 request
let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json }
context 'with path prefix in api_url' do context 'with path prefix in api_url' do
let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
before do
service.api_url = 'https://kubernetes.example.com/prefix/'
end
it 'tests with the prefix' do it 'tests with the prefix' do
WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) service.api_url = 'https://kubernetes.example.com/prefix/'
expect(service.test[:success]).to be_truthy expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
...@@ -88,17 +103,12 @@ describe KubernetesService, models: true do ...@@ -88,17 +103,12 @@ describe KubernetesService, models: true do
end end
context 'with custom CA certificate' do context 'with custom CA certificate' do
let(:certificate) { "CA PEM DATA" }
before do
service.update_attributes!(ca_pem: certificate)
end
it 'is added to the certificate store' do it 'is added to the certificate store' do
cert = double("certificate") service.ca_pem = "CA PEM DATA"
expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert) cert = double("certificate")
expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert)
expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)
WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
expect(service.test[:success]).to be_truthy expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
...@@ -107,17 +117,15 @@ describe KubernetesService, models: true do ...@@ -107,17 +117,15 @@ describe KubernetesService, models: true do
context 'success' do context 'success' do
it 'reads the discovery endpoint' do it 'reads the discovery endpoint' do
WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
expect(service.test[:success]).to be_truthy expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
end end
end end
context 'failure' do context 'failure' do
it 'fails to read the discovery endpoint' do let(:discovery_response) { { status: 404 } }
WebMock.stub_request(:get, discovery_url).to_return(status: 404)
it 'fails to read the discovery endpoint' do
expect(service.test[:success]).to be_falsy expect(service.test[:success]).to be_falsy
expect(WebMock).to have_requested(:get, discovery_url).once expect(WebMock).to have_requested(:get, discovery_url).once
end end
...@@ -156,4 +164,55 @@ describe KubernetesService, models: true do ...@@ -156,4 +164,55 @@ describe KubernetesService, models: true do
) )
end end
end end
describe '#terminals' do
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
subject { service.terminals(environment) }
context 'with invalid pods' do
it 'returns no terminals' do
stub_reactive_cache(service, pods: [ { "bad" => "pod" } ])
is_expected.to be_empty
end
end
context 'with valid pods' do
let(:pod) { kube_pod(app: environment.slug) }
let(:terminals) { kube_terminals(service, pod) }
it 'returns terminals' do
stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ])
is_expected.to eq(terminals + terminals)
end
end
end
describe '#calculate_reactive_cache' do
before { stub_kubeclient_pods }
subject { service.calculate_reactive_cache }
context 'when service is inactive' do
before { service.active = false }
it { is_expected.to be_nil }
end
context 'when kubernetes responds with valid pods' do
it { is_expected.to eq(pods: [kube_pod]) }
end
context 'when kubernetes responds with 500' do
let(:pods_response) { { status: 500 } }
it { expect { subject }.to raise_error(KubeException) }
end
context 'when kubernetes responds with 404' do
let(:pods_response) { { status: 404 } }
it { is_expected.to eq(pods: []) }
end
end
end end
...@@ -1458,6 +1458,18 @@ describe Project, models: true do ...@@ -1458,6 +1458,18 @@ describe Project, models: true do
end end
end end
describe '#gitlab_project_import?' do
subject(:project) { build(:project, import_type: 'gitlab_project') }
it { expect(project.gitlab_project_import?).to be true }
end
describe '#gitea_import?' do
subject(:project) { build(:project, import_type: 'gitea') }
it { expect(project.gitea_import?).to be true }
end
describe '#lfs_enabled?' do describe '#lfs_enabled?' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -4,7 +4,14 @@ describe API::Files, api: true do ...@@ -4,7 +4,14 @@ describe API::Files, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) } let!(:project) { create(:project, namespace: user.namespace ) }
let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let(:file_path) { 'files/ruby/popen.rb' } let(:file_path) { 'files/ruby/popen.rb' }
let(:params) do
{
file_path: file_path,
ref: 'master'
}
end
let(:author_email) { FFaker::Internet.email } let(:author_email) { FFaker::Internet.email }
# I have to remove periods from the end of the name # I have to remove periods from the end of the name
...@@ -24,36 +31,72 @@ describe API::Files, api: true do ...@@ -24,36 +31,72 @@ describe API::Files, api: true do
before { project.team << [user, :developer] } before { project.team << [user, :developer] }
describe "GET /projects/:id/repository/files" do describe "GET /projects/:id/repository/files" do
it "returns file info" do let(:route) { "/projects/#{project.id}/repository/files" }
params = {
file_path: file_path,
ref: 'master',
}
get api("/projects/#{project.id}/repository/files", user), params shared_examples_for 'repository files' do
it "returns file info" do
get api(route, current_user), params
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path) expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb') expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end end
it "returns a 400 bad request if no params given" do context 'when no params are given' do
get api("/projects/#{project.id}/repository/files", user) it_behaves_like '400 response' do
let(:request) { get api(route, current_user) }
end
end
expect(response).to have_http_status(400) context 'when file_path does not exist' do
let(:params) do
{
file_path: 'app/models/application.rb',
ref: 'master',
}
end
it_behaves_like '404 response' do
let(:request) { get api(route, current_user), params }
let(:message) { '404 File Not Found' }
end
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get api(route, current_user), params }
end
end
end end
it "returns a 404 if such file does not exist" do context 'when unauthenticated', 'and project is public' do
params = { it_behaves_like 'repository files' do
file_path: 'app/models/application.rb', let(:project) { create(:project, :public) }
ref: 'master', let(:current_user) { nil }
} end
end
get api("/projects/#{project.id}/repository/files", user), params context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
let(:request) { get api(route), params }
let(:message) { '404 Project Not Found' }
end
end
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository files' do
let(:current_user) { user }
end
end
expect(response).to have_http_status(404) context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:request) { get api(route, guest), params }
end
end end
end end
......
...@@ -7,204 +7,415 @@ describe API::Repositories, api: true do ...@@ -7,204 +7,415 @@ describe API::Repositories, api: true do
include WorkhorseHelpers include WorkhorseHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let!(:project) { create(:project, creator_id: user.id) } let!(:project) { create(:project, creator_id: user.id) }
let!(:master) { create(:project_member, :master, user: user, project: project) } let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
describe "GET /projects/:id/repository/tree" do describe "GET /projects/:id/repository/tree" do
context "authorized user" do let(:route) { "/projects/#{project.id}/repository/tree" }
before { project.team << [user2, :reporter] }
it "returns project commits" do shared_examples_for 'repository tree' do
get api("/projects/#{project.id}/repository/tree", user) it 'returns the repository tree' do
get api(route, current_user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
first_commit = json_response.first
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['name']).to eq('bar') expect(first_commit['name']).to eq('bar')
expect(json_response.first['type']).to eq('tree') expect(first_commit['type']).to eq('tree')
expect(json_response.first['mode']).to eq('040000') expect(first_commit['mode']).to eq('040000')
end
context 'when ref does not exist' do
it_behaves_like '404 response' do
let(:request) { get api("#{route}?ref_name=foo", current_user) }
let(:message) { '404 Tree Not Found' }
end
end end
it 'returns a 404 for unknown ref' do context 'when repository is disabled' do
get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) include_context 'disabled repository'
expect(response).to have_http_status(404)
expect(json_response).to be_an Object it_behaves_like '403 response' do
json_response['message'] == '404 Tree Not Found' let(:request) { get api(route, current_user) }
end
end
context 'with recursive=1' do
it 'returns recursive project paths tree' do
get api("#{route}?recursive=1", current_user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response[4]['name']).to eq('html')
expect(json_response[4]['path']).to eq('files/html')
expect(json_response[4]['type']).to eq('tree')
expect(json_response[4]['mode']).to eq('040000')
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get api(route, current_user) }
end
end
context 'when ref does not exist' do
it_behaves_like '404 response' do
let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) }
let(:message) { '404 Tree Not Found' }
end
end
end end
end end
context "unauthorized user" do context 'when unauthenticated', 'and project is public' do
it "does not return project commits" do it_behaves_like 'repository tree' do
get api("/projects/#{project.id}/repository/tree") let(:project) { create(:project, :public) }
expect(response).to have_http_status(401) let(:current_user) { nil }
end end
end end
end
describe 'GET /projects/:id/repository/tree?recursive=1' do context 'when unauthenticated', 'and project is private' do
context 'authorized user' do it_behaves_like '404 response' do
before { project.team << [user2, :reporter] } let(:request) { get api(route) }
let(:message) { '404 Project Not Found' }
end
end
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository tree' do
let(:current_user) { user }
end
end
it 'should return recursive project paths tree' do context 'when authenticated', 'as a guest' do
get api("/projects/#{project.id}/repository/tree?recursive=1", user) it_behaves_like '403 response' do
let(:request) { get api(route, guest) }
end
end
end
expect(response.status).to eq(200) {
'blobs/:sha' => 'blobs/master',
'commits/:sha/blob' => 'commits/master/blob'
}.each do |desc_path, example_path|
describe "GET /projects/:id/repository/#{desc_path}" do
let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
shared_examples_for 'repository blob' do
it 'returns the repository blob' do
get api(route, current_user)
expect(response).to have_http_status(200)
end
context 'when sha does not exist' do
it_behaves_like '404 response' do
let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) }
let(:message) { '404 Commit Not Found' }
end
end
context 'when filepath does not exist' do
it_behaves_like '404 response' do
let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) }
let(:message) { '404 File Not Found' }
end
end
context 'when no filepath is given' do
it_behaves_like '400 response' do
let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) }
end
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get api(route, current_user) }
end
end
end
expect(json_response).to be_an Array context 'when unauthenticated', 'and project is public' do
expect(json_response[4]['name']).to eq('html') it_behaves_like 'repository blob' do
expect(json_response[4]['path']).to eq('files/html') let(:project) { create(:project, :public) }
expect(json_response[4]['type']).to eq('tree') let(:current_user) { nil }
expect(json_response[4]['mode']).to eq('040000') end
end end
it 'returns a 404 for unknown ref' do context 'when unauthenticated', 'and project is private' do
get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user) it_behaves_like '404 response' do
expect(response).to have_http_status(404) let(:request) { get api(route) }
let(:message) { '404 Project Not Found' }
end
end
expect(json_response).to be_an Object context 'when authenticated', 'as a developer' do
json_response['message'] == '404 Tree Not Found' it_behaves_like 'repository blob' do
let(:current_user) { user }
end
end end
end
context "unauthorized user" do context 'when authenticated', 'as a guest' do
it "does not return project commits" do it_behaves_like '403 response' do
get api("/projects/#{project.id}/repository/tree?recursive=1") let(:request) { get api(route, guest) }
expect(response).to have_http_status(401) end
end end
end end
end end
describe "GET /projects/:id/repository/blobs/:sha" do describe "GET /projects/:id/repository/raw_blobs/:sha" do
it "gets the raw file contents" do let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
expect(response).to have_http_status(200) shared_examples_for 'repository raw blob' do
it 'returns the repository raw blob' do
get api(route, current_user)
expect(response).to have_http_status(200)
end
context 'when sha does not exist' do
it_behaves_like '404 response' do
let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) }
let(:message) { '404 Blob Not Found' }
end
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get api(route, current_user) }
end
end
end end
it "returns 404 for invalid branch_name" do context 'when unauthenticated', 'and project is public' do
get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) it_behaves_like 'repository raw blob' do
expect(response).to have_http_status(404) let(:project) { create(:project, :public) }
let(:current_user) { nil }
end
end end
it "returns 404 for invalid file" do context 'when unauthenticated', 'and project is private' do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) it_behaves_like '404 response' do
expect(response).to have_http_status(404) let(:request) { get api(route) }
let(:message) { '404 Project Not Found' }
end
end end
it "returns a 400 error if filepath is missing" do context 'when authenticated', 'as a developer' do
get api("/projects/#{project.id}/repository/blobs/master", user) it_behaves_like 'repository raw blob' do
expect(response).to have_http_status(400) let(:current_user) { user }
end
end end
end
describe "GET /projects/:id/repository/commits/:sha/blob" do context 'when authenticated', 'as a guest' do
it "gets the raw file contents" do it_behaves_like '403 response' do
get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) let(:request) { get api(route, guest) }
expect(response).to have_http_status(200) end
end end
end end
describe "GET /projects/:id/repository/raw_blobs/:sha" do describe "GET /projects/:id/repository/archive(.:format)?:sha" do
it "gets the raw file contents" do let(:route) { "/projects/#{project.id}/repository/archive" }
get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
expect(response).to have_http_status(200) shared_examples_for 'repository archive' do
end it 'returns the repository archive' do
get api(route, current_user)
expect(response).to have_http_status(200)
it 'returns a 404 for unknown blob' do repo_name = project.repository.name.gsub("\.git", "")
get api("/projects/#{project.id}/repository/raw_blobs/123456", user) type, params = workhorse_send_data
expect(response).to have_http_status(404)
expect(json_response).to be_an Object expect(type).to eq('git-archive')
json_response['message'] == '404 Blob Not Found' expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
end
it 'returns the repository archive archive.zip' do
get api("/projects/#{project.id}/repository/archive.zip", user)
expect(response).to have_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
end
it 'returns the repository archive archive.tar.bz2' do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
expect(response).to have_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
end
context 'when sha does not exist' do
it_behaves_like '404 response' do
let(:request) { get api("#{route}?sha=xxx", current_user) }
let(:message) { '404 File Not Found' }
end
end
end end
end
describe "GET /projects/:id/repository/archive(.:format)?:sha" do context 'when unauthenticated', 'and project is public' do
it "gets the archive" do it_behaves_like 'repository archive' do
get api("/projects/#{project.id}/repository/archive", user) let(:project) { create(:project, :public) }
repo_name = project.repository.name.gsub("\.git", "") let(:current_user) { nil }
expect(response).to have_http_status(200) end
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
end end
it "gets the archive.zip" do context 'when unauthenticated', 'and project is private' do
get api("/projects/#{project.id}/repository/archive.zip", user) it_behaves_like '404 response' do
repo_name = project.repository.name.gsub("\.git", "") let(:request) { get api(route) }
expect(response).to have_http_status(200) let(:message) { '404 Project Not Found' }
type, params = workhorse_send_data end
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
end end
it "gets the archive.tar.bz2" do context 'when authenticated', 'as a developer' do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user) it_behaves_like 'repository archive' do
repo_name = project.repository.name.gsub("\.git", "") let(:current_user) { user }
expect(response).to have_http_status(200) end
type, params = workhorse_send_data
expect(type).to eq('git-archive')
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
end end
it "returns 404 for invalid sha" do context 'when authenticated', 'as a guest' do
get api("/projects/#{project.id}/repository/archive/?sha=xxx", user) it_behaves_like '403 response' do
expect(response).to have_http_status(404) let(:request) { get api(route, guest) }
end
end end
end end
describe 'GET /projects/:id/repository/compare' do describe 'GET /projects/:id/repository/compare' do
it "compares branches" do let(:route) { "/projects/#{project.id}/repository/compare" }
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
expect(response).to have_http_status(200) shared_examples_for 'repository compare' do
expect(json_response['commits']).to be_present it "compares branches" do
expect(json_response['diffs']).to be_present get api(route, current_user), from: 'master', to: 'feature'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "compares tags" do
get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "compares commits" do
get api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_falsey
end
it "compares commits in reverse order" do
get api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "compares same refs" do
get api(route, current_user), from: 'master', to: 'master'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_truthy
end
end end
it "compares tags" do context 'when unauthenticated', 'and project is public' do
get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0' it_behaves_like 'repository compare' do
expect(response).to have_http_status(200) let(:project) { create(:project, :public) }
expect(json_response['commits']).to be_present let(:current_user) { nil }
expect(json_response['diffs']).to be_present end
end end
it "compares commits" do context 'when unauthenticated', 'and project is private' do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id it_behaves_like '404 response' do
expect(response).to have_http_status(200) let(:request) { get api(route) }
expect(json_response['commits']).to be_empty let(:message) { '404 Project Not Found' }
expect(json_response['diffs']).to be_empty end
expect(json_response['compare_same_ref']).to be_falsey
end end
it "compares commits in reverse order" do context 'when authenticated', 'as a developer' do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id it_behaves_like 'repository compare' do
expect(response).to have_http_status(200) let(:current_user) { user }
expect(json_response['commits']).to be_present end
expect(json_response['diffs']).to be_present
end end
it "compares same refs" do context 'when authenticated', 'as a guest' do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' it_behaves_like '403 response' do
expect(response).to have_http_status(200) let(:request) { get api(route, guest) }
expect(json_response['commits']).to be_empty end
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_truthy
end end
end end
describe 'GET /projects/:id/repository/contributors' do describe 'GET /projects/:id/repository/contributors' do
it 'returns valid data' do let(:route) { "/projects/#{project.id}/repository/contributors" }
get api("/projects/#{project.id}/repository/contributors", user)
expect(response).to have_http_status(200) shared_examples_for 'repository contributors' do
expect(json_response).to be_an Array it 'returns valid data' do
contributor = json_response.first get api(route, current_user)
expect(contributor['email']).to eq('tiagonbotelho@hotmail.com')
expect(contributor['name']).to eq('tiagonbotelho') expect(response).to have_http_status(200)
expect(contributor['commits']).to eq(1) expect(json_response).to be_an Array
expect(contributor['additions']).to eq(0)
expect(contributor['deletions']).to eq(0) first_contributor = json_response.first
expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com')
expect(first_contributor['name']).to eq('tiagonbotelho')
expect(first_contributor['commits']).to eq(1)
expect(first_contributor['additions']).to eq(0)
expect(first_contributor['deletions']).to eq(0)
end
end
context 'when unauthenticated', 'and project is public' do
it_behaves_like 'repository contributors' do
let(:project) { create(:project, :public) }
let(:current_user) { nil }
end
end
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
let(:request) { get api(route) }
let(:message) { '404 Project Not Found' }
end
end
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository contributors' do
let(:current_user) { user }
end
end
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:request) { get api(route, guest) }
end
end end
end end
end end
require 'spec_helper'
# Shared examples for a resource inside a Project
#
# By default it tests all the default REST actions: index, create, new, edit,
# show, update, and destroy. You can remove actions by customizing the
# `actions` variable.
#
# It also expects a `controller` variable to be available which defines both
# the path to the resource as well as the controller name.
#
# Examples
#
# # Default behavior
# it_behaves_like 'RESTful project resources' do
# let(:controller) { 'issues' }
# end
#
# # Customizing actions
# it_behaves_like 'RESTful project resources' do
# let(:actions) { [:index] }
# let(:controller) { 'issues' }
# end
shared_examples 'importer routing' do
let(:except_actions) { [] }
it 'to #create' do
expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create)
end
it 'to #new' do
expect(get("/import/#{provider}/new")).to route_to("import/#{provider}#new") unless except_actions.include?(:new)
end
it 'to #status' do
expect(get("/import/#{provider}/status")).to route_to("import/#{provider}#status") unless except_actions.include?(:status)
end
it 'to #callback' do
expect(get("/import/#{provider}/callback")).to route_to("import/#{provider}#callback") unless except_actions.include?(:callback)
end
it 'to #jobs' do
expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs)
end
end
# personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token
# status_import_github GET /import/github/status(.:format) import/github#status
# callback_import_github GET /import/github/callback(.:format) import/github#callback
# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs
# import_github POST /import/github(.:format) import/github#create
# new_import_github GET /import/github/new(.:format) import/github#new
describe Import::GithubController, 'routing' do
it_behaves_like 'importer routing' do
let(:provider) { 'github' }
end
it 'to #personal_access_token' do
expect(post('/import/github/personal_access_token')).to route_to('import/github#personal_access_token')
end
end
# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token
# status_import_gitea GET /import/gitea/status(.:format) import/gitea#status
# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs
# import_gitea POST /import/gitea(.:format) import/gitea#create
# new_import_gitea GET /import/gitea/new(.:format) import/gitea#new
describe Import::GiteaController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:callback] }
let(:provider) { 'gitea' }
end
it 'to #personal_access_token' do
expect(post('/import/gitea/personal_access_token')).to route_to('import/gitea#personal_access_token')
end
end
# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status
# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback
# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs
# import_gitlab POST /import/gitlab(.:format) import/gitlab#create
describe Import::GitlabController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:new] }
let(:provider) { 'gitlab' }
end
end
# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status
# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback
# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs
# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create
describe Import::BitbucketController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:new] }
let(:provider) { 'bitbucket' }
end
end
# status_import_google_code GET /import/google_code/status(.:format) import/google_code#status
# callback_import_google_code POST /import/google_code/callback(.:format) import/google_code#callback
# jobs_import_google_code GET /import/google_code/jobs(.:format) import/google_code#jobs
# new_user_map_import_google_code GET /import/google_code/user_map(.:format) import/google_code#new_user_map
# create_user_map_import_google_code POST /import/google_code/user_map(.:format) import/google_code#create_user_map
# import_google_code POST /import/google_code(.:format) import/google_code#create
# new_import_google_code GET /import/google_code/new(.:format) import/google_code#new
describe Import::GoogleCodeController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:callback] }
let(:provider) { 'google_code' }
end
it 'to #callback' do
expect(post("/import/google_code/callback")).to route_to("import/google_code#callback")
end
it 'to #new_user_map' do
expect(get('/import/google_code/user_map')).to route_to('import/google_code#new_user_map')
end
it 'to #create_user_map' do
expect(post('/import/google_code/user_map')).to route_to('import/google_code#create_user_map')
end
end
# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status
# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback
# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs
# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map
# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map
# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create
# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new
describe Import::FogbugzController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:callback] }
let(:provider) { 'fogbugz' }
end
it 'to #callback' do
expect(post("/import/fogbugz/callback")).to route_to("import/fogbugz#callback")
end
it 'to #new_user_map' do
expect(get('/import/fogbugz/user_map')).to route_to('import/fogbugz#new_user_map')
end
it 'to #create_user_map' do
expect(post('/import/fogbugz/user_map')).to route_to('import/fogbugz#create_user_map')
end
end
# import_gitlab_project POST /import/gitlab_project(.:format) import/gitlab_projects#create
# POST /import/gitlab_project(.:format) import/gitlab_projects#create
# new_import_gitlab_project GET /import/gitlab_project/new(.:format) import/gitlab_projects#new
describe Import::GitlabProjectsController, 'routing' do
it 'to #create' do
expect(post('/import/gitlab_project')).to route_to('import/gitlab_projects#create')
end
it 'to #new' do
expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new')
end
end
require 'spec_helper'
describe Users::RefreshAuthorizedProjectsService do
let(:project) { create(:empty_project) }
let(:user) { project.namespace.owner }
let(:service) { described_class.new(user) }
def create_authorization(project, user, access_level = Gitlab::Access::MASTER)
ProjectAuthorization.
create!(project: project, user: user, access_level: access_level)
end
describe '#execute' do
before do
user.project_authorizations.delete_all
end
it 'updates the authorized projects of the user' do
project2 = create(:empty_project)
to_remove = create_authorization(project2, user)
expect(service).to receive(:update_with_lease).
with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
service.execute
end
it 'sets the access level of a project to the highest available level' do
to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
expect(service).to receive(:update_with_lease).
with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]])
service.execute
end
it 'returns a User' do
expect(service.execute).to be_an_instance_of(User)
end
end
describe '#update_with_lease', :redis do
it 'refreshes the authorizations using a lease' do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
and_return('foo')
expect(Gitlab::ExclusiveLease).to receive(:cancel).
with(an_instance_of(String), 'foo')
expect(service).to receive(:update_authorizations).with([1], [])
service.update_with_lease([1])
end
end
describe '#update_authorizations' do
it 'does nothing when there are no rows to add and remove' do
expect(user).not_to receive(:remove_project_authorizations)
expect(ProjectAuthorization).not_to receive(:insert_authorizations)
expect(user).not_to receive(:set_authorized_projects_column)
service.update_authorizations([], [])
end
it 'removes authorizations that should be removed' do
authorization = create_authorization(project, user)
service.update_authorizations([authorization.id])
expect(user.project_authorizations).to be_empty
end
it 'inserts authorizations that should be added' do
service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
authorizations = user.project_authorizations
expect(authorizations.length).to eq(1)
expect(authorizations[0].user_id).to eq(user.id)
expect(authorizations[0].project_id).to eq(project.id)
expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER)
end
it 'populates the authorized projects column' do
# make sure we start with a nil value no matter what the default in the
# factory may be.
user.update(authorized_projects_populated: nil)
service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]])
expect(user.authorized_projects_populated).to eq(true)
end
end
describe '#fresh_access_levels_per_project' do
let(:hash) { service.fresh_access_levels_per_project }
it 'returns a Hash' do
expect(hash).to be_an_instance_of(Hash)
end
it 'sets the keys to the project IDs' do
expect(hash.keys).to eq([project.id])
end
it 'sets the values to the access levels' do
expect(hash.values).to eq([Gitlab::Access::MASTER])
end
end
describe '#current_authorizations_per_project' do
before { create_authorization(project, user) }
let(:hash) { service.current_authorizations_per_project }
it 'returns a Hash' do
expect(hash).to be_an_instance_of(Hash)
end
it 'sets the keys to the project IDs' do
expect(hash.keys).to eq([project.id])
end
it 'sets the values to the project authorization rows' do
expect(hash.values).to eq([ProjectAuthorization.first])
end
end
describe '#current_authorizations' do
context 'without authorizations' do
it 'returns an empty list' do
expect(service.current_authorizations.empty?).to eq(true)
end
end
context 'with an authorization' do
before { create_authorization(project, user) }
let(:row) { service.current_authorizations.take }
it 'returns the currently authorized projects' do
expect(service.current_authorizations.length).to eq(1)
end
it 'includes the row ID for every row' do
expect(row.id).to be_a_kind_of(Numeric)
end
it 'includes the project ID for every row' do
expect(row.project_id).to eq(project.id)
end
it 'includes the access level for every row' do
expect(row.access_level).to eq(Gitlab::Access::MASTER)
end
end
end
describe '#fresh_authorizations' do
it 'returns the new authorized projects' do
expect(service.fresh_authorizations.length).to eq(1)
end
it 'returns the highest access level' do
project.team.add_guest(user)
rows = service.fresh_authorizations.to_a
expect(rows.length).to eq(1)
expect(rows.first.access_level).to eq(Gitlab::Access::MASTER)
end
context 'every returned row' do
let(:row) { service.fresh_authorizations.take }
it 'includes the project ID' do
expect(row.project_id).to eq(project.id)
end
it 'includes the access level' do
expect(row.access_level).to eq(Gitlab::Access::MASTER)
end
end
end
end
shared_context 'disabled repository' do
before do
project.project_feature.update_attributes!(
repository_access_level: ProjectFeature::DISABLED,
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED
)
expect(project.feature_available?(:repository, current_user)).to be false
end
end
# Specs for status checking.
#
# Requires an API request:
# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
shared_examples_for '400 response' do
before do
# Fires the request
request
end
it 'returns 400' do
expect(response).to have_http_status(400)
end
end
shared_examples_for '403 response' do
before do
# Fires the request
request
end
it 'returns 403' do
expect(response).to have_http_status(403)
end
end
shared_examples_for '404 response' do
let(:message) { nil }
before do
# Fires the request
request
end
it 'returns 404' do
expect(response).to have_http_status(404)
expect(json_response).to be_an Object
if message.present?
expect(json_response['message']).to eq(message)
end
end
end
shared_context 'a GitHub-ish import controller' do
let(:user) { create(:user) }
let(:token) { "asdasd12345" }
let(:access_params) { { github_access_token: token } }
before do
sign_in(user)
allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true)
end
end
# Specifications for behavior common to all objects with an email attribute.
# Takes a list of email-format attributes and requires:
# - subject { "the object with a attribute= setter" }
# Note: You have access to `email_value` which is the email address value
# being currently tested).
def assign_session_token(provider)
session[:"#{provider}_access_token"] = 'asdasd12345'
end
shared_examples 'a GitHub-ish import controller: POST personal_access_token' do
let(:status_import_url) { public_send("status_import_#{provider}_url") }
it "updates access token" do
token = 'asdfasdf9876'
allow_any_instance_of(Gitlab::GithubImport::Client).
to receive(:user).and_return(true)
post :personal_access_token, personal_access_token: token
expect(session[:"#{provider}_access_token"]).to eq(token)
expect(controller).to redirect_to(status_import_url)
end
end
shared_examples 'a GitHub-ish import controller: GET new' do
let(:status_import_url) { public_send("status_import_#{provider}_url") }
it "redirects to status if we already have a token" do
assign_session_token(provider)
allow(controller).to receive(:logged_in_with_provider?).and_return(false)
get :new
expect(controller).to redirect_to(status_import_url)
end
it "renders the :new page if no token is present in session" do
get :new
expect(response).to render_template(:new)
end
end
shared_examples 'a GitHub-ish import controller: GET status' do
let(:new_import_url) { public_send("new_import_#{provider}_url") }
let(:user) { create(:user) }
let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') }
let(:org) { OpenStruct.new(login: 'company') }
let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') }
let(:extra_assign_expectations) { {} }
before do
assign_session_token(provider)
end
it "assigns variables" do
project = create(:empty_project, import_type: provider, creator_id: user.id)
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
get :status
expect(assigns(:already_added_projects)).to eq([project])
expect(assigns(:repos)).to eq([repo, org_repo])
extra_assign_expectations.each do |key, value|
expect(assigns(key)).to eq(value)
end
end
it "does not show already added project" do
project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim')
stub_client(repos: [repo], orgs: [])
get :status
expect(assigns(:already_added_projects)).to eq([project])
expect(assigns(:repos)).to eq([])
end
it "handles an invalid access token" do
allow_any_instance_of(Gitlab::GithubImport::Client).
to receive(:repos).and_raise(Octokit::Unauthorized)
get :status
expect(session[:"#{provider}_access_token"]).to be_nil
expect(controller).to redirect_to(new_import_url)
expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.")
end
end
shared_examples 'a GitHub-ish import controller: POST create' do
let(:user) { create(:user) }
let(:provider_username) { user.username }
let(:provider_user) { OpenStruct.new(login: provider_username) }
let(:provider_repo) do
OpenStruct.new(
name: 'vim',
full_name: "#{provider_username}/vim",
owner: OpenStruct.new(login: provider_username)
)
end
before do
stub_client(user: provider_user, repo: provider_repo)
assign_session_token(provider)
end
context "when the repository owner is the provider user" do
context "when the provider user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, format: :js
end
end
context "when the provider user and GitLab user's usernames don't match" do
let(:provider_username) { "someone_else" }
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, format: :js
end
end
end
context "when the repository owner is not the provider user" do
let(:other_username) { "someone_else" }
before do
provider_repo.owner = OpenStruct.new(login: other_username)
assign_session_token(provider)
end
context "when a namespace with the provider user's username already exists" do
let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
context "when the namespace is owned by the GitLab user" do
it "takes the existing namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, format: :js
end
end
context "when the namespace is not owned by the GitLab user" do
before do
existing_namespace.owner = create(:user)
existing_namespace.save
end
it "creates a project using user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, format: :js
end
end
end
context "when a namespace with the provider user's username doesn't exist" do
context "when current user can create namespaces" do
it "creates the namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).and_return(double(execute: true))
expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1)
end
it "takes the new namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider).
and_return(double(execute: true))
post :create, target_namespace: provider_repo.name, format: :js
end
end
context "when current user can't create namespaces" do
before do
user.update_attribute(:can_create_group, false)
end
it "doesn't create the namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).and_return(double(execute: true))
expect { post :create, format: :js }.not_to change(Namespace, :count)
end
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, format: :js
end
end
end
context 'user has chosen a namespace and name for the project' do
let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
end
it 'takes the selected name and default namespace' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, { new_name: test_name, format: :js }
end
end
end
end
module KubernetesHelpers
include Gitlab::Kubernetes
def kube_discovery_body
{ "kind" => "APIResourceList",
"resources" => [
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
],
}
end
def kube_pods_body(*pods)
{ "kind" => "PodList",
"items" => [ kube_pod ],
}
end
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
def kube_pod(app: "valid-pod-label")
{ "metadata" => {
"name" => "kube-pod",
"creationTimestamp" => "2016-11-25T19:55:19Z",
"labels" => { "app" => app },
},
"spec" => {
"containers" => [
{ "name" => "container-0" },
{ "name" => "container-1" },
],
},
"status" => { "phase" => "Running" },
}
end
def kube_terminals(service, pod)
pod_name = pod['metadata']['name']
containers = pod['spec']['containers']
containers.map do |container|
terminal = {
selectors: { pod: pod_name, container: container['name'] },
url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
created_at: DateTime.parse(pod['metadata']['creationTimestamp'])
}
terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
terminal
end
end
end
module ReactiveCachingHelpers
def reactive_cache_key(subject, *qualifiers)
([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':')
end
def stub_reactive_cache(subject = nil, data = nil)
allow(ReactiveCachingWorker).to receive(:perform_async)
allow(ReactiveCachingWorker).to receive(:perform_in)
write_reactive_cache(subject, data) if data
end
def read_reactive_cache(subject)
Rails.cache.read(reactive_cache_key(subject))
end
def write_reactive_cache(subject, data)
start_reactive_cache_lifetime(subject)
Rails.cache.write(reactive_cache_key(subject), data)
end
def reactive_cache_alive?(subject)
Rails.cache.read(reactive_cache_key(subject, 'alive'))
end
def invalidate_reactive_cache(subject)
Rails.cache.delete(reactive_cache_key(subject, 'alive'))
end
def start_reactive_cache_lifetime(subject)
Rails.cache.write(reactive_cache_key(subject, 'alive'), true)
end
def expect_reactive_cache_update_queued(subject)
expect(ReactiveCachingWorker).
to receive(:perform_in).
with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id)
end
end
...@@ -7,27 +7,17 @@ describe AuthorizedProjectsWorker do ...@@ -7,27 +7,17 @@ describe AuthorizedProjectsWorker do
it "refreshes user's authorized projects" do it "refreshes user's authorized projects" do
user = create(:user) user = create(:user)
expect(worker).to receive(:refresh).with(an_instance_of(User)) expect_any_instance_of(User).to receive(:refresh_authorized_projects)
worker.perform(user.id) worker.perform(user.id)
end end
context "when the user is not found" do context "when the user is not found" do
it "does nothing" do it "does nothing" do
expect(worker).not_to receive(:refresh) expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
described_class.new.perform(-1) described_class.new.perform(-1)
end end
end end
end end
describe '#refresh', redis: true do
it 'refreshes the authorized projects of the user' do
user = create(:user)
expect(user).to receive(:refresh_authorized_projects)
worker.refresh(user)
end
end
end end
require 'spec_helper'
describe ReactiveCachingWorker do
let(:project) { create(:kubernetes_project) }
let(:service) { project.deployment_service }
subject { described_class.new.perform("KubernetesService", service.id) }
describe '#perform' do
it 'calls #exclusively_update_reactive_cache!' do
expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
subject
end
end
end
/*
* Fit terminal columns and rows to the dimensions of its
* DOM element.
*
* Approach:
* - Rows: Truncate the division of the terminal parent element height
* by the terminal row height
*
* - Columns: Truncate the division of the terminal parent element width by
* the terminal character width (apply display: inline at the
* terminal row and truncate its width with the current number
* of columns)
*/
(function (fit) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = fit(require('../../xterm'));
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], fit);
} else {
/*
* Plain browser environment
*/
fit(window.Terminal);
}
})(function (Xterm) {
/**
* This module provides methods for fitting a terminal's size to a parent container.
*
* @module xterm/addons/fit/fit
*/
var exports = {};
exports.proposeGeometry = function (term) {
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
parentElementWidth = parseInt(parentElementStyle.getPropertyValue('width')),
elementStyle = window.getComputedStyle(term.element),
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
availableHeight = parentElementHeight - elementPaddingVer,
availableWidth = parentElementWidth - elementPaddingHor,
container = term.rowContainer,
subjectRow = term.rowContainer.firstElementChild,
contentBuffer = subjectRow.innerHTML,
characterHeight,
rows,
characterWidth,
cols,
geometry;
subjectRow.style.display = 'inline';
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
characterHeight = parseInt(subjectRow.offsetHeight);
subjectRow.innerHTML = contentBuffer;
rows = parseInt(availableHeight / characterHeight);
cols = parseInt(availableWidth / characterWidth) - 1;
geometry = {cols: cols, rows: rows};
return geometry;
};
exports.fit = function (term) {
var geometry = exports.proposeGeometry(term);
term.resize(geometry.cols, geometry.rows);
};
Xterm.prototype.proposeGeometry = function () {
return exports.proposeGeometry(this);
};
Xterm.prototype.fit = function () {
return exports.fit(this);
};
return exports;
});
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
* xterm.js: xterm, in the browser
* Copyright (c) 2014-2016, SourceLair Private Company (www.sourcelair.com (MIT License)
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/*
* Default style for xterm.js
*/
.terminal {
background-color: #000;
color: #fff;
font-family: courier-new, courier, monospace;
font-feature-settings: "liga" 0;
position: relative;
}
.terminal.focus,
.terminal:focus {
outline: none;
}
.terminal .xterm-helpers {
position: absolute;
top: 0;
}
.terminal .xterm-helper-textarea {
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
position: absolute;
opacity: 0;
left: -9999em;
top: -9999em;
width: 0;
height: 0;
z-index: -10;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.terminal .terminal-cursor {
background-color: #fff;
color: #000;
}
.terminal:not(.focus) .terminal-cursor {
outline: 1px solid #fff;
outline-offset: -1px;
background-color: transparent;
}
.terminal.focus .terminal-cursor.blinking {
animation: blink-cursor 1.2s infinite step-end;
}
@keyframes blink-cursor {
0% {
background-color: #fff;
color: #000;
}
50% {
background-color: transparent;
color: #FFF;
}
}
.terminal .composition-view {
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.terminal .composition-view.active {
display: block;
}
.terminal .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
}
.terminal .xterm-rows {
position: absolute;
left: 0;
top: 0;
}
.terminal .xterm-rows > div {
/* Lines containing spans and text nodes ocassionally wrap despite being the same width (#327) */
white-space: nowrap;
}
.terminal .xterm-scroll-area {
visibility: hidden;
}
.terminal .xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
left: -9999em;
}
/*
* Determine default colors for xterm.js
*/
.terminal .xterm-bold {
font-weight: bold;
}
.terminal .xterm-underline {
text-decoration: underline;
}
.terminal .xterm-blink {
text-decoration: blink;
}
.terminal .xterm-hidden {
visibility: hidden;
}
.terminal .xterm-color-0 {
color: #2e3436;
}
.terminal .xterm-bg-color-0 {
background-color: #2e3436;
}
.terminal .xterm-color-1 {
color: #cc0000;
}
.terminal .xterm-bg-color-1 {
background-color: #cc0000;
}
.terminal .xterm-color-2 {
color: #4e9a06;
}
.terminal .xterm-bg-color-2 {
background-color: #4e9a06;
}
.terminal .xterm-color-3 {
color: #c4a000;
}
.terminal .xterm-bg-color-3 {
background-color: #c4a000;
}
.terminal .xterm-color-4 {
color: #3465a4;
}
.terminal .xterm-bg-color-4 {
background-color: #3465a4;
}
.terminal .xterm-color-5 {
color: #75507b;
}
.terminal .xterm-bg-color-5 {
background-color: #75507b;
}
.terminal .xterm-color-6 {
color: #06989a;
}
.terminal .xterm-bg-color-6 {
background-color: #06989a;
}
.terminal .xterm-color-7 {
color: #d3d7cf;
}
.terminal .xterm-bg-color-7 {
background-color: #d3d7cf;
}
.terminal .xterm-color-8 {
color: #555753;
}
.terminal .xterm-bg-color-8 {
background-color: #555753;
}
.terminal .xterm-color-9 {
color: #ef2929;
}
.terminal .xterm-bg-color-9 {
background-color: #ef2929;
}
.terminal .xterm-color-10 {
color: #8ae234;
}
.terminal .xterm-bg-color-10 {
background-color: #8ae234;
}
.terminal .xterm-color-11 {
color: #fce94f;
}
.terminal .xterm-bg-color-11 {
background-color: #fce94f;
}
.terminal .xterm-color-12 {
color: #729fcf;
}
.terminal .xterm-bg-color-12 {
background-color: #729fcf;
}
.terminal .xterm-color-13 {
color: #ad7fa8;
}
.terminal .xterm-bg-color-13 {
background-color: #ad7fa8;
}
.terminal .xterm-color-14 {
color: #34e2e2;
}
.terminal .xterm-bg-color-14 {
background-color: #34e2e2;
}
.terminal .xterm-color-15 {
color: #eeeeec;
}
.terminal .xterm-bg-color-15 {
background-color: #eeeeec;
}
.terminal .xterm-color-16 {
color: #000000;
}
.terminal .xterm-bg-color-16 {
background-color: #000000;
}
.terminal .xterm-color-17 {
color: #00005f;
}
.terminal .xterm-bg-color-17 {
background-color: #00005f;
}
.terminal .xterm-color-18 {
color: #000087;
}
.terminal .xterm-bg-color-18 {
background-color: #000087;
}
.terminal .xterm-color-19 {
color: #0000af;
}
.terminal .xterm-bg-color-19 {
background-color: #0000af;
}
.terminal .xterm-color-20 {
color: #0000d7;
}
.terminal .xterm-bg-color-20 {
background-color: #0000d7;
}
.terminal .xterm-color-21 {
color: #0000ff;
}
.terminal .xterm-bg-color-21 {
background-color: #0000ff;
}
.terminal .xterm-color-22 {
color: #005f00;
}
.terminal .xterm-bg-color-22 {
background-color: #005f00;
}
.terminal .xterm-color-23 {
color: #005f5f;
}
.terminal .xterm-bg-color-23 {
background-color: #005f5f;
}
.terminal .xterm-color-24 {
color: #005f87;
}
.terminal .xterm-bg-color-24 {
background-color: #005f87;
}
.terminal .xterm-color-25 {
color: #005faf;
}
.terminal .xterm-bg-color-25 {
background-color: #005faf;
}
.terminal .xterm-color-26 {
color: #005fd7;
}
.terminal .xterm-bg-color-26 {
background-color: #005fd7;
}
.terminal .xterm-color-27 {
color: #005fff;
}
.terminal .xterm-bg-color-27 {
background-color: #005fff;
}
.terminal .xterm-color-28 {
color: #008700;
}
.terminal .xterm-bg-color-28 {
background-color: #008700;
}
.terminal .xterm-color-29 {
color: #00875f;
}
.terminal .xterm-bg-color-29 {
background-color: #00875f;
}
.terminal .xterm-color-30 {
color: #008787;
}
.terminal .xterm-bg-color-30 {
background-color: #008787;
}
.terminal .xterm-color-31 {
color: #0087af;
}
.terminal .xterm-bg-color-31 {
background-color: #0087af;
}
.terminal .xterm-color-32 {
color: #0087d7;
}
.terminal .xterm-bg-color-32 {
background-color: #0087d7;
}
.terminal .xterm-color-33 {
color: #0087ff;
}
.terminal .xterm-bg-color-33 {
background-color: #0087ff;
}
.terminal .xterm-color-34 {
color: #00af00;
}
.terminal .xterm-bg-color-34 {
background-color: #00af00;
}
.terminal .xterm-color-35 {
color: #00af5f;
}
.terminal .xterm-bg-color-35 {
background-color: #00af5f;
}
.terminal .xterm-color-36 {
color: #00af87;
}
.terminal .xterm-bg-color-36 {
background-color: #00af87;
}
.terminal .xterm-color-37 {
color: #00afaf;
}
.terminal .xterm-bg-color-37 {
background-color: #00afaf;
}
.terminal .xterm-color-38 {
color: #00afd7;
}
.terminal .xterm-bg-color-38 {
background-color: #00afd7;
}
.terminal .xterm-color-39 {
color: #00afff;
}
.terminal .xterm-bg-color-39 {
background-color: #00afff;
}
.terminal .xterm-color-40 {
color: #00d700;
}
.terminal .xterm-bg-color-40 {
background-color: #00d700;
}
.terminal .xterm-color-41 {
color: #00d75f;
}
.terminal .xterm-bg-color-41 {
background-color: #00d75f;
}
.terminal .xterm-color-42 {
color: #00d787;
}
.terminal .xterm-bg-color-42 {
background-color: #00d787;
}
.terminal .xterm-color-43 {
color: #00d7af;
}
.terminal .xterm-bg-color-43 {
background-color: #00d7af;
}
.terminal .xterm-color-44 {
color: #00d7d7;
}
.terminal .xterm-bg-color-44 {
background-color: #00d7d7;
}
.terminal .xterm-color-45 {
color: #00d7ff;
}
.terminal .xterm-bg-color-45 {
background-color: #00d7ff;
}
.terminal .xterm-color-46 {
color: #00ff00;
}
.terminal .xterm-bg-color-46 {
background-color: #00ff00;
}
.terminal .xterm-color-47 {
color: #00ff5f;
}
.terminal .xterm-bg-color-47 {
background-color: #00ff5f;
}
.terminal .xterm-color-48 {
color: #00ff87;
}
.terminal .xterm-bg-color-48 {
background-color: #00ff87;
}
.terminal .xterm-color-49 {
color: #00ffaf;
}
.terminal .xterm-bg-color-49 {
background-color: #00ffaf;
}
.terminal .xterm-color-50 {
color: #00ffd7;
}
.terminal .xterm-bg-color-50 {
background-color: #00ffd7;
}
.terminal .xterm-color-51 {
color: #00ffff;
}
.terminal .xterm-bg-color-51 {
background-color: #00ffff;
}
.terminal .xterm-color-52 {
color: #5f0000;
}
.terminal .xterm-bg-color-52 {
background-color: #5f0000;
}
.terminal .xterm-color-53 {
color: #5f005f;
}
.terminal .xterm-bg-color-53 {
background-color: #5f005f;
}
.terminal .xterm-color-54 {
color: #5f0087;
}
.terminal .xterm-bg-color-54 {
background-color: #5f0087;
}
.terminal .xterm-color-55 {
color: #5f00af;
}
.terminal .xterm-bg-color-55 {
background-color: #5f00af;
}
.terminal .xterm-color-56 {
color: #5f00d7;
}
.terminal .xterm-bg-color-56 {
background-color: #5f00d7;
}
.terminal .xterm-color-57 {
color: #5f00ff;
}
.terminal .xterm-bg-color-57 {
background-color: #5f00ff;
}
.terminal .xterm-color-58 {
color: #5f5f00;
}
.terminal .xterm-bg-color-58 {
background-color: #5f5f00;
}
.terminal .xterm-color-59 {
color: #5f5f5f;
}
.terminal .xterm-bg-color-59 {
background-color: #5f5f5f;
}
.terminal .xterm-color-60 {
color: #5f5f87;
}
.terminal .xterm-bg-color-60 {
background-color: #5f5f87;
}
.terminal .xterm-color-61 {
color: #5f5faf;
}
.terminal .xterm-bg-color-61 {
background-color: #5f5faf;
}
.terminal .xterm-color-62 {
color: #5f5fd7;
}
.terminal .xterm-bg-color-62 {
background-color: #5f5fd7;
}
.terminal .xterm-color-63 {
color: #5f5fff;
}
.terminal .xterm-bg-color-63 {
background-color: #5f5fff;
}
.terminal .xterm-color-64 {
color: #5f8700;
}
.terminal .xterm-bg-color-64 {
background-color: #5f8700;
}
.terminal .xterm-color-65 {
color: #5f875f;
}
.terminal .xterm-bg-color-65 {
background-color: #5f875f;
}
.terminal .xterm-color-66 {
color: #5f8787;
}
.terminal .xterm-bg-color-66 {
background-color: #5f8787;
}
.terminal .xterm-color-67 {
color: #5f87af;
}
.terminal .xterm-bg-color-67 {
background-color: #5f87af;
}
.terminal .xterm-color-68 {
color: #5f87d7;
}
.terminal .xterm-bg-color-68 {
background-color: #5f87d7;
}
.terminal .xterm-color-69 {
color: #5f87ff;
}
.terminal .xterm-bg-color-69 {
background-color: #5f87ff;
}
.terminal .xterm-color-70 {
color: #5faf00;
}
.terminal .xterm-bg-color-70 {
background-color: #5faf00;
}
.terminal .xterm-color-71 {
color: #5faf5f;
}
.terminal .xterm-bg-color-71 {
background-color: #5faf5f;
}
.terminal .xterm-color-72 {
color: #5faf87;
}
.terminal .xterm-bg-color-72 {
background-color: #5faf87;
}
.terminal .xterm-color-73 {
color: #5fafaf;
}
.terminal .xterm-bg-color-73 {
background-color: #5fafaf;
}
.terminal .xterm-color-74 {
color: #5fafd7;
}
.terminal .xterm-bg-color-74 {
background-color: #5fafd7;
}
.terminal .xterm-color-75 {
color: #5fafff;
}
.terminal .xterm-bg-color-75 {
background-color: #5fafff;
}
.terminal .xterm-color-76 {
color: #5fd700;
}
.terminal .xterm-bg-color-76 {
background-color: #5fd700;
}
.terminal .xterm-color-77 {
color: #5fd75f;
}
.terminal .xterm-bg-color-77 {
background-color: #5fd75f;
}
.terminal .xterm-color-78 {
color: #5fd787;
}
.terminal .xterm-bg-color-78 {
background-color: #5fd787;
}
.terminal .xterm-color-79 {
color: #5fd7af;
}
.terminal .xterm-bg-color-79 {
background-color: #5fd7af;
}
.terminal .xterm-color-80 {
color: #5fd7d7;
}
.terminal .xterm-bg-color-80 {
background-color: #5fd7d7;
}
.terminal .xterm-color-81 {
color: #5fd7ff;
}
.terminal .xterm-bg-color-81 {
background-color: #5fd7ff;
}
.terminal .xterm-color-82 {
color: #5fff00;
}
.terminal .xterm-bg-color-82 {
background-color: #5fff00;
}
.terminal .xterm-color-83 {
color: #5fff5f;
}
.terminal .xterm-bg-color-83 {
background-color: #5fff5f;
}
.terminal .xterm-color-84 {
color: #5fff87;
}
.terminal .xterm-bg-color-84 {
background-color: #5fff87;
}
.terminal .xterm-color-85 {
color: #5fffaf;
}
.terminal .xterm-bg-color-85 {
background-color: #5fffaf;
}
.terminal .xterm-color-86 {
color: #5fffd7;
}
.terminal .xterm-bg-color-86 {
background-color: #5fffd7;
}
.terminal .xterm-color-87 {
color: #5fffff;
}
.terminal .xterm-bg-color-87 {
background-color: #5fffff;
}
.terminal .xterm-color-88 {
color: #870000;
}
.terminal .xterm-bg-color-88 {
background-color: #870000;
}
.terminal .xterm-color-89 {
color: #87005f;
}
.terminal .xterm-bg-color-89 {
background-color: #87005f;
}
.terminal .xterm-color-90 {
color: #870087;
}
.terminal .xterm-bg-color-90 {
background-color: #870087;
}
.terminal .xterm-color-91 {
color: #8700af;
}
.terminal .xterm-bg-color-91 {
background-color: #8700af;
}
.terminal .xterm-color-92 {
color: #8700d7;
}
.terminal .xterm-bg-color-92 {
background-color: #8700d7;
}
.terminal .xterm-color-93 {
color: #8700ff;
}
.terminal .xterm-bg-color-93 {
background-color: #8700ff;
}
.terminal .xterm-color-94 {
color: #875f00;
}
.terminal .xterm-bg-color-94 {
background-color: #875f00;
}
.terminal .xterm-color-95 {
color: #875f5f;
}
.terminal .xterm-bg-color-95 {
background-color: #875f5f;
}
.terminal .xterm-color-96 {
color: #875f87;
}
.terminal .xterm-bg-color-96 {
background-color: #875f87;
}
.terminal .xterm-color-97 {
color: #875faf;
}
.terminal .xterm-bg-color-97 {
background-color: #875faf;
}
.terminal .xterm-color-98 {
color: #875fd7;
}
.terminal .xterm-bg-color-98 {
background-color: #875fd7;
}
.terminal .xterm-color-99 {
color: #875fff;
}
.terminal .xterm-bg-color-99 {
background-color: #875fff;
}
.terminal .xterm-color-100 {
color: #878700;
}
.terminal .xterm-bg-color-100 {
background-color: #878700;
}
.terminal .xterm-color-101 {
color: #87875f;
}
.terminal .xterm-bg-color-101 {
background-color: #87875f;
}
.terminal .xterm-color-102 {
color: #878787;
}
.terminal .xterm-bg-color-102 {
background-color: #878787;
}
.terminal .xterm-color-103 {
color: #8787af;
}
.terminal .xterm-bg-color-103 {
background-color: #8787af;
}
.terminal .xterm-color-104 {
color: #8787d7;
}
.terminal .xterm-bg-color-104 {
background-color: #8787d7;
}
.terminal .xterm-color-105 {
color: #8787ff;
}
.terminal .xterm-bg-color-105 {
background-color: #8787ff;
}
.terminal .xterm-color-106 {
color: #87af00;
}
.terminal .xterm-bg-color-106 {
background-color: #87af00;
}
.terminal .xterm-color-107 {
color: #87af5f;
}
.terminal .xterm-bg-color-107 {
background-color: #87af5f;
}
.terminal .xterm-color-108 {
color: #87af87;
}
.terminal .xterm-bg-color-108 {
background-color: #87af87;
}
.terminal .xterm-color-109 {
color: #87afaf;
}
.terminal .xterm-bg-color-109 {
background-color: #87afaf;
}
.terminal .xterm-color-110 {
color: #87afd7;
}
.terminal .xterm-bg-color-110 {
background-color: #87afd7;
}
.terminal .xterm-color-111 {
color: #87afff;
}
.terminal .xterm-bg-color-111 {
background-color: #87afff;
}
.terminal .xterm-color-112 {
color: #87d700;
}
.terminal .xterm-bg-color-112 {
background-color: #87d700;
}
.terminal .xterm-color-113 {
color: #87d75f;
}
.terminal .xterm-bg-color-113 {
background-color: #87d75f;
}
.terminal .xterm-color-114 {
color: #87d787;
}
.terminal .xterm-bg-color-114 {
background-color: #87d787;
}
.terminal .xterm-color-115 {
color: #87d7af;
}
.terminal .xterm-bg-color-115 {
background-color: #87d7af;
}
.terminal .xterm-color-116 {
color: #87d7d7;
}
.terminal .xterm-bg-color-116 {
background-color: #87d7d7;
}
.terminal .xterm-color-117 {
color: #87d7ff;
}
.terminal .xterm-bg-color-117 {
background-color: #87d7ff;
}
.terminal .xterm-color-118 {
color: #87ff00;
}
.terminal .xterm-bg-color-118 {
background-color: #87ff00;
}
.terminal .xterm-color-119 {
color: #87ff5f;
}
.terminal .xterm-bg-color-119 {
background-color: #87ff5f;
}
.terminal .xterm-color-120 {
color: #87ff87;
}
.terminal .xterm-bg-color-120 {
background-color: #87ff87;
}
.terminal .xterm-color-121 {
color: #87ffaf;
}
.terminal .xterm-bg-color-121 {
background-color: #87ffaf;
}
.terminal .xterm-color-122 {
color: #87ffd7;
}
.terminal .xterm-bg-color-122 {
background-color: #87ffd7;
}
.terminal .xterm-color-123 {
color: #87ffff;
}
.terminal .xterm-bg-color-123 {
background-color: #87ffff;
}
.terminal .xterm-color-124 {
color: #af0000;
}
.terminal .xterm-bg-color-124 {
background-color: #af0000;
}
.terminal .xterm-color-125 {
color: #af005f;
}
.terminal .xterm-bg-color-125 {
background-color: #af005f;
}
.terminal .xterm-color-126 {
color: #af0087;
}
.terminal .xterm-bg-color-126 {
background-color: #af0087;
}
.terminal .xterm-color-127 {
color: #af00af;
}
.terminal .xterm-bg-color-127 {
background-color: #af00af;
}
.terminal .xterm-color-128 {
color: #af00d7;
}
.terminal .xterm-bg-color-128 {
background-color: #af00d7;
}
.terminal .xterm-color-129 {
color: #af00ff;
}
.terminal .xterm-bg-color-129 {
background-color: #af00ff;
}
.terminal .xterm-color-130 {
color: #af5f00;
}
.terminal .xterm-bg-color-130 {
background-color: #af5f00;
}
.terminal .xterm-color-131 {
color: #af5f5f;
}
.terminal .xterm-bg-color-131 {
background-color: #af5f5f;
}
.terminal .xterm-color-132 {
color: #af5f87;
}
.terminal .xterm-bg-color-132 {
background-color: #af5f87;
}
.terminal .xterm-color-133 {
color: #af5faf;
}
.terminal .xterm-bg-color-133 {
background-color: #af5faf;
}
.terminal .xterm-color-134 {
color: #af5fd7;
}
.terminal .xterm-bg-color-134 {
background-color: #af5fd7;
}
.terminal .xterm-color-135 {
color: #af5fff;
}
.terminal .xterm-bg-color-135 {
background-color: #af5fff;
}
.terminal .xterm-color-136 {
color: #af8700;
}
.terminal .xterm-bg-color-136 {
background-color: #af8700;
}
.terminal .xterm-color-137 {
color: #af875f;
}
.terminal .xterm-bg-color-137 {
background-color: #af875f;
}
.terminal .xterm-color-138 {
color: #af8787;
}
.terminal .xterm-bg-color-138 {
background-color: #af8787;
}
.terminal .xterm-color-139 {
color: #af87af;
}
.terminal .xterm-bg-color-139 {
background-color: #af87af;
}
.terminal .xterm-color-140 {
color: #af87d7;
}
.terminal .xterm-bg-color-140 {
background-color: #af87d7;
}
.terminal .xterm-color-141 {
color: #af87ff;
}
.terminal .xterm-bg-color-141 {
background-color: #af87ff;
}
.terminal .xterm-color-142 {
color: #afaf00;
}
.terminal .xterm-bg-color-142 {
background-color: #afaf00;
}
.terminal .xterm-color-143 {
color: #afaf5f;
}
.terminal .xterm-bg-color-143 {
background-color: #afaf5f;
}
.terminal .xterm-color-144 {
color: #afaf87;
}
.terminal .xterm-bg-color-144 {
background-color: #afaf87;
}
.terminal .xterm-color-145 {
color: #afafaf;
}
.terminal .xterm-bg-color-145 {
background-color: #afafaf;
}
.terminal .xterm-color-146 {
color: #afafd7;
}
.terminal .xterm-bg-color-146 {
background-color: #afafd7;
}
.terminal .xterm-color-147 {
color: #afafff;
}
.terminal .xterm-bg-color-147 {
background-color: #afafff;
}
.terminal .xterm-color-148 {
color: #afd700;
}
.terminal .xterm-bg-color-148 {
background-color: #afd700;
}
.terminal .xterm-color-149 {
color: #afd75f;
}
.terminal .xterm-bg-color-149 {
background-color: #afd75f;
}
.terminal .xterm-color-150 {
color: #afd787;
}
.terminal .xterm-bg-color-150 {
background-color: #afd787;
}
.terminal .xterm-color-151 {
color: #afd7af;
}
.terminal .xterm-bg-color-151 {
background-color: #afd7af;
}
.terminal .xterm-color-152 {
color: #afd7d7;
}
.terminal .xterm-bg-color-152 {
background-color: #afd7d7;
}
.terminal .xterm-color-153 {
color: #afd7ff;
}
.terminal .xterm-bg-color-153 {
background-color: #afd7ff;
}
.terminal .xterm-color-154 {
color: #afff00;
}
.terminal .xterm-bg-color-154 {
background-color: #afff00;
}
.terminal .xterm-color-155 {
color: #afff5f;
}
.terminal .xterm-bg-color-155 {
background-color: #afff5f;
}
.terminal .xterm-color-156 {
color: #afff87;
}
.terminal .xterm-bg-color-156 {
background-color: #afff87;
}
.terminal .xterm-color-157 {
color: #afffaf;
}
.terminal .xterm-bg-color-157 {
background-color: #afffaf;
}
.terminal .xterm-color-158 {
color: #afffd7;
}
.terminal .xterm-bg-color-158 {
background-color: #afffd7;
}
.terminal .xterm-color-159 {
color: #afffff;
}
.terminal .xterm-bg-color-159 {
background-color: #afffff;
}
.terminal .xterm-color-160 {
color: #d70000;
}
.terminal .xterm-bg-color-160 {
background-color: #d70000;
}
.terminal .xterm-color-161 {
color: #d7005f;
}
.terminal .xterm-bg-color-161 {
background-color: #d7005f;
}
.terminal .xterm-color-162 {
color: #d70087;
}
.terminal .xterm-bg-color-162 {
background-color: #d70087;
}
.terminal .xterm-color-163 {
color: #d700af;
}
.terminal .xterm-bg-color-163 {
background-color: #d700af;
}
.terminal .xterm-color-164 {
color: #d700d7;
}
.terminal .xterm-bg-color-164 {
background-color: #d700d7;
}
.terminal .xterm-color-165 {
color: #d700ff;
}
.terminal .xterm-bg-color-165 {
background-color: #d700ff;
}
.terminal .xterm-color-166 {
color: #d75f00;
}
.terminal .xterm-bg-color-166 {
background-color: #d75f00;
}
.terminal .xterm-color-167 {
color: #d75f5f;
}
.terminal .xterm-bg-color-167 {
background-color: #d75f5f;
}
.terminal .xterm-color-168 {
color: #d75f87;
}
.terminal .xterm-bg-color-168 {
background-color: #d75f87;
}
.terminal .xterm-color-169 {
color: #d75faf;
}
.terminal .xterm-bg-color-169 {
background-color: #d75faf;
}
.terminal .xterm-color-170 {
color: #d75fd7;
}
.terminal .xterm-bg-color-170 {
background-color: #d75fd7;
}
.terminal .xterm-color-171 {
color: #d75fff;
}
.terminal .xterm-bg-color-171 {
background-color: #d75fff;
}
.terminal .xterm-color-172 {
color: #d78700;
}
.terminal .xterm-bg-color-172 {
background-color: #d78700;
}
.terminal .xterm-color-173 {
color: #d7875f;
}
.terminal .xterm-bg-color-173 {
background-color: #d7875f;
}
.terminal .xterm-color-174 {
color: #d78787;
}
.terminal .xterm-bg-color-174 {
background-color: #d78787;
}
.terminal .xterm-color-175 {
color: #d787af;
}
.terminal .xterm-bg-color-175 {
background-color: #d787af;
}
.terminal .xterm-color-176 {
color: #d787d7;
}
.terminal .xterm-bg-color-176 {
background-color: #d787d7;
}
.terminal .xterm-color-177 {
color: #d787ff;
}
.terminal .xterm-bg-color-177 {
background-color: #d787ff;
}
.terminal .xterm-color-178 {
color: #d7af00;
}
.terminal .xterm-bg-color-178 {
background-color: #d7af00;
}
.terminal .xterm-color-179 {
color: #d7af5f;
}
.terminal .xterm-bg-color-179 {
background-color: #d7af5f;
}
.terminal .xterm-color-180 {
color: #d7af87;
}
.terminal .xterm-bg-color-180 {
background-color: #d7af87;
}
.terminal .xterm-color-181 {
color: #d7afaf;
}
.terminal .xterm-bg-color-181 {
background-color: #d7afaf;
}
.terminal .xterm-color-182 {
color: #d7afd7;
}
.terminal .xterm-bg-color-182 {
background-color: #d7afd7;
}
.terminal .xterm-color-183 {
color: #d7afff;
}
.terminal .xterm-bg-color-183 {
background-color: #d7afff;
}
.terminal .xterm-color-184 {
color: #d7d700;
}
.terminal .xterm-bg-color-184 {
background-color: #d7d700;
}
.terminal .xterm-color-185 {
color: #d7d75f;
}
.terminal .xterm-bg-color-185 {
background-color: #d7d75f;
}
.terminal .xterm-color-186 {
color: #d7d787;
}
.terminal .xterm-bg-color-186 {
background-color: #d7d787;
}
.terminal .xterm-color-187 {
color: #d7d7af;
}
.terminal .xterm-bg-color-187 {
background-color: #d7d7af;
}
.terminal .xterm-color-188 {
color: #d7d7d7;
}
.terminal .xterm-bg-color-188 {
background-color: #d7d7d7;
}
.terminal .xterm-color-189 {
color: #d7d7ff;
}
.terminal .xterm-bg-color-189 {
background-color: #d7d7ff;
}
.terminal .xterm-color-190 {
color: #d7ff00;
}
.terminal .xterm-bg-color-190 {
background-color: #d7ff00;
}
.terminal .xterm-color-191 {
color: #d7ff5f;
}
.terminal .xterm-bg-color-191 {
background-color: #d7ff5f;
}
.terminal .xterm-color-192 {
color: #d7ff87;
}
.terminal .xterm-bg-color-192 {
background-color: #d7ff87;
}
.terminal .xterm-color-193 {
color: #d7ffaf;
}
.terminal .xterm-bg-color-193 {
background-color: #d7ffaf;
}
.terminal .xterm-color-194 {
color: #d7ffd7;
}
.terminal .xterm-bg-color-194 {
background-color: #d7ffd7;
}
.terminal .xterm-color-195 {
color: #d7ffff;
}
.terminal .xterm-bg-color-195 {
background-color: #d7ffff;
}
.terminal .xterm-color-196 {
color: #ff0000;
}
.terminal .xterm-bg-color-196 {
background-color: #ff0000;
}
.terminal .xterm-color-197 {
color: #ff005f;
}
.terminal .xterm-bg-color-197 {
background-color: #ff005f;
}
.terminal .xterm-color-198 {
color: #ff0087;
}
.terminal .xterm-bg-color-198 {
background-color: #ff0087;
}
.terminal .xterm-color-199 {
color: #ff00af;
}
.terminal .xterm-bg-color-199 {
background-color: #ff00af;
}
.terminal .xterm-color-200 {
color: #ff00d7;
}
.terminal .xterm-bg-color-200 {
background-color: #ff00d7;
}
.terminal .xterm-color-201 {
color: #ff00ff;
}
.terminal .xterm-bg-color-201 {
background-color: #ff00ff;
}
.terminal .xterm-color-202 {
color: #ff5f00;
}
.terminal .xterm-bg-color-202 {
background-color: #ff5f00;
}
.terminal .xterm-color-203 {
color: #ff5f5f;
}
.terminal .xterm-bg-color-203 {
background-color: #ff5f5f;
}
.terminal .xterm-color-204 {
color: #ff5f87;
}
.terminal .xterm-bg-color-204 {
background-color: #ff5f87;
}
.terminal .xterm-color-205 {
color: #ff5faf;
}
.terminal .xterm-bg-color-205 {
background-color: #ff5faf;
}
.terminal .xterm-color-206 {
color: #ff5fd7;
}
.terminal .xterm-bg-color-206 {
background-color: #ff5fd7;
}
.terminal .xterm-color-207 {
color: #ff5fff;
}
.terminal .xterm-bg-color-207 {
background-color: #ff5fff;
}
.terminal .xterm-color-208 {
color: #ff8700;
}
.terminal .xterm-bg-color-208 {
background-color: #ff8700;
}
.terminal .xterm-color-209 {
color: #ff875f;
}
.terminal .xterm-bg-color-209 {
background-color: #ff875f;
}
.terminal .xterm-color-210 {
color: #ff8787;
}
.terminal .xterm-bg-color-210 {
background-color: #ff8787;
}
.terminal .xterm-color-211 {
color: #ff87af;
}
.terminal .xterm-bg-color-211 {
background-color: #ff87af;
}
.terminal .xterm-color-212 {
color: #ff87d7;
}
.terminal .xterm-bg-color-212 {
background-color: #ff87d7;
}
.terminal .xterm-color-213 {
color: #ff87ff;
}
.terminal .xterm-bg-color-213 {
background-color: #ff87ff;
}
.terminal .xterm-color-214 {
color: #ffaf00;
}
.terminal .xterm-bg-color-214 {
background-color: #ffaf00;
}
.terminal .xterm-color-215 {
color: #ffaf5f;
}
.terminal .xterm-bg-color-215 {
background-color: #ffaf5f;
}
.terminal .xterm-color-216 {
color: #ffaf87;
}
.terminal .xterm-bg-color-216 {
background-color: #ffaf87;
}
.terminal .xterm-color-217 {
color: #ffafaf;
}
.terminal .xterm-bg-color-217 {
background-color: #ffafaf;
}
.terminal .xterm-color-218 {
color: #ffafd7;
}
.terminal .xterm-bg-color-218 {
background-color: #ffafd7;
}
.terminal .xterm-color-219 {
color: #ffafff;
}
.terminal .xterm-bg-color-219 {
background-color: #ffafff;
}
.terminal .xterm-color-220 {
color: #ffd700;
}
.terminal .xterm-bg-color-220 {
background-color: #ffd700;
}
.terminal .xterm-color-221 {
color: #ffd75f;
}
.terminal .xterm-bg-color-221 {
background-color: #ffd75f;
}
.terminal .xterm-color-222 {
color: #ffd787;
}
.terminal .xterm-bg-color-222 {
background-color: #ffd787;
}
.terminal .xterm-color-223 {
color: #ffd7af;
}
.terminal .xterm-bg-color-223 {
background-color: #ffd7af;
}
.terminal .xterm-color-224 {
color: #ffd7d7;
}
.terminal .xterm-bg-color-224 {
background-color: #ffd7d7;
}
.terminal .xterm-color-225 {
color: #ffd7ff;
}
.terminal .xterm-bg-color-225 {
background-color: #ffd7ff;
}
.terminal .xterm-color-226 {
color: #ffff00;
}
.terminal .xterm-bg-color-226 {
background-color: #ffff00;
}
.terminal .xterm-color-227 {
color: #ffff5f;
}
.terminal .xterm-bg-color-227 {
background-color: #ffff5f;
}
.terminal .xterm-color-228 {
color: #ffff87;
}
.terminal .xterm-bg-color-228 {
background-color: #ffff87;
}
.terminal .xterm-color-229 {
color: #ffffaf;
}
.terminal .xterm-bg-color-229 {
background-color: #ffffaf;
}
.terminal .xterm-color-230 {
color: #ffffd7;
}
.terminal .xterm-bg-color-230 {
background-color: #ffffd7;
}
.terminal .xterm-color-231 {
color: #ffffff;
}
.terminal .xterm-bg-color-231 {
background-color: #ffffff;
}
.terminal .xterm-color-232 {
color: #080808;
}
.terminal .xterm-bg-color-232 {
background-color: #080808;
}
.terminal .xterm-color-233 {
color: #121212;
}
.terminal .xterm-bg-color-233 {
background-color: #121212;
}
.terminal .xterm-color-234 {
color: #1c1c1c;
}
.terminal .xterm-bg-color-234 {
background-color: #1c1c1c;
}
.terminal .xterm-color-235 {
color: #262626;
}
.terminal .xterm-bg-color-235 {
background-color: #262626;
}
.terminal .xterm-color-236 {
color: #303030;
}
.terminal .xterm-bg-color-236 {
background-color: #303030;
}
.terminal .xterm-color-237 {
color: #3a3a3a;
}
.terminal .xterm-bg-color-237 {
background-color: #3a3a3a;
}
.terminal .xterm-color-238 {
color: #444444;
}
.terminal .xterm-bg-color-238 {
background-color: #444444;
}
.terminal .xterm-color-239 {
color: #4e4e4e;
}
.terminal .xterm-bg-color-239 {
background-color: #4e4e4e;
}
.terminal .xterm-color-240 {
color: #585858;
}
.terminal .xterm-bg-color-240 {
background-color: #585858;
}
.terminal .xterm-color-241 {
color: #626262;
}
.terminal .xterm-bg-color-241 {
background-color: #626262;
}
.terminal .xterm-color-242 {
color: #6c6c6c;
}
.terminal .xterm-bg-color-242 {
background-color: #6c6c6c;
}
.terminal .xterm-color-243 {
color: #767676;
}
.terminal .xterm-bg-color-243 {
background-color: #767676;
}
.terminal .xterm-color-244 {
color: #808080;
}
.terminal .xterm-bg-color-244 {
background-color: #808080;
}
.terminal .xterm-color-245 {
color: #8a8a8a;
}
.terminal .xterm-bg-color-245 {
background-color: #8a8a8a;
}
.terminal .xterm-color-246 {
color: #949494;
}
.terminal .xterm-bg-color-246 {
background-color: #949494;
}
.terminal .xterm-color-247 {
color: #9e9e9e;
}
.terminal .xterm-bg-color-247 {
background-color: #9e9e9e;
}
.terminal .xterm-color-248 {
color: #a8a8a8;
}
.terminal .xterm-bg-color-248 {
background-color: #a8a8a8;
}
.terminal .xterm-color-249 {
color: #b2b2b2;
}
.terminal .xterm-bg-color-249 {
background-color: #b2b2b2;
}
.terminal .xterm-color-250 {
color: #bcbcbc;
}
.terminal .xterm-bg-color-250 {
background-color: #bcbcbc;
}
.terminal .xterm-color-251 {
color: #c6c6c6;
}
.terminal .xterm-bg-color-251 {
background-color: #c6c6c6;
}
.terminal .xterm-color-252 {
color: #d0d0d0;
}
.terminal .xterm-bg-color-252 {
background-color: #d0d0d0;
}
.terminal .xterm-color-253 {
color: #dadada;
}
.terminal .xterm-bg-color-253 {
background-color: #dadada;
}
.terminal .xterm-color-254 {
color: #e4e4e4;
}
.terminal .xterm-bg-color-254 {
background-color: #e4e4e4;
}
.terminal .xterm-color-255 {
color: #eeeeee;
}
.terminal .xterm-bg-color-255 {
background-color: #eeeeee;
}
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