Commit 3420c6cc authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into orderable-issues

parents 9895d670 7f2819b7
......@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.17.3 (2017-03-07)
- Fix the redirect to custom home page URL. !9518
- Fix broken migration when upgrading straight to 8.17.1. !9613
- Make projects dropdown only show projects you are a member of. !9614
- Fix creating a file in an empty repo using the API. !9632
- Don't copy tooltip when copying GFM.
- Fix cherry-picking or reverting through an MR.
## 8.17.2 (2017-03-01)
- Expire all webpack assets after 8.17.1 included a badly compiled asset. !9602
......
......@@ -18,25 +18,26 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
......@@ -68,9 +69,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
gem 'grape', '~> 0.19.0'
gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination
gem 'kaminari', '~> 0.17.0'
......@@ -102,19 +103,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......@@ -229,18 +230,18 @@ gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.0'
gem 'gon', '~> 6.1.0'
gem 'gemojione', '~> 3.0'
gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.0.0'
......@@ -278,13 +279,13 @@ group :development, :test do
gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.5.0'
gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.7.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec_profiling', '~> 0.0.5'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
......@@ -292,13 +293,13 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2'
gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
gem 'poltergeist', '~> 1.9.0'
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.47.1', require: false
gem 'rubocop-rspec', '~> 1.12.0', require: false
......
......@@ -78,6 +78,7 @@ GEM
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
......@@ -167,6 +168,9 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
doorkeeper-openid_connect (1.1.2)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
email_reply_trimmer (0.1.6)
......@@ -376,6 +380,12 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
json-jwt (1.7.1)
activesupport
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
......@@ -684,6 +694,7 @@ GEM
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
......@@ -789,6 +800,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
......@@ -866,6 +878,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.1.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
......
import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
......@@ -286,7 +287,7 @@ const UserCallout = require('./user_callout');
case 'search:show':
new Search();
break;
case 'projects:protected_branches:index':
case 'projects:repository:show':
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
......@@ -297,6 +298,8 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show':
new gl.CILintEditor();
break;
case 'projects:environments:metrics':
new PrometheusGraph();
case 'users:show':
new UserCallout();
break;
......
This diff is collapsed.
......@@ -4,6 +4,21 @@
&.reset-filters {
padding: 7px;
}
&.update-issues-btn {
float: right;
margin-right: 0;
@media (max-width: $screen-xs-max) {
float: none;
}
}
}
.filters-section {
@media (max-width: $screen-xs-max) {
display: inline-block;
}
}
@media (min-width: $screen-sm-min) {
......@@ -34,6 +49,11 @@
display: block;
margin: 0 0 10px;
}
.dropdown-menu-toggle,
.update-issues-btn .btn {
width: 100%;
}
}
.filtered-search-container {
......@@ -111,7 +131,15 @@
overflow: auto;
}
@media (max-width: $screen-xs-min) {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.issues-details-filters {
.dropdown-menu-toggle {
width: 100px;
}
}
}
@media (max-width: $screen-xs-max) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
......@@ -205,4 +233,4 @@
.filter-dropdown-loading {
padding: 8px 16px;
}
\ No newline at end of file
}
......@@ -229,44 +229,6 @@ ul.content-list {
}
}
// Table list
.table-list {
display: table;
width: 100%;
.table-list-row {
display: table-row;
}
.table-list-cell {
display: table-cell;
vertical-align: top;
padding: 10px 16px;
border-bottom: 1px solid $gray-darker;
&.avatar-cell {
width: 36px;
padding-right: 0;
img {
margin-right: 0;
}
}
}
&.table-wide {
.table-list-cell {
&:last-of-type {
padding-right: 0;
}
&:first-of-type {
padding-left: 0;
}
}
}
}
.panel > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
......
......@@ -100,8 +100,7 @@
@media (max-width: $screen-sm-max) {
.issues-filters {
.milestone-filter,
.labels-filter {
.milestone-filter {
display: none;
}
}
......
......@@ -48,11 +48,3 @@
line-height: inherit;
}
}
.panel-default {
.table-list-row:last-child {
.table-list-cell {
border-bottom: 0;
}
}
}
......@@ -78,6 +78,7 @@
padding: 5px 10px;
background-color: $gray-light;
border-bottom: 1px solid $gray-darker;
border-top: 1px solid $gray-darker;
font-size: 14px;
&:first-child {
......@@ -117,10 +118,37 @@
}
}
.commit.flex-list {
display: flex;
}
.avatar-cell {
width: 46px;
padding-left: 10px;
img {
margin-right: 0;
}
}
.commit-detail {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-grow: 1;
padding-left: 10px;
.merge-request-branches & {
flex-direction: column;
}
}
.commit-content {
padding-right: 10px;
}
.commit-actions {
@media (min-width: $screen-sm-min) {
width: 300px;
text-align: right;
font-size: 0;
}
......
......@@ -143,3 +143,71 @@
}
}
}
.prometheus-graph {
text {
fill: $stat-graph-axis-fill;
}
}
.x-axis path,
.y-axis path,
.label-x-axis-line,
.label-y-axis-line {
fill: none;
stroke-width: 1;
shape-rendering: crispEdges;
}
.x-axis path,
.y-axis path {
stroke: $stat-graph-axis-fill;
}
.label-x-axis-line,
.label-y-axis-line {
stroke: $border-color;
}
.y-axis {
line {
stroke: $stat-graph-axis-fill;
stroke-width: 1;
}
}
.metric-area {
opacity: 0.8;
}
.prometheus-graph-overlay {
fill: none;
opacity: 0.0;
pointer-events: all;
}
.rect-text-metric {
fill: $white-light;
stroke-width: 1;
stroke: $black;
}
.rect-axis-text {
fill: $white-light;
}
.text-metric,
.text-median-metric,
.text-metric-usage,
.text-metric-date {
fill: $black;
}
.text-metric-date {
font-weight: 200;
}
.selected-metric-line {
stroke: $black;
stroke-width: 1;
}
......@@ -240,8 +240,7 @@
.commit {
margin: 0;
padding-top: 2px;
padding-bottom: 2px;
padding: 10px 0;
list-style: none;
&:hover {
......@@ -409,7 +408,7 @@
}
.panel-footer {
padding: 5px 10px;
padding: 0;
.btn {
min-width: auto;
......
......@@ -331,6 +331,10 @@ ul.notes {
&:hover {
color: $gl-link-color;
}
&:focus,
&:hover {
text-decoration: none;
}
}
......
......@@ -115,7 +115,7 @@
.table.ci-table {
&.builds-page tr {
&.builds-page tbody tr {
height: 71px;
}
......
......@@ -746,6 +746,8 @@ pre.light-well {
}
.protected-branches-list {
margin-bottom: 30px;
a {
color: $gl-text-color;
......
......@@ -24,3 +24,14 @@
.service-settings .control-label {
padding-top: 0;
}
.token-token-container {
#impersonation-token-token {
width: 80%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
}
.triggers-container {
.label-container {
display: inline-block;
margin-left: 10px;
}
}
.trigger-actions {
.btn {
margin-left: 10px;
}
}
......@@ -139,18 +139,10 @@
.blob-commit-info {
list-style: none;
background: $gray-light;
padding: 6px 0;
padding: 16px 16px 16px 6px;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
.table-list-cell {
border-bottom: none;
}
.commit-actions {
width: 260px;
}
}
#modal-remove-blob > .modal-dialog { width: 850px; }
......
......@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy]
before_action :load_scopes, only: [:new, :edit]
before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
......
class Admin::ImpersonationTokensController < Admin::ApplicationController
before_action :user
def index
set_index_vars
end
def create
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
render :index
end
end
def revoke
@impersonation_token = finder.find(params[:id])
if @impersonation_token.revoke!
flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!"
else
flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}."
end
redirect_to admin_user_impersonation_tokens_path
end
private
def user
@user ||= User.find_by!(username: params[:user_id])
end
def finder(options = {})
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
end
def impersonation_token_params
params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
end
def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
@active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
end
end
module RepositorySettingsRedirect
extend ActiveSupport::Concern
def redirect_to_repository_settings(project)
redirect_to namespace_project_settings_repository_path(project.namespace, project)
end
end
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :authenticate_resource_owner!
layout 'profile'
# Overriden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new
if pre_auth.authorizable?
if skip_authorization? || matching_token?
......@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
render "doorkeeper/authorizations/error"
end
end
# TODO: Handle raise invalid authorization
def create
redirect_or_render authorization.authorize
end
def destroy
redirect_or_render authorization.deny
end
private
def matching_token?
Doorkeeper::AccessToken.matching_token_for(pre_auth.client,
current_resource_owner.id,
pre_auth.scopes)
end
def redirect_or_render(auth)
if auth.redirectable?
redirect_to auth.redirect_uri
else
render json: auth.body, status: auth.status
end
end
def pre_auth
@pre_auth ||=
Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration,
server.client_via_uid,
params)
end
def authorization
@authorization ||= strategy.request
end
def strategy
@strategy ||= server.authorization_request(pre_auth.response_type)
end
end
......@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def create
@personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
@personal_access_token = finder.build(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
......@@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def revoke
@personal_access_token = current_user.personal_access_tokens.find(params[:id])
@personal_access_token = finder.find(params[:id])
if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
......@@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
def finder(options = {})
PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options))
end
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
def set_index_vars
@personal_access_token ||= current_user.personal_access_tokens.build
@scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
@scopes = Gitlab::Auth::API_SCOPES
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end
end
class Projects::DeployKeysController < Projects::ApplicationController
include RepositorySettingsRedirect
respond_to :html
# Authorize
......@@ -7,51 +8,36 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings"
def index
@key = DeployKey.new
set_index_vars
redirect_to_repository_settings(@project)
end
def new
redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
redirect_to_repository_settings(@project)
end
def create
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
set_index_vars
if @key.valid? && @project.deploy_keys << @key
redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
else
render "index"
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
def enable
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
redirect_to_repository_settings(@project)
end
def disable
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
redirect_back_or_default(default: { action: 'index' })
redirect_to_repository_settings(@project)
end
protected
def set_index_vars
@enabled_keys ||= @project.deploy_keys
@available_keys ||= current_user.accessible_deploy_keys - @enabled_keys
@available_project_keys ||= current_user.project_deploy_keys - @enabled_keys
@available_public_keys ||= DeployKey.are_public - @enabled_keys
# Public keys that are already used by another accessible project are already
# in @available_project_keys.
@available_public_keys -= @available_project_keys
end
def deploy_key_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
......
......@@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
......@@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
def metrics
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
@metrics = environment.metrics || {}
respond_to do |format|
format.html
format.json do
render json: @metrics, status: @metrics.any? ? :ok : :no_content
end
end
end
private
def verify_api_request!
......
class Projects::ProtectedBranchesController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
before_action :load_protected_branches, only: [:index]
layout "project_settings"
def index
@protected_branch = @project.protected_branches.new
load_gon_index
redirect_to_repository_settings(@project)
end
def create
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
if @protected_branch.persisted?
redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
else
load_protected_branches
load_gon_index
render :index
unless @protected_branch.persisted?
flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
def show
......@@ -45,7 +41,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
@protected_branch.destroy
respond_to do |format|
format.html { redirect_to namespace_project_protected_branches_path }
format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end
......@@ -61,24 +57,4 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id])
end
def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
end
def access_levels_options
{
push_access_levels: {
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
},
merge_access_levels: {
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
}
}
end
def load_gon_index
params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
gon.push(params.merge(access_levels_options))
end
end
module Projects
module Settings
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
def show
@deploy_keys = DeployKeysPresenter
.new(@project, current_user: current_user)
define_protected_branches
end
private
def define_protected_branches
load_protected_branches
@protected_branch = @project.protected_branches.new
load_gon_index
end
def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
end
def access_levels_options
{
push_access_levels: {
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
},
merge_access_levels: {
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
}
}
end
def open_branches
branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
{ open_branches: branches }
end
def load_gon_index
gon.push(open_branches.merge(access_levels_options))
end
end
end
end
class Projects::TriggersController < Projects::ApplicationController
before_action :authorize_admin_build!
before_action :authorize_manage_trigger!, except: [:index, :create]
before_action :authorize_admin_trigger!, only: [:edit, :update]
before_action :trigger, only: [:take_ownership, :edit, :update, :destroy]
layout 'project_settings'
......@@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController
end
def create
@trigger = project.triggers.new
@trigger.save
@trigger = project.triggers.create(create_params.merge(owner: current_user))
if @trigger.valid?
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
flash[:notice] = 'Trigger was created successfully.'
else
@triggers = project.triggers.select(&:persisted?)
render action: "show"
flash[:alert] = 'You could not create a new trigger.'
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def take_ownership
if trigger.update(owner: current_user)
flash[:notice] = 'Trigger was re-assigned.'
else
flash[:alert] = 'You could not take ownership of trigger.'
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def edit
end
def update
if trigger.update(update_params)
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
else
render action: "edit"
end
end
def destroy
trigger.destroy
flash[:alert] = "Trigger removed"
if trigger.destroy
flash[:notice] = "Trigger removed."
else
flash[:alert] = "Could not remove the trigger."
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
private
def authorize_manage_trigger!
access_denied! unless can?(current_user, :manage_trigger, trigger)
end
def authorize_admin_trigger!
access_denied! unless can?(current_user, :admin_trigger, trigger)
end
def trigger
@trigger ||= project.triggers.find(params[:id])
@trigger ||= project.triggers.find(params[:id]) || render_404
end
def create_params
params.require(:trigger).permit(:description)
end
def update_params
params.require(:trigger).permit(:description)
end
end
......@@ -14,6 +14,8 @@ class UploadsController < ApplicationController
end
disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
......
class PersonalAccessTokensFinder
attr_accessor :params
delegate :build, :find, :find_by, to: :execute
def initialize(params = {})
@params = params
end
def execute
tokens = PersonalAccessToken.all
tokens = by_user(tokens)
tokens = by_impersonation(tokens)
by_state(tokens)
end
private
def by_user(tokens)
return tokens unless @params[:user]
tokens.where(user: @params[:user])
end
def by_impersonation(tokens)
case @params[:impersonation]
when true
tokens.with_impersonation
when false
tokens.without_impersonation
else
tokens
end
end
def by_state(tokens)
case @params[:state]
when 'active'
tokens.active
when 'inactive'
tokens.inactive
else
tokens
end
end
end
......@@ -81,8 +81,8 @@ module ApplicationSettingsHelper
end
def repository_storages_options_for_select
options = Gitlab.config.repositories.storages.map do |name, path|
["#{name} - #{path}", name]
options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name]
end
options_for_select(options, @application_setting.repository_storages)
......
......@@ -48,6 +48,8 @@ module CiStatusHelper
'icon_status_created'
when 'skipped'
'icon_status_skipped'
when 'manual'
'icon_status_manual'
else
'icon_status_canceled'
end
......
......@@ -162,7 +162,12 @@ module EventsHelper
def event_note(text, options = {})
text = first_line_in_markdown(text, 150, options)
sanitize(text, tags: %w(a img b pre code p span))
sanitize(
text,
tags: %w(a img b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style']
)
end
def event_commit_title(message)
......
......@@ -74,6 +74,10 @@ module GitlabRoutingHelper
namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
end
def environment_metrics_path(environment, *args)
metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
end
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
......
......@@ -50,7 +50,7 @@ module SortingHelper
end
def sort_title_priority
'Priority'
'Label priority'
end
def sort_title_oldest_updated
......
class ChatTeam < ActiveRecord::Base
validates :team_id, presence: true
validates :namespace, uniqueness: true
belongs_to :namespace
end
......@@ -517,6 +517,27 @@ module Ci
]
end
def steps
[Gitlab::Ci::Build::Step.from_commands(self),
Gitlab::Ci::Build::Step.from_after_script(self)].compact
end
def image
Gitlab::Ci::Build::Image.from_image(self)
end
def services
Gitlab::Ci::Build::Image.from_services(self)
end
def artifacts
[options[:artifacts]]
end
def cache
[options[:cache]]
end
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
......@@ -543,10 +564,35 @@ module Ci
@unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
{ key: 'CI_JOB_ID', value: id.to_s, public: true },
{ key: 'CI_JOB_NAME', value: name, public: true },
{ key: 'CI_JOB_STAGE', value: stage, public: true },
{ key: 'CI_JOB_TOKEN', value: token, public: false },
{ key: 'CI_COMMIT_SHA', value: sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
{ key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
]
variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
variables.concat(legacy_variables)
end
def legacy_variables
variables = [
{ key: 'CI_BUILD_ID', value: id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: token, public: false },
{ key: 'CI_BUILD_REF', value: sha, public: true },
......@@ -554,14 +600,12 @@ module Ci
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
{ key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
{ key: 'CI_BUILD_STAGE', value: stage, public: true }
]
variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action?
variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag?
variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
variables
end
......
......@@ -29,8 +29,12 @@ module Ci
token[0...4]
end
def can_show_token?(user)
owner.blank? || owner == user
def legacy?
self.owner_id.blank?
end
def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end
end
end
......@@ -7,7 +7,7 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped manual].freeze
ACTIVE_STATUSES = %w[pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[manual failed pending running canceled success skipped].freeze
ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze
class_methods do
def status_sql
......
......@@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base
project.deployment_service.terminals(self) if has_terminals?
end
def has_metrics?
project.monitoring_service.present? && available? && last_deployment.present?
end
def metrics
project.monitoring_service.metrics(self) if has_metrics?
end
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
......
class OauthAccessGrant < Doorkeeper::AccessGrant
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end
class OauthAccessToken < ActiveRecord::Base
class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end
class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
add_authentication_token_field :token
......@@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
before_save :ensure_token
scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
def self.generate(params)
personal_access_token = self.new(params)
personal_access_token.ensure_token
personal_access_token
end
validates :scopes, presence: true
validate :validate_api_scopes
def revoke!
self.revoked = true
self.save
end
def active?
!revoked? && !expired?
end
protected
def validate_api_scopes
unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain API scopes"
end
end
end
......@@ -113,6 +113,7 @@ class Project < ActiveRecord::Base
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
......@@ -392,7 +393,7 @@ class Project < ActiveRecord::Base
end
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]
Gitlab.config.repositories.storages[repository_storage]['path']
end
def team
......@@ -771,6 +772,14 @@ class Project < ActiveRecord::Base
@deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
end
def monitoring_services
services.where(category: :monitoring)
end
def monitoring_service
@monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
issues_tracker.to_param == 'jira'
end
......
# Base class for monitoring services
#
# These services integrate with a deployment solution like Prometheus
# to provide additional features for environments.
class MonitoringService < Service
default_value_for :category, 'monitoring'
def self.supported_events
%w()
end
# Environments have a number of metrics
def metrics(environment)
raise NotImplementedError
end
end
class PrometheusService < MonitoringService
include ReactiveCaching
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
self.reactive_cache_lease_timeout = 30.seconds
self.reactive_cache_refresh_interval = 30.seconds
self.reactive_cache_lifetime = 1.minute
# Access to prometheus is directly through the API
prop_accessor :api_url
with_options presence: true, if: :activated? do
validates :api_url, url: true
end
after_save :clear_reactive_cache!
def initialize_properties
if properties.nil?
self.properties = {}
end
end
def title
'Prometheus'
end
def description
'Prometheus monitoring'
end
def help
'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.'
end
def self.to_param
'prometheus'
end
def fields
[
{
type: 'text',
name: 'api_url',
title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
}
]
end
# Check we can connect to the Prometheus API
def test(*args)
client.ping
{ success: true, result: 'Checked API endpoint' }
rescue Gitlab::PrometheusError => err
{ success: false, result: err }
end
def metrics(environment)
with_reactive_cache(environment.slug) do |data|
data
end
end
# Cache metrics for specific environment
def calculate_reactive_cache(environment_slug)
return unless active? && project && !project.pending_delete?
memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024}
cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
{
success: true,
metrics: {
# Memory used in MB
memory_values: client.query_range(memory_query, start: 8.hours.ago),
memory_current: client.query(memory_query),
# CPU Usage rate in cores.
cpu_values: client.query_range(cpu_query, start: 8.hours.ago),
cpu_current: client.query(cpu_query)
},
last_update: Time.now.utc
}
rescue Gitlab::PrometheusError => err
{ success: false, result: err.message }
end
def client
@prometheus ||= Gitlab::Prometheus.new(api_url: api_url)
end
end
......@@ -50,10 +50,6 @@ class Repository
end
end
def self.storages
Gitlab.config.repositories.storages
end
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
......
......@@ -232,6 +232,7 @@ class Service < ActiveRecord::Base
mattermost
pipelines_email
pivotaltracker
prometheus
pushover
redmine
slack_slash_commands
......
......@@ -325,8 +325,7 @@ class User < ActiveRecord::Base
end
def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
personal_access_token&.user
PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end
# Returns a user for the given SSH key.
......
module Ci
class TriggerPolicy < BasePolicy
def rules
delegate! @subject.project
if can?(:admin_build)
can! :admin_trigger if @subject.owner.blank? ||
@subject.owner == @user
can! :manage_trigger
end
end
end
end
module Projects
module Settings
class DeployKeysPresenter < Gitlab::View::Presenter::Simple
presents :project
delegate :size, to: :enabled_keys, prefix: true
delegate :size, to: :available_project_keys, prefix: true
delegate :size, to: :available_public_keys, prefix: true
def new_key
@key ||= DeployKey.new
end
def enabled_keys
@enabled_keys ||= project.deploy_keys
end
def any_keys_enabled?
enabled_keys.any?
end
def available_keys
@available_keys ||= current_user.accessible_deploy_keys - enabled_keys
end
def available_project_keys
@available_project_keys ||= current_user.project_deploy_keys - enabled_keys
end
def any_available_project_keys_enabled?
available_project_keys.any?
end
def key_available?(deploy_key)
available_keys.include?(deploy_key)
end
def available_public_keys
return @available_public_keys if defined?(@available_public_keys)
@available_public_keys ||= DeployKey.are_public - enabled_keys
# Public keys that are already used by another accessible project are already
# in @available_project_keys.
@available_public_keys -= available_project_keys
end
def any_available_public_keys_enabled?
available_public_keys.any?
end
def to_partial_path
'projects/deploy_keys/index'
end
def form_partial_path
'projects/deploy_keys/form'
end
end
end
end
module Ci
# This class responsible for assigning
# proper pending build to runner on runner API request
class RegisterBuildService
class RegisterJobService
include Gitlab::CurrentSettings
attr_reader :runner
......
......@@ -99,6 +99,8 @@ class GitPushService < BaseService
UpdateMergeRequestsWorker
.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks)
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
......
- page_title "Impersonation Tokens", @user.name, "Users"
= render 'admin/users/head'
.row.prepend-top-default
.col-lg-12
= render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens
......@@ -21,4 +21,6 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
= nav_link(controller: :impersonation_tokens) do
= link_to "Impersonation Tokens", admin_user_impersonation_tokens_path(@user)
.append-bottom-default
......@@ -27,6 +27,7 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
= form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid
......@@ -34,4 +35,5 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
......@@ -4,18 +4,14 @@
%span
Members
- if can_edit
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= nav_link(controller: :repository) do
= link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do
%span
Deploy Keys
Repository
= nav_link(controller: :integrations) do
= link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span
Integrations
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span
Protected Branches
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
......
......@@ -24,80 +24,11 @@
%hr
%h5.prepend-top-0
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
= render "form", personal_access_token: @personal_access_token, scopes: @scopes
%hr
%h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
- if @active_personal_access_tokens.present?
.table-responsive
%table.table.active-personal-access-tokens
%thead
%tr
%th Name
%th Created
%th Expires
%th Scopes
%th
%tbody
- @active_personal_access_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
%td
- if token.expires_at.present?
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
.settings-message.text-center
You don't have any active tokens yet.
%hr
%h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
- if @inactive_personal_access_tokens.present?
.table-responsive
%table.table.inactive-personal-access-tokens
%thead
%tr
%th Name
%th Created
%tbody
- @inactive_personal_access_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
- else
.settings-message.text-center
There are no inactive tokens.
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
:javascript
var $dateField = $('#personal_access_token_expires_at');
var date = $dateField.val();
new Pikaday({
field: $dateField.get(0),
theme: 'gitlab-theme',
format: 'yyyy-mm-dd',
minDate: new Date(),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
$("#created-personal-access-token").click(function() {
this.select();
});
......@@ -18,7 +18,7 @@
- else
= link_to title, '#'
%ul.blob-commit-info.table-list.hidden-xs
%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
......
......@@ -9,33 +9,34 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
%li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
.table-list-cell.avatar-cell.hidden-xs
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
.table-list-cell.commit-content
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
%span.commit-row-message.visible-xs-inline
&middot;
= commit.short_id
- if commit.status(ref)
.visible-xs-inline
= render_commit_status(commit, ref: ref)
- if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ...
.commit-detail
.commit-content
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
%span.commit-row-message.visible-xs-inline
&middot;
= commit.short_id
- if commit.status(ref)
.visible-xs-inline
= render_commit_status(commit, ref: ref)
- if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ...
- if commit.description?
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commiter
= commit_author_link(commit, avatar: false, size: 24)
committed
#{time_ago_with_tooltip(commit.committed_date)}
- if commit.description?
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commiter
= commit_author_link(commit, avatar: false, size: 24)
committed
#{time_ago_with_tooltip(commit.committed_date)}
.table-list-cell.commit-actions.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
= clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit)
.commit-actions.flex-row.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
= clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit)
......@@ -11,4 +11,4 @@
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
%ul.content-list.table-list= render commits, project: @project, ref: @ref
%ul.content-list= render commits, project: @project, ref: @ref
......@@ -4,7 +4,7 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row
%ul.content-list.commit-list.table-list.table-wide
%ul.content-list.commit-list
= render commits, project: project, ref: ref
- if hidden > 0
......
......@@ -18,7 +18,7 @@
%span.key-created-at
created #{time_ago_with_tooltip(deploy_key.created_at)}
.visible-xs-block.visible-sm-block
- if @available_keys.include?(deploy_key)
- if @deploy_keys.key_available?(deploy_key)
= link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do
Enable
- else
......
= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
= form_errors(@key)
= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
= form_errors(@deploy_keys.new_key)
.form-group
= f.label :title, class: "label-light"
= f.text_field :title, class: 'form-control', autofocus: true, required: true
......
- page_title "Deploy Keys"
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= page_title
Deploy Keys
%p
Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
.col-lg-9
%h5.prepend-top-0
Create a new deploy key for this project
= render "form"
= render @deploy_keys.form_partial_path
.col-lg-9.col-lg-offset-3
%hr
.col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
%h5.prepend-top-0
Enabled deploy keys for this project (#{@enabled_keys.size})
- if @enabled_keys.any?
Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size})
- if @deploy_keys.any_keys_enabled?
%ul.well-list
= render @enabled_keys
= render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key
- else
.settings-message.text-center
No deploy keys found. Create one with the form above or add existing one below.
No deploy keys found. Create one with the form above.
%h5.prepend-top-default
Deploy keys from projects you have access to (#{@available_project_keys.size})
- if @available_project_keys.any?
Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size})
- if @deploy_keys.any_available_project_keys_enabled?
%ul.well-list
= render @available_project_keys
= render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
- else
.settings-message.text-center
No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- if @available_public_keys.any?
- if @deploy_keys.any_available_public_keys_enabled?
%h5.prepend-top-default
Public deploy keys available to any project (#{@available_public_keys.size})
Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
%ul.well-list
= render @available_public_keys
= render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key
- environment = local_assigns.fetch(:environment)
- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart')
- @no_container = true
- page_title "Metrics for environment", @environment.name
= render "projects/pipelines/head"
%div{ class: container_class }
.top-area
.row
.col-sm-6
%h3.page-title
Environment:
= @environment.name
.col-sm-6
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
......@@ -8,6 +8,7 @@
%h3.page-title= @environment.name
.col-md-3
.nav-controls
= render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
......
......@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
= dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
......@@ -30,7 +30,7 @@
branches: @merge_request.source_branches,
selected: f.object.source_branch
.panel-footer
= icon('spinner spin', class: 'js-source-loading')
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
.col-md-6
......@@ -60,7 +60,7 @@
branches: @merge_request.target_branches,
selected: f.object.target_branch
.panel-footer
= icon('spinner spin', class: "js-target-loading")
.text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
......
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- %w[success success_with_warnings skipped manual canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
%div{ class: "ci-status-icon ci-status-icon-#{status}" }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
......
......@@ -27,6 +27,8 @@
= render 'projects/merge_requests/widget/open/build_failed'
- elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions'
- elsif @pipeline&.blocked?
= render 'projects/merge_requests/widget/open/manual'
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
......
%h4
Pipeline blocked
%p
The pipeline for this merge request requires a manual action to proceed.
- @stage.statuses.latest.each do |status|
%li
= render 'ci/status/dropdown_graph_badge', subject: status
- grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status)
- HasStatus::ORDERED_STATUSES.each do |ordered_status|
- grouped_statuses.fetch(ordered_status, []).each do |status|
%li
= render 'ci/status/dropdown_graph_badge', subject: status
......@@ -23,6 +23,6 @@
- if can_admin_project
%th
%tbody
= render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
= render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
= paginate @protected_branches, theme: 'gitlab'
......@@ -10,7 +10,7 @@
= f.label :name, class: 'col-md-2 text-right' do
Branch:
.col-md-10
= render partial: "dropdown", locals: { f: f }
= render partial: "projects/protected_branches/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
such as
......
- page_title "Protected branches"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_branches')
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
= page_title
Protected Branches
%p Keep stable branches secure and force developers to use merge requests.
%p.prepend-top-20
By default, protected branches are designed to:
......@@ -17,6 +16,6 @@
%p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
.col-lg-9
- if can? current_user, :admin_project, @project
= render 'create_protected_branch'
= render 'projects/protected_branches/create_protected_branch'
= render "branches_list"
= render "projects/protected_branches/branches_list"
......@@ -14,7 +14,7 @@
- else
(branch was removed from repository)
= render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
= render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch }
- if can_admin_project
%td
......
- page_title "Repository"
= render @deploy_keys
= render "projects/protected_branches/index"
%h4.prepend-top-0
Triggers
%p.prepend-top-20
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
impersonate their associated user including their access to projects and their project
permissions.
%p.prepend-top-20
Triggers with the
%span.label.label-primary legacy
label do not have an associated user and only have access to the current project.
%p.append-bottom-0
= succeed '.' do
Learn more in the
= link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
= form_for [@project.namespace.becomes(Namespace), @project, @trigger], html: { class: 'gl-show-field-errors' } do |f|
= form_errors(@trigger)
- if @trigger.token
.form-group
%label.label-light Token
%p.form-control-static= @trigger.token
.form-group
= f.label :key, "Description", class: "label-light"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
= f.submit btn_text, class: "btn btn-save"
.row.prepend-top-default.append-bottom-default
.row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-3
%h4.prepend-top-0
Triggers
%p.prepend-top-20
Triggers can force a specific branch or tag to get rebuilt with an API call.
%p.append-bottom-0
= succeed '.' do
Learn more in the
= link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
= render "projects/triggers/content"
.col-lg-9
.panel.panel-default
.panel-heading
%h4.panel-title
Manage your project's triggers
.panel-body
= render "projects/triggers/form", btn_text: "Add trigger"
%hr
- if @triggers.any?
.table-responsive
.table-responsive.triggers-list
%table.table
%thead
%th
%strong Token
%th
%strong Description
%th
%strong Owner
%th
%strong Last used
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.append-bottom-default
No triggers have been created yet. Add one using the button below.
= form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f|
= f.submit "Add trigger", class: 'btn btn-success'
No triggers have been created yet. Add one using the form above.
.panel-footer
......
%tr
%td
%span.monospace= trigger.token
- if can?(current_user, :admin_trigger, trigger)
%span= trigger.token
= clipboard_button(clipboard_text: trigger.token, title: "Copy trigger token to clipboard")
- else
%span= trigger.short_token
.label-container
- if trigger.legacy?
%span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
- if !trigger.can_access_project?
%span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
%td
- if trigger.description? && trigger.description.length > 15
%span.has-tooltip{ title: trigger.description }= truncate(trigger.description, length: 15)
- else
= trigger.description
%td
- if trigger.owner
.trigger-owner.sr-only= trigger.owner.name
= user_avatar(user: trigger.owner, size: 20)
%td
- if trigger.last_trigger_request
#{time_ago_in_words(trigger.last_trigger_request.created_at)} ago
- if trigger.last_used
#{time_ago_in_words(trigger.last_used)} ago
- else
Never
%td.text-right
= link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-warning btn-sm"
%td.text-right.trigger-actions
- take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?"
- revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
- if trigger.owner != current_user && can?(current_user, :manage_trigger, trigger)
= link_to 'Take ownership', take_ownership_namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: take_ownership_confirmation }, method: :post, class: "btn btn-default btn-sm btn-trigger-take-ownership"
- if can?(current_user, :admin_trigger, trigger)
= link_to edit_namespace_project_trigger_path(@project.namespace, @project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do
%i.fa.fa-pencil
- if can?(current_user, :manage_trigger, trigger)
= link_to namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
%i.fa.fa-trash
- page_title "Trigger"
.row.prepend-top-default.append-bottom-default
.col-lg-3
= render "content"
.col-lg-9
%h4.prepend-top-0
Update trigger
= render "form", btn_text: "Save trigger"
......@@ -11,7 +11,7 @@
.results.prepend-top-10
- if @scope == 'commits'
%ul.content-list.commit-list.table-list.table-wide
%ul.content-list.commit-list
= render partial: "search/results/commit", collection: @search_objects
- else
.search-results
......
- personal_access_token = local_assigns.fetch(:personal_access_token)
- scopes = local_assigns.fetch(:scopes)
- type = impersonation ? "Impersonation" : "Personal Access"
= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
%h5.prepend-top-0
Add a #{type} Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique #{type} Token.
= form_errors(personal_access_token)
= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
= form_errors(token)
.form-group
= f.label :name, class: 'label-light'
......@@ -15,7 +19,21 @@
.form-group
= f.label :scopes, class: 'label-light'
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes
.prepend-top-default
= f.submit 'Create Personal Access Token', class: "btn btn-create"
= f.submit "Create #{type} Token", class: "btn btn-create"
:javascript
var $dateField = $('.datepicker');
var date = $dateField.val();
new Pikaday({
field: $dateField.get(0),
theme: 'gitlab-theme',
format: 'yyyy-mm-dd',
minDate: new Date(),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
- type = impersonation ? "Impersonation" : "Personal Access"
%hr
%h5 Active #{type} Tokens (#{active_tokens.length})
- if impersonation
%p.profile-settings-content
To see all the user's personal access tokens you must impersonate them first.
- if active_tokens.present?
.table-responsive
%table.table.active-tokens
%thead
%tr
%th Name
%th Created
%th Expires
%th Scopes
- if impersonation
%th Token
%th
%tbody
- active_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
%td
- if token.expires?
%span{ class: ('text-warning' if token.expires_soon?) }
In #{distance_of_time_in_words_to_now(token.expires_at)}
- else
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
- if impersonation
%td.token-token-container
= text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control"
= clipboard_button(clipboard_text: token.token)
- path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
%td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
- else
.settings-message.text-center
This user has no active #{type} Tokens.
%hr
%h5 Inactive #{type} Tokens (#{inactive_tokens.length})
- if inactive_tokens.present?
.table-responsive
%table.table.inactive-tokens
%thead
%tr
%th Name
%th Created
%tbody
- inactive_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
- else
.settings-message.text-center
This user has no inactive #{type} Tokens.
......@@ -112,7 +112,7 @@
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
.filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
:javascript
......
......@@ -3,8 +3,8 @@ class PostReceive
include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
repo_path.gsub!(path[1].to_s, "")
if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) }
repo_path.gsub!(repository_storage[1]['path'].to_s, "")
else
log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
end
......
class SystemHookPushWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(push_data, hook_id)
SystemHooksService.new.execute_hooks(push_data, hook_id)
end
end
......@@ -10,8 +10,5 @@ class UpdateMergeRequestsWorker
return unless user
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, [])
SystemHooksService.new.execute_hooks(push_data, :push_hooks)
end
end
---
title: Fix cherry-picking or reverting through an MR
title: Align bulk update issues button to the right
merge_request:
author:
---
title: Manage user personal access tokens through api and add impersonation tokens
merge_request: 9099
author: Simon Vocella
---
title: Combined deploy keys, push rules, protect branches and mirror repository settings options into a single one called
Repository
merge_request:
author:
---
title: fix background color for labels mention in todo
merge_request: 9155
author: mhasbini
---
title: Refactor dropdown_assignee_spec
merge_request: 9711
author: George Andrinopoulos
---
title: Uploaded files which content can change now require revalidation on each page load
merge_request: 9453
author:
---
title: Update storage settings to allow extra values per repository storage
merge_request: 9597
author:
---
title: Fix the redirect to custom home page URL
merge_request: 9518
author:
---
title: Don't copy tooltip when copying GFM
title: Fix jobs table header height
merge_request:
author:
---
title: Fix broken migration when upgrading straight to 8.17.1
merge_request: 9613
author:
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment