Merge branch 'master' into developer_can_push_to_protected_branches_option
Conflicts: db/schema.rb
This commit is contained in:
commit
42fb42aed4
81 changed files with 1510 additions and 423 deletions
60
.gitignore
vendored
60
.gitignore
vendored
|
@ -1,42 +1,42 @@
|
|||
.bundle
|
||||
.rbx/
|
||||
db/*.sqlite3
|
||||
db/*.sqlite3-journal
|
||||
log/*.log*
|
||||
tmp/
|
||||
.sass-cache/
|
||||
coverage/*
|
||||
backups/*
|
||||
*.log
|
||||
*.swp
|
||||
public/uploads/
|
||||
.ruby-version
|
||||
.ruby-gemset
|
||||
.rvmrc
|
||||
.rbenv-version
|
||||
.DS_Store
|
||||
.bundle
|
||||
.chef
|
||||
.directory
|
||||
nohup.out
|
||||
Vagrantfile
|
||||
.envrc
|
||||
.gitlab_shell_secret
|
||||
.idea
|
||||
.rbenv-version
|
||||
.rbx/
|
||||
.ruby-gemset
|
||||
.ruby-version
|
||||
.rvmrc
|
||||
.sass-cache/
|
||||
.secret
|
||||
.vagrant
|
||||
config/gitlab.yml
|
||||
Vagrantfile
|
||||
backups/*
|
||||
config/aws.yml
|
||||
config/database.yml
|
||||
config/gitlab.yml
|
||||
config/initializers/omniauth.rb
|
||||
config/initializers/rack_attack.rb
|
||||
config/initializers/smtp_settings.rb
|
||||
config/unicorn.rb
|
||||
config/resque.yml
|
||||
config/aws.yml
|
||||
config/unicorn.rb
|
||||
coverage/*
|
||||
db/*.sqlite3
|
||||
db/*.sqlite3-journal
|
||||
db/data.yml
|
||||
.idea
|
||||
.DS_Store
|
||||
.chef
|
||||
vendor/bundle/*
|
||||
rails_best_practices_output.html
|
||||
doc/code/*
|
||||
.secret
|
||||
*.log
|
||||
public/uploads.*
|
||||
public/assets/
|
||||
.envrc
|
||||
dump.rdb
|
||||
log/*.log*
|
||||
nohup.out
|
||||
public/assets/
|
||||
public/uploads.*
|
||||
public/uploads/
|
||||
rails_best_practices_output.html
|
||||
tags
|
||||
.gitlab_shell_secret
|
||||
tmp/
|
||||
vendor/bundle/*
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.1.3
|
||||
2.1.5
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
v 7.7.0
|
||||
-
|
||||
-
|
||||
- Add Jetbrains Teamcity CI service (Jason Lippert)
|
||||
-
|
||||
-
|
||||
-
|
||||
-
|
||||
-
|
||||
- OAuth applications feature
|
||||
-
|
||||
-
|
||||
- Set project path instead of project name in create form
|
||||
-
|
||||
-
|
||||
-
|
||||
-
|
||||
-
|
||||
-
|
||||
- New side navigation
|
||||
|
||||
|
||||
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -29,6 +29,8 @@ gem 'omniauth-twitter'
|
|||
gem 'omniauth-github'
|
||||
gem 'omniauth-shibboleth'
|
||||
gem 'omniauth-kerberos'
|
||||
gem 'doorkeeper', '2.0.1'
|
||||
gem "rack-oauth2", "~> 1.0.5"
|
||||
|
||||
# Extracting information from a git repository
|
||||
# Provide access to Gitlab::Git library
|
||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -37,6 +37,7 @@ GEM
|
|||
rake (>= 0.8.7)
|
||||
arel (5.0.1.20140414130214)
|
||||
asciidoctor (0.1.4)
|
||||
attr_required (1.0.0)
|
||||
awesome_print (1.2.0)
|
||||
axiom-types (0.0.5)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
|
@ -107,6 +108,8 @@ GEM
|
|||
diff-lcs (1.2.5)
|
||||
diffy (3.0.3)
|
||||
docile (1.1.5)
|
||||
doorkeeper (2.0.1)
|
||||
railties (>= 3.1)
|
||||
dotenv (0.9.0)
|
||||
dropzonejs-rails (0.4.14)
|
||||
rails (> 3.1)
|
||||
|
@ -250,6 +253,7 @@ GEM
|
|||
json (~> 1.8)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpauth (0.2.1)
|
||||
httpclient (2.5.3.3)
|
||||
i18n (0.6.11)
|
||||
ice_nine (0.10.0)
|
||||
jasmine (2.0.2)
|
||||
|
@ -368,6 +372,12 @@ GEM
|
|||
rack (>= 1.1.3)
|
||||
rack-mount (0.8.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-oauth2 (1.0.8)
|
||||
activesupport (>= 2.3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.2.0.2)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-protection (1.5.1)
|
||||
rack
|
||||
rack-test (0.6.2)
|
||||
|
@ -616,6 +626,7 @@ DEPENDENCIES
|
|||
devise (= 3.2.4)
|
||||
devise-async (= 0.9.0)
|
||||
diffy (~> 3.0.3)
|
||||
doorkeeper (= 2.0.1)
|
||||
dropzonejs-rails
|
||||
email_spec
|
||||
enumerize
|
||||
|
@ -672,6 +683,7 @@ DEPENDENCIES
|
|||
rack-attack
|
||||
rack-cors
|
||||
rack-mini-profiler
|
||||
rack-oauth2 (~> 1.0.5)
|
||||
rails (~> 4.1.0)
|
||||
rails_autolink (~> 1.1)
|
||||
rails_best_practices
|
||||
|
|
|
@ -89,6 +89,9 @@ class @MergeRequest
|
|||
this.$('.merge-request-tabs .diffs-tab').addClass 'active'
|
||||
this.loadDiff() unless @diffs_loaded
|
||||
this.$('.diffs').show()
|
||||
when 'commits'
|
||||
this.$('.merge-request-tabs .commits-tab').addClass 'active'
|
||||
this.$('.commits').show()
|
||||
else
|
||||
this.$('.merge-request-tabs .notes-tab').addClass 'active'
|
||||
this.$('.notes').show()
|
||||
|
|
|
@ -207,26 +207,6 @@ li.note {
|
|||
}
|
||||
}
|
||||
|
||||
.no-ssh-key-message {
|
||||
padding: 10px 0;
|
||||
background: #C67;
|
||||
margin: 0;
|
||||
color: #FFF;
|
||||
margin-top: -1px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.links-xs {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.warning_message {
|
||||
border-left: 4px solid #ed9;
|
||||
color: #b90;
|
||||
|
@ -282,7 +262,7 @@ img.emoji {
|
|||
}
|
||||
|
||||
.navless-container {
|
||||
margin-top: 20px;
|
||||
margin-top: 68px;
|
||||
}
|
||||
|
||||
.description-block {
|
||||
|
@ -355,3 +335,9 @@ table {
|
|||
.task-status {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#nprogress .spinner {
|
||||
top: auto !important;
|
||||
bottom: 20px !important;
|
||||
left: 20px !important;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
.issue-box {
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
padding: 7px 13px;
|
||||
font-weight: normal;
|
||||
margin-right: 5px;
|
||||
|
||||
&.issue-box-closed {
|
||||
background-color: $bg_danger;
|
||||
|
|
20
app/assets/stylesheets/generic/tables.scss
Normal file
20
app/assets/stylesheets/generic/tables.scss
Normal file
|
@ -0,0 +1,20 @@
|
|||
table {
|
||||
&.table {
|
||||
tr {
|
||||
td, th {
|
||||
padding: 8px 10px;
|
||||
line-height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
th {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
border-bottom: 1px solid #CCC !important;
|
||||
}
|
||||
td {
|
||||
border-color: #F1F1F1 !important;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,4 +46,4 @@ $deleted: #f77;
|
|||
/**
|
||||
* NProgress customize
|
||||
*/
|
||||
$nprogress-color: #3498db;
|
||||
$nprogress-color: #c0392b;
|
||||
|
|
|
@ -4,9 +4,13 @@
|
|||
*/
|
||||
header {
|
||||
&.navbar-gitlab {
|
||||
z-index: 100;
|
||||
margin-bottom: 0;
|
||||
min-height: 40px;
|
||||
border: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
.navbar-inner {
|
||||
filter: none;
|
||||
|
@ -82,8 +86,6 @@ header {
|
|||
}
|
||||
}
|
||||
|
||||
z-index: 10;
|
||||
|
||||
.container {
|
||||
width: 100% !important;
|
||||
padding-left: 0px;
|
||||
|
|
|
@ -19,13 +19,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.merge-request .merge-request-tabs{
|
||||
margin: 20px 0;
|
||||
@media(min-width: $screen-sm-max) {
|
||||
.merge-request .merge-request-tabs{
|
||||
margin: 20px 0;
|
||||
|
||||
li {
|
||||
a {
|
||||
padding: 15px 40px;
|
||||
font-size: 14px;
|
||||
li {
|
||||
a {
|
||||
padding: 15px 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +104,7 @@
|
|||
.mr-state-widget {
|
||||
background: $box_bg;
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09));
|
||||
|
||||
.ci_widget {
|
||||
|
@ -146,7 +149,6 @@
|
|||
padding: 10px 15px;
|
||||
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
z-index: 99;
|
||||
overflow-y: auto;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
@ -11,6 +12,7 @@
|
|||
width: 100%;
|
||||
padding: 15px;
|
||||
background: #FFF;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
|
@ -60,7 +62,7 @@
|
|||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
text-shadow: 0 1px 2px #FFF;
|
||||
padding-left: 67px;
|
||||
padding-left: 20px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
@ -75,6 +77,7 @@
|
|||
i {
|
||||
width: 20px;
|
||||
color: #888;
|
||||
margin-right: 23px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +94,7 @@
|
|||
a {
|
||||
padding: 5px 15px;
|
||||
font-size: 12px;
|
||||
padding-left: 67px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,10 +106,11 @@
|
|||
|
||||
.sidebar-wrapper {
|
||||
width: 250px;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 250px;
|
||||
height: 100%;
|
||||
margin-left: -250px;
|
||||
border-right: 1px solid #EAEAEA;
|
||||
|
||||
.nav-sidebar {
|
||||
margin-top: 20px;
|
||||
|
@ -118,7 +122,6 @@
|
|||
|
||||
.content-wrapper {
|
||||
padding: 20px;
|
||||
border-left: 1px solid #EAEAEA;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,14 +132,16 @@
|
|||
|
||||
.sidebar-wrapper {
|
||||
width: 52px;
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
margin-left: -50px;
|
||||
border-right: 1px solid #EAEAEA;
|
||||
overflow-x: hidden;
|
||||
|
||||
.nav-sidebar {
|
||||
margin-top: 20px;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
width: 52px;
|
||||
|
||||
|
|
|
@ -17,19 +17,6 @@
|
|||
@include border-radius(0);
|
||||
|
||||
tr {
|
||||
td, th {
|
||||
padding: 8px 10px;
|
||||
line-height: 20px;
|
||||
}
|
||||
th {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
border-bottom: 1px solid #CCC !important;
|
||||
}
|
||||
td {
|
||||
border-color: #F1F1F1 !important;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
&:hover {
|
||||
td {
|
||||
background: $hover;
|
||||
|
|
|
@ -257,10 +257,6 @@ class ApplicationController < ActionController::Base
|
|||
# or improve current implementation to filter only issues you
|
||||
# created or assigned or mentioned
|
||||
#@filter_params[:authorized_only] = true
|
||||
|
||||
unless @filter_params[:assignee_id]
|
||||
@filter_params[:assignee_id] = current_user.id
|
||||
end
|
||||
end
|
||||
|
||||
@filter_params
|
||||
|
|
41
app/controllers/oauth/applications_controller.rb
Normal file
41
app/controllers/oauth/applications_controller.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||
before_filter :authenticate_user!
|
||||
layout "profile"
|
||||
|
||||
def index
|
||||
head :forbidden and return
|
||||
end
|
||||
|
||||
def create
|
||||
@application = Doorkeeper::Application.new(application_params)
|
||||
|
||||
if Doorkeeper.configuration.confirm_application_owner?
|
||||
@application.owner = current_user
|
||||
end
|
||||
|
||||
if @application.save
|
||||
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
|
||||
redirect_to oauth_application_url(@application)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @application.destroy
|
||||
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
|
||||
end
|
||||
|
||||
redirect_to applications_profile_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_application
|
||||
@application = current_user.oauth_applications.find(params[:id])
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |exception|
|
||||
render "errors/not_found", layout: "errors", status: 404
|
||||
end
|
||||
end
|
57
app/controllers/oauth/authorizations_controller.rb
Normal file
57
app/controllers/oauth/authorizations_controller.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
before_filter :authenticate_resource_owner!
|
||||
layout "profile"
|
||||
|
||||
def new
|
||||
if pre_auth.authorizable?
|
||||
if skip_authorization? || matching_token?
|
||||
auth = authorization.authorize
|
||||
redirect_to auth.redirect_uri
|
||||
else
|
||||
render "doorkeeper/authorizations/new"
|
||||
end
|
||||
else
|
||||
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
|
|
@ -0,0 +1,8 @@
|
|||
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||
layout "profile"
|
||||
|
||||
def destroy
|
||||
Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner)
|
||||
redirect_to applications_profile_url, notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
|
||||
end
|
||||
end
|
|
@ -13,6 +13,11 @@ class ProfilesController < ApplicationController
|
|||
def design
|
||||
end
|
||||
|
||||
def applications
|
||||
@applications = current_user.oauth_applications
|
||||
@authorized_tokens = current_user.oauth_authorized_tokens
|
||||
end
|
||||
|
||||
def update
|
||||
user_params.except!(:email) if @user.ldap_user?
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ class Projects::ServicesController < Projects::ApplicationController
|
|||
:title, :token, :type, :active, :api_key, :subdomain,
|
||||
:room, :recipients, :project_url, :webhook,
|
||||
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
|
||||
:build_key, :server
|
||||
:build_key, :server, :teamcity_url, :build_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,4 @@
|
|||
module DashboardHelper
|
||||
def entities_per_project(project, entity)
|
||||
case entity.to_sym
|
||||
when :issue then @issues.where(project_id: project.id)
|
||||
when :merge_request then @merge_requests.where(target_project_id: project.id)
|
||||
else
|
||||
[]
|
||||
end.count
|
||||
end
|
||||
|
||||
def projects_dashboard_filter_path(options={})
|
||||
exist_opts = {
|
||||
sort: params[:sort],
|
||||
|
@ -22,32 +13,11 @@ module DashboardHelper
|
|||
path
|
||||
end
|
||||
|
||||
def assigned_entities_count(current_user, entity, scope = nil)
|
||||
items = current_user.send('assigned_' + entity.pluralize)
|
||||
get_count(items, scope)
|
||||
def assigned_issues_dashboard_path
|
||||
issues_dashboard_path(assignee_id: current_user.id)
|
||||
end
|
||||
|
||||
def authored_entities_count(current_user, entity, scope = nil)
|
||||
items = current_user.send(entity.pluralize)
|
||||
get_count(items, scope)
|
||||
end
|
||||
|
||||
def authorized_entities_count(current_user, entity, scope = nil)
|
||||
items = entity.classify.constantize
|
||||
get_count(items, scope, true, current_user)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get_count(items, scope, get_authorized = false, current_user = nil)
|
||||
items = items.opened
|
||||
if scope.kind_of?(Group)
|
||||
items = items.of_group(scope)
|
||||
elsif scope.kind_of?(Project)
|
||||
items = items.of_projects(scope)
|
||||
elsif get_authorized
|
||||
items = items.of_projects(current_user.authorized_projects)
|
||||
end
|
||||
items.count
|
||||
def assigned_mrs_dashboard_path
|
||||
merge_requests_dashboard_path(assignee_id: current_user.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -117,4 +117,22 @@ module DiffHelper
|
|||
|
||||
[comments_left, comments_right]
|
||||
end
|
||||
|
||||
def inline_diff_btn
|
||||
params_copy = params.dup
|
||||
params_copy[:view] = 'inline'
|
||||
|
||||
link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do
|
||||
'Inline'
|
||||
end
|
||||
end
|
||||
|
||||
def parallel_diff_btn
|
||||
params_copy = params.dup
|
||||
params_copy[:view] = 'parallel'
|
||||
|
||||
link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do
|
||||
'Side-by-side'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,6 +28,10 @@ module TabHelper
|
|||
# nav_link(controller: [:tree, :refs]) { "Hello" }
|
||||
# # => '<li class="active">Hello</li>'
|
||||
#
|
||||
# # Several paths
|
||||
# nav_link(path: ['tree#show', 'profile#show']) { "Hello" }
|
||||
# # => '<li class="active">Hello</li>'
|
||||
#
|
||||
# # Shorthand path
|
||||
# nav_link(path: 'tree#show') { "Hello" }
|
||||
# # => '<li class="active">Hello</li>'
|
||||
|
@ -38,25 +42,7 @@ module TabHelper
|
|||
#
|
||||
# Returns a list item element String
|
||||
def nav_link(options = {}, &block)
|
||||
if path = options.delete(:path)
|
||||
if path.respond_to?(:each)
|
||||
c = path.map { |p| p.split('#').first }
|
||||
a = path.map { |p| p.split('#').last }
|
||||
else
|
||||
c, a, _ = path.split('#')
|
||||
end
|
||||
else
|
||||
c = options.delete(:controller)
|
||||
a = options.delete(:action)
|
||||
end
|
||||
|
||||
if c && a
|
||||
# When given both options, make sure BOTH are active
|
||||
klass = current_controller?(*c) && current_action?(*a) ? 'active' : ''
|
||||
else
|
||||
# Otherwise check EITHER option
|
||||
klass = current_controller?(*c) || current_action?(*a) ? 'active' : ''
|
||||
end
|
||||
klass = active_nav_link?(options) ? 'active' : ''
|
||||
|
||||
# Add our custom class into the html_options, which may or may not exist
|
||||
# and which may or may not already have a :class key
|
||||
|
@ -72,6 +58,34 @@ module TabHelper
|
|||
end
|
||||
end
|
||||
|
||||
def active_nav_link?(options)
|
||||
if path = options.delete(:path)
|
||||
unless path.respond_to?(:each)
|
||||
path = [path]
|
||||
end
|
||||
|
||||
path.any? do |single_path|
|
||||
current_path?(single_path)
|
||||
end
|
||||
else
|
||||
c = options.delete(:controller)
|
||||
a = options.delete(:action)
|
||||
|
||||
if c && a
|
||||
# When given both options, make sure BOTH are true
|
||||
current_controller?(*c) && current_action?(*a)
|
||||
else
|
||||
# Otherwise check EITHER option
|
||||
current_controller?(*c) || current_action?(*a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_path?(path)
|
||||
c, a, _ = path.split('#')
|
||||
current_controller?(c) && current_action?(a)
|
||||
end
|
||||
|
||||
def project_tab_class
|
||||
return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ class Project < ActiveRecord::Base
|
|||
has_one :slack_service, dependent: :destroy
|
||||
has_one :buildbox_service, dependent: :destroy
|
||||
has_one :bamboo_service, dependent: :destroy
|
||||
has_one :teamcity_service, dependent: :destroy
|
||||
has_one :pushover_service, dependent: :destroy
|
||||
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
|
||||
has_one :forked_from_project, through: :forked_project_link
|
||||
|
@ -314,7 +315,8 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def available_services_names
|
||||
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack pushover buildbox bamboo)
|
||||
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla
|
||||
emails_on_push gemnasium slack pushover buildbox bamboo teamcity)
|
||||
end
|
||||
|
||||
def gitlab_ci?
|
||||
|
@ -329,11 +331,6 @@ class Project < ActiveRecord::Base
|
|||
@ci_service ||= ci_services.select(&:activated?).first
|
||||
end
|
||||
|
||||
# For compatibility with old code
|
||||
def code
|
||||
path
|
||||
end
|
||||
|
||||
def items_for(entity)
|
||||
case entity
|
||||
when 'issue' then
|
||||
|
|
116
app/models/project_services/teamcity_service.rb
Normal file
116
app/models/project_services/teamcity_service.rb
Normal file
|
@ -0,0 +1,116 @@
|
|||
class TeamcityService < CiService
|
||||
include HTTParty
|
||||
|
||||
prop_accessor :teamcity_url, :build_type, :username, :password
|
||||
|
||||
validates :teamcity_url, presence: true,
|
||||
format: { with: URI::regexp }, if: :activated?
|
||||
validates :build_type, presence: true, if: :activated?
|
||||
validates :username, presence: true,
|
||||
if: ->(service) { service.password? }, if: :activated?
|
||||
validates :password, presence: true,
|
||||
if: ->(service) { service.username? }, if: :activated?
|
||||
|
||||
attr_accessor :response
|
||||
|
||||
after_save :compose_service_hook, if: :activated?
|
||||
|
||||
def compose_service_hook
|
||||
hook = service_hook || build_service_hook
|
||||
hook.save
|
||||
end
|
||||
|
||||
def title
|
||||
'JetBrains TeamCity CI'
|
||||
end
|
||||
|
||||
def description
|
||||
'A continuous integration and build server'
|
||||
end
|
||||
|
||||
def help
|
||||
'The build configuration in Teamcity must use the build format '\
|
||||
'number %build.vcs.number% '\
|
||||
'you will also want to configure monitoring of all branches so merge '\
|
||||
'requests build, that setting is in the vsc root advanced settings.'
|
||||
end
|
||||
|
||||
def to_param
|
||||
'teamcity'
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{ type: 'text', name: 'teamcity_url',
|
||||
placeholder: 'TeamCity root URL like https://teamcity.example.com' },
|
||||
{ type: 'text', name: 'build_type',
|
||||
placeholder: 'Build configuration ID' },
|
||||
{ type: 'text', name: 'username',
|
||||
placeholder: 'A user with permissions to trigger a manual build' },
|
||||
{ type: 'password', name: 'password' },
|
||||
]
|
||||
end
|
||||
|
||||
def build_info(sha)
|
||||
url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\
|
||||
"branch:unspecified:any,number:#{sha}")
|
||||
auth = {
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
@response = HTTParty.get("#{url}", verify: false, basic_auth: auth)
|
||||
end
|
||||
|
||||
def build_page(sha)
|
||||
build_info(sha) if @response.nil? || !@response.code
|
||||
|
||||
if @response.code != 200
|
||||
# If actual build link can't be determined,
|
||||
# send user to build summary page.
|
||||
"#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}"
|
||||
else
|
||||
# If actual build link is available, go to build result page.
|
||||
built_id = @response['build']['id']
|
||||
"#{teamcity_url}/viewLog.html?buildId=#{built_id}"\
|
||||
"&buildTypeId=#{build_type}"
|
||||
end
|
||||
end
|
||||
|
||||
def commit_status(sha)
|
||||
build_info(sha) if @response.nil? || !@response.code
|
||||
return :error unless @response.code == 200 || @response.code == 404
|
||||
|
||||
status = if @response.code == 404
|
||||
'Pending'
|
||||
else
|
||||
@response['build']['status']
|
||||
end
|
||||
|
||||
if status.include?('SUCCESS')
|
||||
'success'
|
||||
elsif status.include?('FAILURE')
|
||||
'failed'
|
||||
elsif status.include?('Pending')
|
||||
'pending'
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def execute(data)
|
||||
auth = {
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
branch = data[:ref]
|
||||
|
||||
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
|
||||
body: "<build branchName=\"#{branch}\">"\
|
||||
"<buildType id=\"#{build_type}\"/>"\
|
||||
'</build>',
|
||||
headers: { 'Content-type' => 'application/xml' },
|
||||
basic_auth: auth
|
||||
)
|
||||
end
|
||||
end
|
|
@ -106,6 +106,7 @@ class User < ActiveRecord::Base
|
|||
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
|
||||
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
|
||||
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
|
||||
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
|
||||
|
||||
|
||||
#
|
||||
|
@ -564,4 +565,8 @@ class User < ActiveRecord::Base
|
|||
namespaces += masters_groups
|
||||
end
|
||||
end
|
||||
|
||||
def oauth_authorized_tokens
|
||||
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
|
||||
end
|
||||
end
|
||||
|
|
41
app/services/oauth2/access_token_validation_service.rb
Normal file
41
app/services/oauth2/access_token_validation_service.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
module Oauth2::AccessTokenValidationService
|
||||
# Results:
|
||||
VALID = :valid
|
||||
EXPIRED = :expired
|
||||
REVOKED = :revoked
|
||||
INSUFFICIENT_SCOPE = :insufficient_scope
|
||||
|
||||
class << self
|
||||
def validate(token, scopes: [])
|
||||
if token.expired?
|
||||
return EXPIRED
|
||||
|
||||
elsif token.revoked?
|
||||
return REVOKED
|
||||
|
||||
elsif !self.sufficent_scope?(token, scopes)
|
||||
return INSUFFICIENT_SCOPE
|
||||
|
||||
else
|
||||
return VALID
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# True if the token's scope is a superset of required scopes,
|
||||
# or the required scopes is empty.
|
||||
def sufficent_scope?(token, scopes)
|
||||
if scopes.blank?
|
||||
# if no any scopes required, the scopes of token is sufficient.
|
||||
return true
|
||||
else
|
||||
# If there are scopes required, then check whether
|
||||
# the set of authorized scopes is a superset of the set of required scopes
|
||||
required_scopes = Set.new(scopes)
|
||||
authorized_scopes = Set.new(token.scopes)
|
||||
|
||||
return authorized_scopes >= required_scopes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -38,17 +38,19 @@
|
|||
= link_to project_path(project), class: dom_class(project) do
|
||||
= project.name_with_namespace
|
||||
|
||||
- if project.forked_from_project
|
||||
|
||||
%small
|
||||
%i.fa.fa-code-fork
|
||||
Forked from:
|
||||
= link_to project.forked_from_project.name_with_namespace, project_path(project.forked_from_project)
|
||||
|
||||
- if current_user.can_leave_project?(project)
|
||||
.pull-right
|
||||
= link_to leave_project_team_members_path(project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do
|
||||
%i.fa.fa-sign-out
|
||||
Leave
|
||||
|
||||
- if project.forked_from_project
|
||||
%small.pull-right
|
||||
%i.fa.fa-code-fork
|
||||
Forked from:
|
||||
= link_to project.forked_from_project.name_with_namespace, project_path(project.forked_from_project)
|
||||
.project-info
|
||||
.pull-right
|
||||
- if project.archived?
|
||||
|
|
4
app/views/doorkeeper/applications/_delete_form.html.haml
Normal file
4
app/views/doorkeeper/applications/_delete_form.html.haml
Normal file
|
@ -0,0 +1,4 @@
|
|||
- submit_btn_css ||= 'btn btn-link btn-remove btn-small'
|
||||
= form_tag oauth_application_path(application) do
|
||||
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
|
24
app/views/doorkeeper/applications/_form.html.haml
Normal file
24
app/views/doorkeeper/applications/_form.html.haml
Normal file
|
@ -0,0 +1,24 @@
|
|||
= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f|
|
||||
- if application.errors.any?
|
||||
.alert.alert-danger{"data-alert" => ""}
|
||||
%p Whoops! Check your form for possible errors
|
||||
= content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do
|
||||
= f.label :name, class: 'col-sm-2 control-label'
|
||||
.col-sm-10
|
||||
= f.text_field :name, class: 'form-control'
|
||||
= doorkeeper_errors_for application, :name
|
||||
= content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do
|
||||
= f.label :redirect_uri, class: 'col-sm-2 control-label'
|
||||
.col-sm-10
|
||||
= f.text_area :redirect_uri, class: 'form-control'
|
||||
= doorkeeper_errors_for application, :redirect_uri
|
||||
%span.help-block
|
||||
Use one line per URI
|
||||
- if Doorkeeper.configuration.native_redirect_uri
|
||||
%span.help-block
|
||||
Use
|
||||
%code= Doorkeeper.configuration.native_redirect_uri
|
||||
for local tests
|
||||
.form-actions
|
||||
= f.submit 'Submit', class: "btn btn-primary wide"
|
||||
= link_to "Cancel", applications_profile_path, class: "btn btn-default"
|
2
app/views/doorkeeper/applications/edit.html.haml
Normal file
2
app/views/doorkeeper/applications/edit.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
%h3.page-title Edit application
|
||||
= render 'form', application: @application
|
16
app/views/doorkeeper/applications/index.html.haml
Normal file
16
app/views/doorkeeper/applications/index.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
%h3.page-title Your applications
|
||||
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td= application.redirect_uri
|
||||
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
|
||||
%td= render 'delete_form', application: application
|
2
app/views/doorkeeper/applications/new.html.haml
Normal file
2
app/views/doorkeeper/applications/new.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
%h3.page-title New application
|
||||
= render 'form', application: @application
|
26
app/views/doorkeeper/applications/show.html.haml
Normal file
26
app/views/doorkeeper/applications/show.html.haml
Normal file
|
@ -0,0 +1,26 @@
|
|||
%h3.page-title
|
||||
Application: #{@application.name}
|
||||
|
||||
|
||||
%table.table
|
||||
%tr
|
||||
%td
|
||||
Application Id
|
||||
%td
|
||||
%code#application_id= @application.uid
|
||||
%tr
|
||||
%td
|
||||
Secret:
|
||||
%td
|
||||
%code#secret= @application.secret
|
||||
|
||||
%tr
|
||||
%td
|
||||
Callback url
|
||||
%td
|
||||
- @application.redirect_uri.split.each do |uri|
|
||||
%div
|
||||
%span.monospace= uri
|
||||
.form-actions
|
||||
= link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
|
||||
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
|
3
app/views/doorkeeper/authorizations/error.html.haml
Normal file
3
app/views/doorkeeper/authorizations/error.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%h3.page-title An error has occurred
|
||||
%main{:role => "main"}
|
||||
%pre= @pre_auth.error_response.body[:error_description]
|
28
app/views/doorkeeper/authorizations/new.html.haml
Normal file
28
app/views/doorkeeper/authorizations/new.html.haml
Normal file
|
@ -0,0 +1,28 @@
|
|||
%h3.page-title Authorize required
|
||||
%main{:role => "main"}
|
||||
%p.h4
|
||||
Authorize
|
||||
%strong.text-info= @pre_auth.client.name
|
||||
to use your account?
|
||||
- if @pre_auth.scopes
|
||||
#oauth-permissions
|
||||
%p This application will be able to:
|
||||
%ul.text-info
|
||||
- @pre_auth.scopes.each do |scope|
|
||||
%li= t scope, scope: [:doorkeeper, :scopes]
|
||||
%hr/
|
||||
.actions
|
||||
= form_tag oauth_authorization_path, method: :post do
|
||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||
= hidden_field_tag :state, @pre_auth.state
|
||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||
= hidden_field_tag :scope, @pre_auth.scope
|
||||
= 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
|
||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||
= hidden_field_tag :state, @pre_auth.state
|
||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||
= hidden_field_tag :scope, @pre_auth.scope
|
||||
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
|
3
app/views/doorkeeper/authorizations/show.html.haml
Normal file
3
app/views/doorkeeper/authorizations/show.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%h3.page-title Authorization code:
|
||||
%main{:role => "main"}
|
||||
%code#authorization_code= params[:code]
|
|
@ -0,0 +1,4 @@
|
|||
- submit_btn_css ||= 'btn btn-link btn-remove'
|
||||
= form_tag oauth_authorized_application_path(application) do
|
||||
%input{:name => "_method", :type => "hidden", :value => "delete"}/
|
||||
= submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-small'
|
16
app/views/doorkeeper/authorized_applications/index.html.haml
Normal file
16
app/views/doorkeeper/authorized_applications/index.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
%header.page-header
|
||||
%h1 Your authorized applications
|
||||
%main{:role => "main"}
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Application
|
||||
%th Created At
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr
|
||||
%td= application.name
|
||||
%td= application.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
%td= render 'delete_form', application: application
|
|
@ -1,4 +1,9 @@
|
|||
%h3.page-title
|
||||
%h4.page-title
|
||||
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
|
||||
- if @group_milestone.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
Milestone #{@group_milestone.title}
|
||||
.pull-right
|
||||
- if can?(current_user, :manage_group, @group)
|
||||
|
@ -7,46 +12,41 @@
|
|||
- else
|
||||
= link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped btn-reopen"
|
||||
|
||||
%hr
|
||||
- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
|
||||
.alert.alert-success
|
||||
%span All issues for this milestone are closed. You may close the milestone now.
|
||||
|
||||
.back-link
|
||||
= link_to group_milestones_path(@group) do
|
||||
← To milestones list
|
||||
|
||||
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
|
||||
.state.clearfix
|
||||
.state-label
|
||||
- if @group_milestone.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
|
||||
%h4.title
|
||||
= gfm escape_once(@group_milestone.title)
|
||||
|
||||
.description
|
||||
- @group_milestone.milestones.each do |milestone|
|
||||
%hr
|
||||
%h4
|
||||
= link_to "#{milestone.project.name} - #{milestone.title}", project_milestone_path(milestone.project, milestone)
|
||||
%span.pull-right= milestone.expires_at
|
||||
.description
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th Project
|
||||
%th Open issues
|
||||
%th State
|
||||
%th Due date
|
||||
- @group_milestone.milestones.each do |milestone|
|
||||
%tr
|
||||
%td
|
||||
= link_to "#{milestone.project.name}", project_milestone_path(milestone.project, milestone)
|
||||
%td
|
||||
= milestone.issues.opened.count
|
||||
%td
|
||||
- if milestone.closed?
|
||||
%span.label.label-danger #{milestone.state}
|
||||
= preserve do
|
||||
- if milestone.description.present?
|
||||
= milestone.description
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
%td
|
||||
= milestone.expires_at
|
||||
|
||||
.context
|
||||
%p
|
||||
Progress:
|
||||
#{@group_milestone.closed_items_count} closed
|
||||
–
|
||||
#{@group_milestone.open_items_count} open
|
||||
|
||||
.progress.progress-info
|
||||
.progress-bar{style: "width: #{@group_milestone.percent_complete}%;"}
|
||||
.context
|
||||
%p.lead
|
||||
Progress:
|
||||
#{@group_milestone.closed_items_count} closed
|
||||
–
|
||||
#{@group_milestone.open_items_count} open
|
||||
.progress.progress-info
|
||||
.progress-bar{style: "width: #{@group_milestone.percent_complete}%;"}
|
||||
|
||||
%ul.nav.nav-tabs
|
||||
%li.active
|
||||
|
|
22
app/views/layouts/doorkeeper/admin.html.haml
Normal file
22
app/views/layouts/doorkeeper/admin.html.haml
Normal file
|
@ -0,0 +1,22 @@
|
|||
!!!
|
||||
%html
|
||||
%head
|
||||
%meta{:charset => "utf-8"}
|
||||
%meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}
|
||||
%meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
|
||||
%title Doorkeeper
|
||||
= stylesheet_link_tag "doorkeeper/admin/application"
|
||||
= csrf_meta_tags
|
||||
%body
|
||||
.navbar.navbar-inverse.navbar-fixed-top{:role => "navigation"}
|
||||
.container
|
||||
.navbar-header
|
||||
= link_to 'OAuth2 Provider', oauth_applications_path, class: 'navbar-brand'
|
||||
%ul.nav.navbar-nav
|
||||
= content_tag :li, class: "#{'active' if request.path == oauth_applications_path}" do
|
||||
= link_to 'Applications', oauth_applications_path
|
||||
.container
|
||||
- if flash[:notice].present?
|
||||
.alert.alert-info
|
||||
= flash[:notice]
|
||||
= yield
|
15
app/views/layouts/doorkeeper/application.html.haml
Normal file
15
app/views/layouts/doorkeeper/application.html.haml
Normal file
|
@ -0,0 +1,15 @@
|
|||
!!!
|
||||
%html
|
||||
%head
|
||||
%title OAuth authorize required
|
||||
%meta{:charset => "utf-8"}
|
||||
%meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}
|
||||
%meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
|
||||
= stylesheet_link_tag "doorkeeper/application"
|
||||
= csrf_meta_tags
|
||||
%body
|
||||
#container
|
||||
- if flash[:notice].present?
|
||||
.alert.alert-info
|
||||
= flash[:notice]
|
||||
= yield
|
|
@ -10,13 +10,13 @@
|
|||
%span
|
||||
Projects
|
||||
= nav_link(path: 'dashboard#issues') do
|
||||
= link_to issues_dashboard_path, class: 'shortcuts-issues' do
|
||||
= link_to assigned_issues_dashboard_path, class: 'shortcuts-issues' do
|
||||
%i.fa.fa-exclamation-circle
|
||||
%span
|
||||
Issues
|
||||
%span.count= current_user.assigned_issues.opened.count
|
||||
= nav_link(path: 'dashboard#merge_requests') do
|
||||
= link_to merge_requests_dashboard_path, class: 'shortcuts-merge_requests' do
|
||||
= link_to assigned_mrs_dashboard_path, class: 'shortcuts-merge_requests' do
|
||||
%i.fa.fa-tasks
|
||||
%span
|
||||
Merge Requests
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
= link_to profile_account_path do
|
||||
%i.fa.fa-gear
|
||||
Account
|
||||
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do
|
||||
= link_to applications_profile_path do
|
||||
%i.fa.fa-cloud
|
||||
%span
|
||||
Applications
|
||||
= nav_link(controller: :emails) do
|
||||
= link_to profile_emails_path do
|
||||
%i.fa.fa-envelope-o
|
||||
|
|
|
@ -5,8 +5,5 @@
|
|||
= render "layouts/broadcast"
|
||||
= render "layouts/head_panel", title: project_title(@project)
|
||||
= render "layouts/init_auto_complete"
|
||||
- if can?(current_user, :download_code, @project)
|
||||
= render 'shared/no_ssh'
|
||||
|
||||
- @project_settings_nav = true
|
||||
= render 'layouts/page', sidebar: 'layouts/nav/project'
|
||||
|
|
|
@ -5,6 +5,4 @@
|
|||
= render "layouts/broadcast"
|
||||
= render "layouts/head_panel", title: project_title(@project)
|
||||
= render "layouts/init_auto_complete"
|
||||
- if can?(current_user, :download_code, @project)
|
||||
= render 'shared/no_ssh'
|
||||
= render 'layouts/page', sidebar: 'layouts/nav/project'
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
- @commits.each do |commit|
|
||||
%li
|
||||
%strong #{link_to commit.short_id, project_commit_url(@project, commit)}
|
||||
%span by #{commit.author_name}
|
||||
%div
|
||||
%span by #{commit.author_name}
|
||||
%i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
|
||||
%pre #{commit.safe_message}
|
||||
|
||||
%h4 Changes:
|
||||
|
|
|
@ -75,3 +75,4 @@
|
|||
The following groups will be abandoned. You should transfer or remove them:
|
||||
%strong #{current_user.solo_owned_groups.map(&:name).join(', ')}
|
||||
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
|
||||
|
||||
|
|
47
app/views/profiles/applications.html.haml
Normal file
47
app/views/profiles/applications.html.haml
Normal file
|
@ -0,0 +1,47 @@
|
|||
%h3.page-title
|
||||
OAuth2
|
||||
|
||||
%fieldset.oauth-applications
|
||||
%legend Your applications
|
||||
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
|
||||
- if @applications.any?
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Callback URL
|
||||
%th Clients
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
- @applications.each do |application|
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= link_to application.name, oauth_application_path(application)
|
||||
%td
|
||||
- application.redirect_uri.split.each do |uri|
|
||||
%div= uri
|
||||
%td= application.access_tokens.count
|
||||
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-small'
|
||||
%td= render 'doorkeeper/applications/delete_form', application: application
|
||||
|
||||
%fieldset.oauth-authorized-applications.prepend-top-20
|
||||
%legend Authorized applications
|
||||
|
||||
- if @authorized_tokens.any?
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Name
|
||||
%th Authorized At
|
||||
%th Scope
|
||||
%th
|
||||
%tbody
|
||||
- @authorized_tokens.each do |token|
|
||||
- application = token.application
|
||||
%tr{:id => "application_#{application.id}"}
|
||||
%td= application.name
|
||||
%td= token.created_at
|
||||
%td= token.scopes
|
||||
%td= render 'doorkeeper/authorized_applications/delete_form', application: application
|
||||
- else
|
||||
%p.light You dont have any authorized applications
|
|
@ -2,15 +2,9 @@
|
|||
.col-md-8
|
||||
= render 'projects/diffs/stats', diffs: diffs
|
||||
.col-md-4
|
||||
%ul.nav.nav-tabs
|
||||
%li.pull-right{class: params[:view] == 'parallel' ? 'active' : ''}
|
||||
- params_copy = params.dup
|
||||
- params_copy[:view] = 'parallel'
|
||||
= link_to "Side-by-side Diff", url_for(params_copy), {id: "commit-diff-viewtype"}
|
||||
%li.pull-right{class: params[:view] != 'parallel' ? 'active' : ''}
|
||||
- params_copy[:view] = 'inline'
|
||||
= link_to "Inline Diff", url_for(params_copy), {id: "commit-diff-viewtype"}
|
||||
|
||||
.btn-group.pull-right
|
||||
= inline_diff_btn
|
||||
= parallel_diff_btn
|
||||
|
||||
- if show_diff_size_warning?(diffs)
|
||||
= render 'projects/diffs/warning', diffs: diffs
|
||||
|
|
37
app/views/projects/issues/_discussion.html.haml
Normal file
37
app/views/projects/issues/_discussion.html.haml
Normal file
|
@ -0,0 +1,37 @@
|
|||
- content_for :note_actions do
|
||||
- if can?(current_user, :modify_issue, @issue)
|
||||
- if @issue.closed?
|
||||
= link_to 'Reopen Issue', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue'
|
||||
- else
|
||||
= link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue"
|
||||
.row
|
||||
.col-md-9
|
||||
.participants
|
||||
%cite.cgray
|
||||
= pluralize(@issue.participants.count, 'participant')
|
||||
- @issue.participants.each do |participant|
|
||||
= link_to_member(@project, participant, name: false, size: 24)
|
||||
|
||||
.voting_notes#notes= render "projects/notes/notes_with_form"
|
||||
.col-md-3.hidden-sm.hidden-xs
|
||||
%div
|
||||
.clearfix
|
||||
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @issue)
|
||||
%hr
|
||||
.clearfix
|
||||
.votes-holder
|
||||
%h6 Votes
|
||||
#votes= render 'votes/votes_block', votable: @issue
|
||||
%hr
|
||||
.context
|
||||
%cite.cgray
|
||||
= render partial: 'issue_context', locals: { issue: @issue }
|
||||
|
||||
- if @issue.labels.any?
|
||||
%hr
|
||||
%h6 Labels
|
||||
.issue-show-labels
|
||||
- @issue.labels.each do |label|
|
||||
= link_to project_issues_path(@project, label_name: label.name) do
|
||||
%p= render_colored_label(label)
|
|
@ -1,65 +1,37 @@
|
|||
%h3.page-title
|
||||
%h4.page-title
|
||||
.issue-box{ class: issue_box_class(@issue) }
|
||||
- if @issue.closed?
|
||||
Closed
|
||||
- else
|
||||
Open
|
||||
Issue ##{@issue.iid}
|
||||
.pull-right.creator
|
||||
%small Created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)}
|
||||
%small.creator
|
||||
· created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)}
|
||||
|
||||
.pull-right
|
||||
- if can?(current_user, :write_issue, @project)
|
||||
= link_to new_project_issue_path(@project), class: "btn btn-grouped", title: "New Issue", id: "new_issue_link" do
|
||||
%i.fa.fa-plus
|
||||
New Issue
|
||||
- if can?(current_user, :modify_issue, @issue)
|
||||
- if @issue.closed?
|
||||
= link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen"
|
||||
- else
|
||||
= link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue"
|
||||
|
||||
= link_to edit_project_issue_path(@project, @issue), class: "btn btn-grouped issuable-edit" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
|
||||
%hr
|
||||
.row
|
||||
.col-sm-9
|
||||
%h3.issue-title
|
||||
= gfm escape_once(@issue.title)
|
||||
%div
|
||||
- if @issue.description.present?
|
||||
.description
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown(@issue.description, parse_tasks: true)
|
||||
%hr
|
||||
- content_for :note_actions do
|
||||
- if can?(current_user, :modify_issue, @issue)
|
||||
- if @issue.closed?
|
||||
= link_to 'Reopen Issue', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue'
|
||||
- else
|
||||
= link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue"
|
||||
.participants
|
||||
%cite.cgray
|
||||
= pluralize(@issue.participants.count, 'participant')
|
||||
- @issue.participants.each do |participant|
|
||||
= link_to_member(@project, participant, name: false, size: 24)
|
||||
.issue-show-labels.pull-right
|
||||
- @issue.labels.each do |label|
|
||||
= link_to project_issues_path(@project, label_name: label.name) do
|
||||
= render_colored_label(label)
|
||||
%h3.issue-title
|
||||
= gfm escape_once(@issue.title)
|
||||
%div
|
||||
- if @issue.description.present?
|
||||
.description
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown(@issue.description, parse_tasks: true)
|
||||
|
||||
.voting_notes#notes= render "projects/notes/notes_with_form"
|
||||
.col-sm-3
|
||||
%div
|
||||
- if can?(current_user, :write_issue, @project)
|
||||
= link_to new_project_issue_path(@project), class: "btn btn-block", title: "New Issue", id: "new_issue_link" do
|
||||
%i.fa.fa-plus
|
||||
New Issue
|
||||
- if can?(current_user, :modify_issue, @issue)
|
||||
- if @issue.closed?
|
||||
= link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-block btn-reopen"
|
||||
- else
|
||||
= link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-block btn-close", title: "Close Issue"
|
||||
|
||||
= link_to edit_project_issue_path(@project, @issue), class: "btn btn-block issuable-edit" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
.clearfix
|
||||
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @issue)
|
||||
%hr
|
||||
.clearfix
|
||||
.votes-holder
|
||||
%h6 Votes
|
||||
#votes= render 'votes/votes_block', votable: @issue
|
||||
%hr
|
||||
.context
|
||||
%cite.cgray
|
||||
= render partial: 'issue_context', locals: { issue: @issue }
|
||||
%hr
|
||||
= render "projects/issues/discussion"
|
||||
|
|
31
app/views/projects/merge_requests/_discussion.html.haml
Normal file
31
app/views/projects/merge_requests/_discussion.html.haml
Normal file
|
@ -0,0 +1,31 @@
|
|||
- content_for :note_actions do
|
||||
- if can?(current_user, :modify_merge_request, @merge_request)
|
||||
- if @merge_request.open?
|
||||
= link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
|
||||
- if @merge_request.closed?
|
||||
= link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
|
||||
|
||||
.row
|
||||
.col-md-9
|
||||
= render "projects/merge_requests/show/participants"
|
||||
= render "projects/notes/notes_with_form"
|
||||
.col-md-3.hidden-sm.hidden-xs
|
||||
.clearfix
|
||||
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @merge_request)
|
||||
%hr
|
||||
.votes-holder.hidden-sm.hidden-xs
|
||||
%h6 Votes
|
||||
#votes= render 'votes/votes_block', votable: @merge_request
|
||||
%hr
|
||||
.context
|
||||
%cite.cgray
|
||||
= render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request }
|
||||
|
||||
- if @merge_request.labels.any?
|
||||
%hr
|
||||
%h6 Labels
|
||||
.merge-request-show-labels
|
||||
- @merge_request.labels.each do |label|
|
||||
= link_to project_merge_requests_path(@project, label_name: label.name) do
|
||||
%p= render_colored_label(label)
|
|
@ -1,89 +1,64 @@
|
|||
.merge-request
|
||||
= render "projects/merge_requests/show/mr_title"
|
||||
%hr
|
||||
.row
|
||||
.col-sm-9
|
||||
= render "projects/merge_requests/show/how_to_merge"
|
||||
= render "projects/merge_requests/show/mr_box"
|
||||
%hr
|
||||
.append-bottom-20
|
||||
%p.slead
|
||||
%span From
|
||||
- if @merge_request.for_fork?
|
||||
%strong.label-branch<
|
||||
- if @merge_request.source_project
|
||||
= link_to @merge_request.source_project_namespace, project_path(@merge_request.source_project)
|
||||
- else
|
||||
\ #{@merge_request.source_project_namespace}
|
||||
\:#{@merge_request.source_branch}
|
||||
%span into
|
||||
%strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
|
||||
= render "projects/merge_requests/show/mr_box"
|
||||
%hr
|
||||
.append-bottom-20
|
||||
.slead
|
||||
%span From
|
||||
- if @merge_request.for_fork?
|
||||
%strong.label-branch<
|
||||
- if @merge_request.source_project
|
||||
= link_to @merge_request.source_project_namespace, project_path(@merge_request.source_project)
|
||||
- else
|
||||
%strong.label-branch #{@merge_request.source_branch}
|
||||
%span into
|
||||
%strong.label-branch #{@merge_request.target_branch}
|
||||
= render "projects/merge_requests/show/state_widget"
|
||||
= render "projects/merge_requests/show/commits"
|
||||
= render "projects/merge_requests/show/participants"
|
||||
\ #{@merge_request.source_project_namespace}
|
||||
\:#{@merge_request.source_branch}
|
||||
%span into
|
||||
%strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
|
||||
- else
|
||||
%strong.label-branch #{@merge_request.source_branch}
|
||||
%span into
|
||||
%strong.label-branch #{@merge_request.target_branch}
|
||||
- if @merge_request.open?
|
||||
%span.pull-right
|
||||
.btn-group
|
||||
%a.btn.dropdown-toggle{ data: {toggle: :dropdown} }
|
||||
%i.fa.fa-download
|
||||
Download as
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
%li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch)
|
||||
%li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff)
|
||||
|
||||
.col-sm-3
|
||||
.issue-btn-group
|
||||
- if can?(current_user, :modify_merge_request, @merge_request)
|
||||
- if @merge_request.open?
|
||||
.btn-group-justified.append-bottom-20
|
||||
.btn-group
|
||||
%a.btn.dropdown-toggle{ data: {toggle: :dropdown} }
|
||||
%i.fa.fa-download
|
||||
Download as
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
%li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch)
|
||||
%li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff)
|
||||
= link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-block btn-close", title: "Close merge request"
|
||||
= link_to edit_project_merge_request_path(@project, @merge_request), class: "btn btn-block issuable-edit", id: "edit_merge_request" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
- if @merge_request.closed?
|
||||
= link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-block btn-reopen reopen-mr-link", title: "Close merge request"
|
||||
.clearfix
|
||||
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
|
||||
= cross_project_reference(@project, @merge_request)
|
||||
%hr
|
||||
.votes-holder.hidden-sm.hidden-xs
|
||||
%h6 Votes
|
||||
#votes= render 'votes/votes_block', votable: @merge_request
|
||||
%hr
|
||||
.context
|
||||
%cite.cgray
|
||||
= render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request }
|
||||
= render "projects/merge_requests/show/how_to_merge"
|
||||
= render "projects/merge_requests/show/state_widget"
|
||||
|
||||
- if @commits.present?
|
||||
%ul.nav.nav-tabs.merge-request-tabs
|
||||
%li.notes-tab{data: {action: 'notes'}}
|
||||
= link_to project_merge_request_path(@project, @merge_request) do
|
||||
%i.fa.fa-comment
|
||||
%i.fa.fa-comments
|
||||
Discussion
|
||||
%span.badge= @merge_request.mr_and_commit_notes.count
|
||||
%li.commits-tab{data: {action: 'commits'}}
|
||||
= link_to project_merge_request_path(@project, @merge_request), title: 'Commits' do
|
||||
%i.fa.fa-database
|
||||
Commits
|
||||
%span.badge= @commits.size
|
||||
%li.diffs-tab{data: {action: 'diffs'}}
|
||||
= link_to diffs_project_merge_request_path(@project, @merge_request) do
|
||||
%i.fa.fa-list-alt
|
||||
Changes
|
||||
%span.badge= @merge_request.diffs.size
|
||||
|
||||
- content_for :note_actions do
|
||||
- if can?(current_user, :modify_merge_request, @merge_request)
|
||||
- if @merge_request.open?
|
||||
= link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
|
||||
- if @merge_request.closed?
|
||||
= link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
|
||||
|
||||
.notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" }
|
||||
= render "projects/merge_requests/discussion"
|
||||
.commits.tab-content
|
||||
= render "projects/merge_requests/show/commits"
|
||||
.diffs.tab-content
|
||||
- if current_page?(action: 'diffs')
|
||||
= render "projects/merge_requests/show/diffs"
|
||||
.notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" }
|
||||
.row
|
||||
.col-sm-9
|
||||
= render "projects/notes/notes_with_form"
|
||||
|
||||
.mr-loading-status
|
||||
= spinner
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%h3.page-title
|
||||
%h4.page-title
|
||||
.issue-box{ class: issue_box_class(@merge_request) }
|
||||
- if @merge_request.merged?
|
||||
Merged
|
||||
|
@ -7,5 +7,16 @@
|
|||
- else
|
||||
Open
|
||||
= "Merge Request ##{@merge_request.iid}"
|
||||
.pull-right.creator
|
||||
%small Created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)}
|
||||
%small.creator
|
||||
·
|
||||
created by #{link_to_member(@project, @merge_request.author)} #{time_ago_with_tooltip(@merge_request.created_at)}
|
||||
|
||||
.issue-btn-group.pull-right
|
||||
- if can?(current_user, :modify_merge_request, @merge_request)
|
||||
- if @merge_request.open?
|
||||
= link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request"
|
||||
= link_to edit_project_merge_request_path(@project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
- if @merge_request.closed?
|
||||
= link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request"
|
||||
|
|
|
@ -2,8 +2,3 @@
|
|||
%cite.cgray #{@merge_request.participants.count} participants
|
||||
- @merge_request.participants.each do |participant|
|
||||
= link_to_member(@project, participant, name: false, size: 24)
|
||||
|
||||
.merge-request-show-labels.pull-right
|
||||
- @merge_request.labels.each do |label|
|
||||
= link_to project_merge_requests_path(@project, label_name: label.name) do
|
||||
= render_colored_label(label)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= render "projects/issues_nav"
|
||||
%h3.page-title
|
||||
%h4.page-title
|
||||
.issue-box{ class: issue_box_class(@milestone) }
|
||||
- if @milestone.closed?
|
||||
Closed
|
||||
|
@ -8,52 +8,44 @@
|
|||
- else
|
||||
Open
|
||||
Milestone ##{@milestone.iid}
|
||||
.pull-right.creator
|
||||
%small= @milestone.expires_at
|
||||
%small.creator
|
||||
= @milestone.expires_at
|
||||
.pull-right
|
||||
- if can?(current_user, :admin_milestone, @project)
|
||||
= link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
- if @milestone.active?
|
||||
= link_to 'Close Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
|
||||
- else
|
||||
= link_to 'Reopen Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
|
||||
|
||||
%hr
|
||||
- if @milestone.issues.any? && @milestone.can_be_closed?
|
||||
.alert.alert-success
|
||||
%span All issues for this milestone are closed. You may close milestone now.
|
||||
.row
|
||||
.col-sm-9
|
||||
%h3.issue-title
|
||||
= gfm escape_once(@milestone.title)
|
||||
%div
|
||||
- if @milestone.description.present?
|
||||
.description
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown @milestone.description
|
||||
|
||||
%hr
|
||||
.context
|
||||
%p.lead
|
||||
Progress:
|
||||
#{@milestone.closed_items_count} closed
|
||||
–
|
||||
#{@milestone.open_items_count} open
|
||||
|
||||
%span.light #{@milestone.percent_complete}% complete
|
||||
%span.pull-right= @milestone.expires_at
|
||||
.progress.progress-info
|
||||
.progress-bar{style: "width: #{@milestone.percent_complete}%;"}
|
||||
|
||||
.col-sm-3
|
||||
%div
|
||||
- if can?(current_user, :admin_milestone, @project)
|
||||
= link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-block" do
|
||||
%i.fa.fa-pencil-square-o
|
||||
Edit
|
||||
- if @milestone.active?
|
||||
= link_to 'Close Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-block"
|
||||
- else
|
||||
= link_to 'Reopen Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-block"
|
||||
= link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn btn-block", title: "New Issue" do
|
||||
%i.fa.fa-plus
|
||||
New Issue
|
||||
= link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-block"
|
||||
%h3.issue-title
|
||||
= gfm escape_once(@milestone.title)
|
||||
%div
|
||||
- if @milestone.description.present?
|
||||
.description
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown @milestone.description
|
||||
|
||||
%hr
|
||||
.context
|
||||
%p.lead
|
||||
Progress:
|
||||
#{@milestone.closed_items_count} closed
|
||||
–
|
||||
#{@milestone.open_items_count} open
|
||||
|
||||
%span.light #{@milestone.percent_complete}% complete
|
||||
%span.pull-right= @milestone.expires_at
|
||||
.progress.progress-info
|
||||
.progress-bar{style: "width: #{@milestone.percent_complete}%;"}
|
||||
|
||||
|
||||
%ul.nav.nav-tabs
|
||||
|
@ -71,6 +63,10 @@
|
|||
%span.badge= @users.count
|
||||
|
||||
.pull-right
|
||||
= link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
|
||||
%i.fa.fa-plus
|
||||
New Issue
|
||||
= link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped"
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active#tab-issues
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
- if current_user && can?(current_user, :download_code, @project)
|
||||
= render 'shared/no_ssh'
|
||||
|
||||
= render "home_panel"
|
||||
|
||||
- readme = @repository.readme
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
- if cookies[:hide_no_ssh_message].blank? && current_user.require_ssh_key? && !current_user.hide_no_ssh_key
|
||||
.no-ssh-key-message
|
||||
.container
|
||||
You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile
|
||||
.pull-right.hidden-xs
|
||||
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'hide-no-ssh-message', remote: true
|
||||
|
|
||||
= link_to 'Remind later', '#', class: 'hide-no-ssh-message'
|
||||
.links-xs.visible-xs
|
||||
= link_to "Add key", new_profile_key_path
|
||||
|
|
||||
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'hide-no-ssh-message', remote: true
|
||||
|
|
||||
= link_to 'Later', '#', class: 'hide-no-ssh-message'
|
||||
.no-ssh-key-message.alert.alert-warning.hidden-xs
|
||||
You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile
|
||||
|
||||
.pull-right
|
||||
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put
|
||||
|
|
||||
= link_to 'Remind later', '#', class: 'hide-no-ssh-message'
|
||||
|
|
91
config/initializers/doorkeeper.rb
Normal file
91
config/initializers/doorkeeper.rb
Normal file
|
@ -0,0 +1,91 @@
|
|||
Doorkeeper.configure do
|
||||
# Change the ORM that doorkeeper will use.
|
||||
# Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper
|
||||
orm :active_record
|
||||
|
||||
# This block will be called to check whether the resource owner is authenticated or not.
|
||||
resource_owner_authenticator do
|
||||
# Put your resource owner authentication logic here.
|
||||
# Example implementation:
|
||||
current_user || redirect_to(new_user_session_url)
|
||||
end
|
||||
|
||||
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
|
||||
# admin_authenticator do
|
||||
# # Put your admin authentication logic here.
|
||||
# # Example implementation:
|
||||
# Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url)
|
||||
# end
|
||||
|
||||
# Authorization Code expiration time (default 10 minutes).
|
||||
# authorization_code_expires_in 10.minutes
|
||||
|
||||
# Access token expiration time (default 2 hours).
|
||||
# If you want to disable expiration, set this to nil.
|
||||
# access_token_expires_in 2.hours
|
||||
|
||||
# Reuse access token for the same resource owner within an application (disabled by default)
|
||||
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
|
||||
# reuse_access_token
|
||||
|
||||
# Issue access tokens with refresh token (disabled by default)
|
||||
use_refresh_token
|
||||
|
||||
# Provide support for an owner to be assigned to each registered application (disabled by default)
|
||||
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
|
||||
# a registered application
|
||||
# Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support
|
||||
enable_application_owner :confirmation => true
|
||||
|
||||
# Define access token scopes for your provider
|
||||
# For more information go to
|
||||
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
|
||||
default_scopes :api
|
||||
#optional_scopes :write, :update
|
||||
|
||||
# Change the way client credentials are retrieved from the request object.
|
||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||
# falls back to the `:client_id` and `:client_secret` params from the `params` object.
|
||||
# Check out the wiki for more information on customization
|
||||
# client_credentials :from_basic, :from_params
|
||||
|
||||
# Change the way access token is authenticated from the request object.
|
||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||
# falls back to the `:access_token` or `:bearer_token` params from the `params` object.
|
||||
# Check out the wiki for more information on customization
|
||||
access_token_methods :from_access_token_param, :from_bearer_authorization, :from_bearer_param
|
||||
|
||||
# Change the native redirect uri for client apps
|
||||
# When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider
|
||||
# The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL
|
||||
# (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
|
||||
#
|
||||
native_redirect_uri nil#'urn:ietf:wg:oauth:2.0:oob'
|
||||
|
||||
# Specify what grant flows are enabled in array of Strings. The valid
|
||||
# strings and the flows they enable are:
|
||||
#
|
||||
# "authorization_code" => Authorization Code Grant Flow
|
||||
# "implicit" => Implicit Grant Flow
|
||||
# "password" => Resource Owner Password Credentials Grant Flow
|
||||
# "client_credentials" => Client Credentials Grant Flow
|
||||
#
|
||||
# If not specified, Doorkeeper enables all the four grant flows.
|
||||
#
|
||||
# grant_flows %w(authorization_code implicit password client_credentials)
|
||||
|
||||
# Under some circumstances you might want to have applications auto-approved,
|
||||
# so that the user skips the authorization step.
|
||||
# For example if dealing with trusted a application.
|
||||
# skip_authorization do |resource_owner, client|
|
||||
# client.superapp? or resource_owner.admin?
|
||||
# end
|
||||
|
||||
# WWW-Authenticate Realm (default "Doorkeeper").
|
||||
# realm "Doorkeeper"
|
||||
|
||||
# Allow dynamic query parameters (disabled by default)
|
||||
# Some applications require dynamic query parameters on their request_uri
|
||||
# set to true if you want this to be allowed
|
||||
# wildcard_redirect_uri false
|
||||
end
|
73
config/locales/doorkeeper.en.yml
Normal file
73
config/locales/doorkeeper.en.yml
Normal file
|
@ -0,0 +1,73 @@
|
|||
en:
|
||||
activerecord:
|
||||
errors:
|
||||
models:
|
||||
application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: 'cannot contain a fragment.'
|
||||
invalid_uri: 'must be a valid URI.'
|
||||
relative_uri: 'must be an absolute URI.'
|
||||
mongoid:
|
||||
errors:
|
||||
models:
|
||||
application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: 'cannot contain a fragment.'
|
||||
invalid_uri: 'must be a valid URI.'
|
||||
relative_uri: 'must be an absolute URI.'
|
||||
mongo_mapper:
|
||||
errors:
|
||||
models:
|
||||
application:
|
||||
attributes:
|
||||
redirect_uri:
|
||||
fragment_present: 'cannot contain a fragment.'
|
||||
invalid_uri: 'must be a valid URI.'
|
||||
relative_uri: 'must be an absolute URI.'
|
||||
doorkeeper:
|
||||
errors:
|
||||
messages:
|
||||
# Common error messages
|
||||
invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
|
||||
invalid_redirect_uri: 'The redirect uri included is not valid.'
|
||||
unauthorized_client: 'The client is not authorized to perform this request using this method.'
|
||||
access_denied: 'The resource owner or authorization server denied the request.'
|
||||
invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
|
||||
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
|
||||
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
|
||||
|
||||
#configuration error messages
|
||||
credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
|
||||
resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.'
|
||||
|
||||
# Access grant errors
|
||||
unsupported_response_type: 'The authorization server does not support this response type.'
|
||||
|
||||
# Access token errors
|
||||
invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
|
||||
invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
|
||||
unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'
|
||||
|
||||
# Password Access token errors
|
||||
invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found'
|
||||
|
||||
invalid_token:
|
||||
revoked: "The access token was revoked"
|
||||
expired: "The access token expired"
|
||||
unknown: "The access token is invalid"
|
||||
scopes:
|
||||
api: Access your API
|
||||
|
||||
flash:
|
||||
applications:
|
||||
create:
|
||||
notice: 'Application created.'
|
||||
destroy:
|
||||
notice: 'Application deleted.'
|
||||
update:
|
||||
notice: 'Application updated.'
|
||||
authorized_applications:
|
||||
destroy:
|
||||
notice: 'Application revoked.'
|
|
@ -2,6 +2,11 @@ require 'sidekiq/web'
|
|||
require 'api/api'
|
||||
|
||||
Gitlab::Application.routes.draw do
|
||||
use_doorkeeper do
|
||||
controllers :applications => 'oauth/applications',
|
||||
:authorized_applications => 'oauth/authorized_applications',
|
||||
:authorizations => 'oauth/authorizations'
|
||||
end
|
||||
#
|
||||
# Search
|
||||
#
|
||||
|
@ -113,6 +118,7 @@ Gitlab::Application.routes.draw do
|
|||
member do
|
||||
get :history
|
||||
get :design
|
||||
get :applications
|
||||
|
||||
put :reset_private_token
|
||||
put :update_username
|
||||
|
@ -212,7 +218,8 @@ Gitlab::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/}
|
||||
get '/compare/:from...:to' => 'compare#show', :as => 'compare',
|
||||
:constraints => {from: /.+/, to: /.+/}
|
||||
|
||||
resources :snippets, constraints: {id: /\d+/} do
|
||||
member do
|
||||
|
|
42
db/migrate/20141216155758_create_doorkeeper_tables.rb
Normal file
42
db/migrate/20141216155758_create_doorkeeper_tables.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
class CreateDoorkeeperTables < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :oauth_applications do |t|
|
||||
t.string :name, null: false
|
||||
t.string :uid, null: false
|
||||
t.string :secret, null: false
|
||||
t.text :redirect_uri, null: false
|
||||
t.string :scopes, null: false, default: ''
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :oauth_applications, :uid, unique: true
|
||||
|
||||
create_table :oauth_access_grants do |t|
|
||||
t.integer :resource_owner_id, null: false
|
||||
t.integer :application_id, null: false
|
||||
t.string :token, null: false
|
||||
t.integer :expires_in, null: false
|
||||
t.text :redirect_uri, null: false
|
||||
t.datetime :created_at, null: false
|
||||
t.datetime :revoked_at
|
||||
t.string :scopes
|
||||
end
|
||||
|
||||
add_index :oauth_access_grants, :token, unique: true
|
||||
|
||||
create_table :oauth_access_tokens do |t|
|
||||
t.integer :resource_owner_id
|
||||
t.integer :application_id
|
||||
t.string :token, null: false
|
||||
t.string :refresh_token
|
||||
t.integer :expires_in
|
||||
t.datetime :revoked_at
|
||||
t.datetime :created_at, null: false
|
||||
t.string :scopes
|
||||
end
|
||||
|
||||
add_index :oauth_access_tokens, :token, unique: true
|
||||
add_index :oauth_access_tokens, :resource_owner_id
|
||||
add_index :oauth_access_tokens, :refresh_token, unique: true
|
||||
end
|
||||
end
|
7
db/migrate/20141217125223_add_owner_to_application.rb
Normal file
7
db/migrate/20141217125223_add_owner_to_application.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class AddOwnerToApplication < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :oauth_applications, :owner_id, :integer, null: true
|
||||
add_column :oauth_applications, :owner_type, :string, null: true
|
||||
add_index :oauth_applications, [:owner_id, :owner_type]
|
||||
end
|
||||
end
|
43
db/schema.rb
43
db/schema.rb
|
@ -249,6 +249,49 @@ ActiveRecord::Schema.define(version: 20141226080412) do
|
|||
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
|
||||
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
|
||||
|
||||
create_table "oauth_access_grants", force: true do |t|
|
||||
t.integer "resource_owner_id", null: false
|
||||
t.integer "application_id", null: false
|
||||
t.string "token", null: false
|
||||
t.integer "expires_in", null: false
|
||||
t.text "redirect_uri", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "revoked_at"
|
||||
t.string "scopes"
|
||||
end
|
||||
|
||||
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
|
||||
|
||||
create_table "oauth_access_tokens", force: true do |t|
|
||||
t.integer "resource_owner_id"
|
||||
t.integer "application_id"
|
||||
t.string "token", null: false
|
||||
t.string "refresh_token"
|
||||
t.integer "expires_in"
|
||||
t.datetime "revoked_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.string "scopes"
|
||||
end
|
||||
|
||||
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
|
||||
add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
|
||||
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
|
||||
|
||||
create_table "oauth_applications", force: true do |t|
|
||||
t.string "name", null: false
|
||||
t.string "uid", null: false
|
||||
t.string "secret", null: false
|
||||
t.text "redirect_uri", null: false
|
||||
t.string "scopes", default: "", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "owner_id"
|
||||
t.string "owner_type"
|
||||
end
|
||||
|
||||
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
|
||||
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
|
||||
|
||||
create_table "projects", force: true do |t|
|
||||
t.string "name"
|
||||
t.string "path"
|
||||
|
|
|
@ -16,3 +16,4 @@ __Project integrations with external services for continuous integration and mor
|
|||
- PivotalTracker
|
||||
- Pushover
|
||||
- Slack
|
||||
- TeamCity
|
|
@ -1,3 +1,5 @@
|
|||
# Workflow
|
||||
|
||||
- [Workflow](workflow.md)
|
||||
- [Project Features](project_features.md)
|
||||
- [Authorization for merge requests](authorization_for_merge_requests.md)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
![GitLab Flow](gitlab_flow.png)
|
||||
|
||||
# Introduction
|
||||
## Introduction
|
||||
|
||||
Version management with git makes branching and merging much easier than older versioning systems such as SVN.
|
||||
This allows a wide variety of branching strategies and workflows.
|
||||
|
@ -29,9 +29,9 @@ People have a hard time figuring out which branch they should develop on or depl
|
|||
Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html)
|
||||
We think there is still room for improvement and will detail a set of practices we call GitLab flow.
|
||||
|
||||
# Git flow and its problems
|
||||
## Git flow and its problems
|
||||
|
||||
[![Git Flow timeline by Vincent Driessen, used with persmission](gitdashflow.png)
|
||||
[![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png)
|
||||
|
||||
Git flow was one of the first proposals to use git branches and it has gotten a lot of attention.
|
||||
It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes.
|
||||
|
@ -50,7 +50,7 @@ Frequently developers make a mistake and for example changes are only merged int
|
|||
The root cause of these errors is that git flow is too complex for most of the use cases.
|
||||
And doing releases doesn't automatically mean also doing hotfixes.
|
||||
|
||||
# GitHub flow as a simpler alternative
|
||||
## GitHub flow as a simpler alternative
|
||||
|
||||
![Master branch with feature branches merged in](github_flow.png)
|
||||
|
||||
|
@ -62,13 +62,13 @@ Merging everything into the master branch and deploying often means you minimize
|
|||
But this flow still leaves a lot of questions unanswered regarding deployments, environments, releases and integrations with issues.
|
||||
With GitLab flow we offer additional guidance for these questions.
|
||||
|
||||
# Production branch with GitLab flow
|
||||
## Production branch with GitLab flow
|
||||
|
||||
![Master branch and production branch with arrow that indicate deployments](production_branch.png)
|
||||
|
||||
GitHub flow does assume you are able to deploy to production every time you merge a feature branch.
|
||||
This is possible for SaaS applications but are many cases where this is not possible.
|
||||
One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass AppStore validation.
|
||||
One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation.
|
||||
Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times.
|
||||
In these cases you can make a production branch that reflects the deployed code.
|
||||
You can deploy a new version by merging in master to the production branch.
|
||||
|
@ -78,7 +78,7 @@ This time is pretty accurate if you automatically deploy your production branch.
|
|||
If you need a more exact time you can have your deployment script create a tag on each deployment.
|
||||
This flow prevents the overhead of releasing, tagging and merging that is common to git flow.
|
||||
|
||||
# Environment branches with GitLab flow
|
||||
## Environment branches with GitLab flow
|
||||
|
||||
![Multiple branches with the code cascading from one to another](environment_branches.png)
|
||||
|
||||
|
@ -93,7 +93,7 @@ If master is good to go (it should be if you a practicing [continuous delivery](
|
|||
If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
|
||||
An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](http://teatro.io/).
|
||||
|
||||
# Release branches with GitLab flow
|
||||
## Release branches with GitLab flow
|
||||
|
||||
![Master and multiple release branches that vary in length with cherrypicks from master](release_branches.png)
|
||||
|
||||
|
@ -109,7 +109,7 @@ Every time a bug-fix is included in a release branch the patch version is raised
|
|||
Some projects also have a stable branch that points to the same commit as the latest released branch.
|
||||
In this flow it is not common to have a production branch (or git flow master branch).
|
||||
|
||||
# Merge/pull requests with GitLab flow
|
||||
## Merge/pull requests with GitLab flow
|
||||
|
||||
![Merge request with line comments](mr_inline_comments.png)
|
||||
|
||||
|
@ -134,7 +134,7 @@ If the assigned person does not feel comfortable they can close the merge reques
|
|||
In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/permissions/permissions.md).
|
||||
So if you want to merge it into a protected branch you assign it to someone with master authorizations.
|
||||
|
||||
# Issues with GitLab flow
|
||||
## Issues with GitLab flow
|
||||
|
||||
![Merge request with the branch name 15-require-a-password-to-change-it and assignee field shown](merge_request.png)
|
||||
|
||||
|
@ -168,7 +168,7 @@ In this case it is no problem to reuse the same branch name since it was deleted
|
|||
At any time there is at most one branch for every issue.
|
||||
It is possible that one feature branch solves more than one issue.
|
||||
|
||||
# Linking and closing issues from merge requests
|
||||
## Linking and closing issues from merge requests
|
||||
|
||||
![Merge request showing the linked issues that will be closed](close_issue_mr.png)
|
||||
|
||||
|
@ -181,7 +181,7 @@ If you only want to make the reference without closing the issue you can also ju
|
|||
|
||||
If you have an issue that spans across multiple repositories, the best thing is to create an issue for each repository and link all issues to a parent issue.
|
||||
|
||||
# Squashing commits with rebase
|
||||
## Squashing commits with rebase
|
||||
|
||||
![Vim screen showing the rebase view](rebase.png)
|
||||
|
||||
|
@ -189,7 +189,7 @@ With git you can use an interactive rebase (rebase -i) to squash multiple commit
|
|||
This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical.
|
||||
However you should never rebase commits you have pushed to a remote server.
|
||||
Somebody can have referred to the commits or cherry-picked them.
|
||||
When you rebase you change the identifier (SHA1) of the commit and this is confusing.
|
||||
When you rebase you change the identifier (SHA-1) of the commit and this is confusing.
|
||||
If you do that the same change will be known under multiple identifiers and this can cause much confusion.
|
||||
If people already reviewed your code it will be hard for them to review only the improvements you made since then if you have rebased everything into one commit.
|
||||
|
||||
|
@ -207,7 +207,7 @@ If you revert a merge and you change your mind, revert the revert instead of mer
|
|||
Being able to revert a merge is a good reason always to create a merge commit when you merge manually with the `--no-ff` option.
|
||||
Git management software will always create a merge commit when you accept a merge request.
|
||||
|
||||
# Do not order commits with rebase
|
||||
## Do not order commits with rebase
|
||||
|
||||
![List of sequential merge commits](merge_commits.png)
|
||||
|
||||
|
@ -231,8 +231,8 @@ The last reason for creating merge commits is having long lived branches that yo
|
|||
Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI).
|
||||
At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
|
||||
That's continuous building, and a Good Thing, but there's no integration, so it's not CI.".
|
||||
The solution to prevent many merge commits is to keep your feature branches shortlived, the vast majority should take less than one day of work.
|
||||
If your feature branches commenly take more than a day of work, look into ways to create smaller units of work and/or use [feature toggles](http://martinfowler.com/bliki/FeatureToggle.html).
|
||||
The solution to prevent many merge commits is to keep your feature branches short-lived, the vast majority should take less than one day of work.
|
||||
If your feature branches commonly take more than a day of work, look into ways to create smaller units of work and/or use [feature toggles](http://martinfowler.com/bliki/FeatureToggle.html).
|
||||
As for the long running branches that take more than one day there are two strategies.
|
||||
In a CI strategy you can merge in master at the start of the day to prevent painful merges at a later time.
|
||||
In a synchronization point strategy you only merge in from well defined points in time, for example a tagged release.
|
||||
|
@ -244,7 +244,7 @@ Developing software happen in small messy steps and it is OK to have your histor
|
|||
You can use tools to view the network graphs of commits and understand the messy history that created your code.
|
||||
If you rebase code the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers.
|
||||
|
||||
# Voting on merge requests
|
||||
## Voting on merge requests
|
||||
|
||||
![Voting slider in GitLab](voting_slider.png)
|
||||
|
||||
|
@ -252,7 +252,7 @@ It is common to voice approval or disapproval by using +1 or -1 emoticons.
|
|||
In GitLab the +1 and -1 are aggregated and shown at the top of the merge request.
|
||||
As a rule of thumb anything that doesn't have two times more +1's than -1's is suspect and should not be merged yet.
|
||||
|
||||
# Pushing and removing branches
|
||||
## Pushing and removing branches
|
||||
|
||||
![Remove checkbox for branch in merge requests](remove_checkbox.png)
|
||||
|
||||
|
@ -266,7 +266,7 @@ This ensures that the branch overview in the repository management software show
|
|||
This also ensures that when someone reopens the issue a new branch with the same name can be used without problem.
|
||||
When you reopen an issue you need to create a new merge request.
|
||||
|
||||
# Committing often and with the right message
|
||||
## Committing often and with the right message
|
||||
|
||||
![Good and bad commit message](good_commit.png)
|
||||
|
||||
|
@ -282,7 +282,7 @@ Some words that are bad commit messages because they don't contain munch informa
|
|||
The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number.
|
||||
To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
# Testing before merging
|
||||
## Testing before merging
|
||||
|
||||
![Merge requests showing the test states, red, yellow and green](ci_mr.png)
|
||||
|
||||
|
@ -299,7 +299,7 @@ If there are no merge conflicts and the feature branches are short lived the ris
|
|||
If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests.
|
||||
If you have long lived feature branches that last for more than a few days you should make your issues smaller.
|
||||
|
||||
# Merging in other code
|
||||
## Merging in other code
|
||||
|
||||
![Shell output showing git pull output](git_pull.png)
|
||||
|
||||
|
|
|
@ -71,6 +71,20 @@ Feature: Profile
|
|||
And I click on my profile picture
|
||||
Then I should see my user page
|
||||
|
||||
Scenario: I can manage application
|
||||
Given I visit profile applications page
|
||||
Then I click on new application button
|
||||
And I should see application form
|
||||
Then I fill application form out and submit
|
||||
And I see application
|
||||
Then I click edit
|
||||
And I see edit application form
|
||||
Then I change name of application and submit
|
||||
And I see that application was changed
|
||||
Then I visit profile applications page
|
||||
And I click to remove application
|
||||
Then I see that application is removed
|
||||
|
||||
@javascript
|
||||
Scenario: I change my application theme
|
||||
Given I visit profile design page
|
||||
|
@ -101,4 +115,4 @@ Feature: Profile
|
|||
Scenario: I see the password strength indicator with success
|
||||
Given I visit profile password page
|
||||
When I try to set a strong password
|
||||
Then I should see the input field green
|
||||
Then I should see the input field green
|
||||
|
|
|
@ -66,3 +66,10 @@ Feature: Project Services
|
|||
And I click Atlassian Bamboo CI service link
|
||||
And I fill Atlassian Bamboo CI settings
|
||||
Then I should see Atlassian Bamboo CI service settings saved
|
||||
|
||||
Scenario: Activate jetBrains TeamCity CI service
|
||||
When I visit project "Shop" services page
|
||||
And I click jetBrains TeamCity CI service link
|
||||
And I fill jetBrains TeamCity CI settings
|
||||
Then I should see jetBrains TeamCity CI service settings saved
|
||||
|
||||
|
|
|
@ -188,7 +188,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I should see group milestone with descriptions and expiry date' do
|
||||
page.should have_content('Lorem Ipsum is simply dummy text of the printing and typesetting industry')
|
||||
page.should have_content('expires at Aug 20, 2114')
|
||||
end
|
||||
|
||||
|
|
|
@ -221,4 +221,54 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
|
|||
step 'I should see groups I belong to' do
|
||||
page.should have_css('.profile-groups-avatars', visible: true)
|
||||
end
|
||||
|
||||
step 'I click on new application button' do
|
||||
click_on 'New Application'
|
||||
end
|
||||
|
||||
step 'I should see application form' do
|
||||
page.should have_content "New application"
|
||||
end
|
||||
|
||||
step 'I fill application form out and submit' do
|
||||
fill_in :doorkeeper_application_name, with: 'test'
|
||||
fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
|
||||
click_on "Submit"
|
||||
end
|
||||
|
||||
step 'I see application' do
|
||||
page.should have_content "Application: test"
|
||||
page.should have_content "Application Id"
|
||||
page.should have_content "Secret"
|
||||
end
|
||||
|
||||
step 'I click edit' do
|
||||
click_on "Edit"
|
||||
end
|
||||
|
||||
step 'I see edit application form' do
|
||||
page.should have_content "Edit application"
|
||||
end
|
||||
|
||||
step 'I change name of application and submit' do
|
||||
page.should have_content "Edit application"
|
||||
fill_in :doorkeeper_application_name, with: 'test_changed'
|
||||
click_on "Submit"
|
||||
end
|
||||
|
||||
step 'I see that application was changed' do
|
||||
page.should have_content "test_changed"
|
||||
page.should have_content "Application Id"
|
||||
page.should have_content "Secret"
|
||||
end
|
||||
|
||||
step 'I click to remove application' do
|
||||
within '.oauth-applications' do
|
||||
click_on "Destroy"
|
||||
end
|
||||
end
|
||||
|
||||
step "I see that application is removed" do
|
||||
page.find(".oauth-applications").should_not have_content "test_changed"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -78,14 +78,14 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I click side-by-side diff button' do
|
||||
click_link "Side-by-side Diff"
|
||||
click_link "Side-by-side"
|
||||
end
|
||||
|
||||
step 'I see side-by-side diff button' do
|
||||
page.should have_content "Side-by-side Diff"
|
||||
page.should have_content "Side-by-side"
|
||||
end
|
||||
|
||||
step 'I see inline diff button' do
|
||||
page.should have_content "Inline Diff"
|
||||
page.should have_content "Inline"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -109,6 +109,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I click on the commit in the merge request' do
|
||||
within '.merge-request-tabs' do
|
||||
click_link 'Commits'
|
||||
end
|
||||
|
||||
within '.mr-commits' do
|
||||
click_link Commit.truncate_sha(sample_commit.id)
|
||||
end
|
||||
|
@ -261,7 +265,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
|
|||
end
|
||||
|
||||
step 'I click Side-by-side Diff tab' do
|
||||
click_link 'Side-by-side Diff'
|
||||
click_link 'Side-by-side'
|
||||
end
|
||||
|
||||
step 'I should see comments on the side-by-side diff page' do
|
||||
|
|
|
@ -15,6 +15,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
|
|||
page.should have_content 'Assembla'
|
||||
page.should have_content 'Pushover'
|
||||
page.should have_content 'Atlassian Bamboo'
|
||||
page.should have_content 'JetBrains TeamCity'
|
||||
end
|
||||
|
||||
step 'I click gitlab-ci service link' do
|
||||
|
@ -168,4 +169,23 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
|
|||
find_field('Build key').value.should == 'KEY'
|
||||
find_field('Username').value.should == 'user'
|
||||
end
|
||||
|
||||
step 'I click JetBrains TeamCity CI service link' do
|
||||
click_link 'JetBrains TeamCity CI'
|
||||
end
|
||||
|
||||
step 'I fill JetBrains TeamCity CI settings' do
|
||||
check 'Active'
|
||||
fill_in 'Teamcity url', with: 'http://teamcity.example.com'
|
||||
fill_in 'Build type', with: 'GitlabTest_Build'
|
||||
fill_in 'Username', with: 'user'
|
||||
fill_in 'Password', with: 'verySecret'
|
||||
click_button 'Save'
|
||||
end
|
||||
|
||||
step 'I should see JetBrains TeamCity CI service settings saved' do
|
||||
find_field('Teamcity url').value.should == 'http://teamcity.example.com'
|
||||
find_field('Build type').value.should == 'GitlabTest_Build'
|
||||
find_field('Username').value.should == 'user'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
module SharedPaths
|
||||
include Spinach::DSL
|
||||
include RepoHelpers
|
||||
include DashboardHelper
|
||||
|
||||
step 'I visit new project page' do
|
||||
visit new_project_path
|
||||
|
@ -71,11 +72,11 @@ module SharedPaths
|
|||
end
|
||||
|
||||
step 'I visit dashboard issues page' do
|
||||
visit issues_dashboard_path
|
||||
visit assigned_issues_dashboard_path
|
||||
end
|
||||
|
||||
step 'I visit dashboard merge requests page' do
|
||||
visit merge_requests_dashboard_path
|
||||
visit assigned_mrs_dashboard_path
|
||||
end
|
||||
|
||||
step 'I visit dashboard search page' do
|
||||
|
@ -94,6 +95,10 @@ module SharedPaths
|
|||
visit profile_path
|
||||
end
|
||||
|
||||
step 'I visit profile applications page' do
|
||||
visit applications_profile_path
|
||||
end
|
||||
|
||||
step 'I visit profile password page' do
|
||||
visit edit_profile_password_path
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file}
|
|||
|
||||
module API
|
||||
class API < Grape::API
|
||||
include APIGuard
|
||||
version 'v3', using: :path
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do
|
||||
|
|
175
lib/api/api_guard.rb
Normal file
175
lib/api/api_guard.rb
Normal file
|
@ -0,0 +1,175 @@
|
|||
# Guard API with OAuth 2.0 Access Token
|
||||
|
||||
require 'rack/oauth2'
|
||||
|
||||
module APIGuard
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do |base|
|
||||
# OAuth2 Resource Server Authentication
|
||||
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
|
||||
# The authenticator only fetches the raw token string
|
||||
|
||||
# Must yield access token to store it in the env
|
||||
request.access_token
|
||||
end
|
||||
|
||||
helpers HelperMethods
|
||||
|
||||
install_error_responders(base)
|
||||
end
|
||||
|
||||
# Helper Methods for Grape Endpoint
|
||||
module HelperMethods
|
||||
# Invokes the doorkeeper guard.
|
||||
#
|
||||
# If token is presented and valid, then it sets @current_user.
|
||||
#
|
||||
# If the token does not have sufficient scopes to cover the requred scopes,
|
||||
# then it raises InsufficientScopeError.
|
||||
#
|
||||
# If the token is expired, then it raises ExpiredError.
|
||||
#
|
||||
# If the token is revoked, then it raises RevokedError.
|
||||
#
|
||||
# If the token is not found (nil), then it raises TokenNotFoundError.
|
||||
#
|
||||
# Arguments:
|
||||
#
|
||||
# scopes: (optional) scopes required for this guard.
|
||||
# Defaults to empty array.
|
||||
#
|
||||
def doorkeeper_guard!(scopes: [])
|
||||
if (access_token = find_access_token).nil?
|
||||
raise TokenNotFoundError
|
||||
|
||||
else
|
||||
case validate_access_token(access_token, scopes)
|
||||
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
|
||||
raise InsufficientScopeError.new(scopes)
|
||||
|
||||
when Oauth2::AccessTokenValidationService::EXPIRED
|
||||
raise ExpiredError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::REVOKED
|
||||
raise RevokedError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::VALID
|
||||
@current_user = User.find(access_token.resource_owner_id)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def doorkeeper_guard(scopes: [])
|
||||
if access_token = find_access_token
|
||||
case validate_access_token(access_token, scopes)
|
||||
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
|
||||
raise InsufficientScopeError.new(scopes)
|
||||
|
||||
when Oauth2::AccessTokenValidationService::EXPIRED
|
||||
raise ExpiredError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::REVOKED
|
||||
raise RevokedError
|
||||
|
||||
when Oauth2::AccessTokenValidationService::VALID
|
||||
@current_user = User.find(access_token.resource_owner_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user
|
||||
end
|
||||
|
||||
private
|
||||
def find_access_token
|
||||
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
|
||||
end
|
||||
|
||||
def doorkeeper_request
|
||||
@doorkeeper_request ||= ActionDispatch::Request.new(env)
|
||||
end
|
||||
|
||||
def validate_access_token(access_token, scopes)
|
||||
Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Installs the doorkeeper guard on the whole Grape API endpoint.
|
||||
#
|
||||
# Arguments:
|
||||
#
|
||||
# scopes: (optional) scopes required for this guard.
|
||||
# Defaults to empty array.
|
||||
#
|
||||
def guard_all!(scopes: [])
|
||||
before do
|
||||
guard! scopes: scopes
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def install_error_responders(base)
|
||||
error_classes = [ MissingTokenError, TokenNotFoundError,
|
||||
ExpiredError, RevokedError, InsufficientScopeError]
|
||||
|
||||
base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
|
||||
end
|
||||
|
||||
def oauth2_bearer_token_error_handler
|
||||
Proc.new {|e|
|
||||
response = case e
|
||||
when MissingTokenError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
|
||||
|
||||
when TokenNotFoundError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||
:invalid_token,
|
||||
"Bad Access Token.")
|
||||
|
||||
when ExpiredError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||
:invalid_token,
|
||||
"Token is expired. You can either do re-authorization or token refresh.")
|
||||
|
||||
when RevokedError
|
||||
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
|
||||
:invalid_token,
|
||||
"Token was revoked. You have to re-authorize from the user.")
|
||||
|
||||
when InsufficientScopeError
|
||||
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
|
||||
# does not include WWW-Authenticate header, which breaks the standard.
|
||||
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
|
||||
:insufficient_scope,
|
||||
Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope],
|
||||
{ :scope => e.scopes})
|
||||
end
|
||||
|
||||
response.finish
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Exceptions
|
||||
#
|
||||
|
||||
class MissingTokenError < StandardError; end
|
||||
|
||||
class TokenNotFoundError < StandardError; end
|
||||
|
||||
class ExpiredError < StandardError; end
|
||||
|
||||
class RevokedError < StandardError; end
|
||||
|
||||
class InsufficientScopeError < StandardError
|
||||
attr_reader :scopes
|
||||
def initialize(scopes)
|
||||
@scopes = scopes
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@ module API
|
|||
|
||||
def current_user
|
||||
private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
|
||||
@current_user ||= User.find_by(authentication_token: private_token)
|
||||
@current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
|
||||
|
||||
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
|
||||
return nil
|
||||
|
|
|
@ -41,6 +41,7 @@ describe API, api: true do
|
|||
describe ".current_user" do
|
||||
it "should return nil for an invalid token" do
|
||||
env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
|
||||
self.class.any_instance.stub(:doorkeeper_guard){ false }
|
||||
current_user.should be_nil
|
||||
end
|
||||
|
||||
|
|
31
spec/requests/api/doorkeeper_access_spec.rb
Normal file
31
spec/requests/api/doorkeeper_access_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::API, api: true do
|
||||
include ApiHelpers
|
||||
|
||||
let!(:user) { create(:user) }
|
||||
let!(:application) { Doorkeeper::Application.create!(:name => "MyApp", :redirect_uri => "https://app.com", :owner => user) }
|
||||
let!(:token) { Doorkeeper::AccessToken.create! :application_id => application.id, :resource_owner_id => user.id }
|
||||
|
||||
|
||||
describe "when unauthenticated" do
|
||||
it "returns authentication success" do
|
||||
get api("/user"), :access_token => token.token
|
||||
response.status.should == 200
|
||||
end
|
||||
end
|
||||
|
||||
describe "when token invalid" do
|
||||
it "returns authentication error" do
|
||||
get api("/user"), :access_token => "123a"
|
||||
response.status.should == 401
|
||||
end
|
||||
end
|
||||
|
||||
describe "authorization by private token" do
|
||||
it "returns authentication success" do
|
||||
get api("/user", user)
|
||||
response.status.should == 200
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue