Commit a5972170 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'origin/master' into ee-7-4

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>

Conflicts:
	app/controllers/projects/team_members_controller.rb
	app/finders/projects_finder.rb
	app/models/group.rb
	app/models/project_team.rb
	app/models/users_group.rb
	config/gitlab.yml.example
	config/initializers/1_settings.rb
	config/routes.rb
	db/schema.rb
	doc/integration/ldap.md
	features/steps/group.rb
	features/steps/project/project.rb
parents cb51407c 9f939c96
v 7.4.0
- Refactored membership logic
- Improve error reporting on users API (Julien Bianchi)
- Refactor test coverage tools usage. Use SIMPLECOV=true to generate it locally
- Default branch is protected by default
- Increase unicorn timeout to 60 seconds
- Sort search autocomplete projects by stars count so most popular go first
- Add README to tab on project show page
- Do not delete tmp/repositories itself during clean-up, only its contents
- Support for backup uploads to remote storage
- Prevent notes polling when there are not notes
- API: Add support for forking a project via the API (Bernhard Kaindl)
- API: filter project issues by milestone (Julien Bianchi)
- Fail harder in the backup script
v 7.3.2
- Fix creating new file via web editor
- Use gitlab-shell v2.0.1
v 7.3.1
- Fix ref parsing in Gitlab::GitAccess
- Fix error 500 when viewing diff on a file with changed permissions
- Fix adding comments to MR when source branch is master
- Fix error 500 when searching description contains relative link
v 7.3.0
- Always set the 'origin' remote in satellite actions
- Write authorized_keys in tmp/ during tests
......
......@@ -61,7 +61,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Fork the project on GitLab Cloud
1. Create a feature branch
1. Write [tests](README.md#run-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. Add your changes to the [CHANGELOG](CHANGELOG) insert your line at a [random point](doc/workflow/gitlab_flow.md#do-not-order-commits-with-rebase) in the current version
1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message
1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
1. Push the commit to your fork
......
......@@ -31,7 +31,7 @@ gem 'omniauth-shibboleth'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", '~> 6.0'
gem "gitlab_git", '7.0.0.rc8'
# Ruby/Rack Git Smart-HTTP Server Handler
gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack'
......@@ -71,8 +71,8 @@ gem "carrierwave"
gem 'dropzonejs-rails'
# for aws storage
gem "fog", "~> 1.14", group: :aws
gem "unf", group: :aws
gem "fog", "~> 1.14"
gem "unf"
# Authorization
gem "six"
......@@ -80,6 +80,9 @@ gem "six"
# Seed data
gem "seed-fu"
# Markup pipeline for GitLab
gem 'html-pipeline-gitlab', '~> 0.1.0'
# Markdown to HTML
gem "github-markup"
......
......@@ -106,7 +106,7 @@ GEM
devise (~> 3.2)
diff-lcs (1.2.5)
diffy (3.0.3)
docile (1.1.1)
docile (1.1.5)
dotenv (0.9.0)
dropzonejs-rails (0.4.14)
rails (> 3.1)
......@@ -179,10 +179,9 @@ GEM
mime-types (~> 1.19)
gitlab_emoji (0.0.1.1)
emoji (~> 1.0.1)
gitlab_git (6.2.1)
gitlab_git (7.0.0.rc8)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-grit (~> 2.6)
gitlab-linguist (~> 3.0)
rugged (~> 0.21.0)
gitlab_meta (7.0)
......@@ -239,6 +238,12 @@ GEM
hipchat (0.14.0)
httparty
httparty
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
html-pipeline-gitlab (0.1.0)
gitlab_emoji (~> 0.0.1.1)
html-pipeline (~> 1.11.0)
http_parser.rb (0.5.3)
httparty (0.13.0)
json (~> 1.8)
......@@ -471,7 +476,7 @@ GEM
redis (>= 3.0.4)
redis-namespace (>= 1.3.1)
simple_oauth (0.1.9)
simplecov (0.8.2)
simplecov (0.9.0)
docile (~> 1.1.0)
multi_json
simplecov-html (~> 0.8.0)
......@@ -617,7 +622,7 @@ DEPENDENCIES
gitlab-grack (~> 2.0.0.pre)
gitlab-linguist (~> 3.0.0)
gitlab_emoji (~> 0.0.1.1)
gitlab_git (~> 6.0)
gitlab_git (= 7.0.0.rc8)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (= 1.1.0)
gollum-lib (~> 3.0.0)
......@@ -629,6 +634,7 @@ DEPENDENCIES
guard-spinach
haml-rails
hipchat (~> 0.14.0)
html-pipeline-gitlab (~> 0.1.0)
httparty
jasmine (= 2.0.2)
jquery-atwho-rails (~> 0.3.3)
......
......@@ -23,7 +23,7 @@
- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq)
- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master)
- [![PullReview stats](https://www.pullreview.com/gitlab/gitlab-org/gitlab-ce/badges/master.svg?)](https://www.pullreview.com/gitlab.gitlab.com/gitlab-org/gitlab-ce/reviews/master)
......@@ -38,17 +38,6 @@ On [about.gitlab.com](https://about.gitlab.com/) you can find more information a
- [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/) with additional features aimed at larger organizations.
- [GitLab CI](https://about.gitlab.com/gitlab-ci/) a continuous integration (CI) server that is easy to integrate with GitLab.
## Third-party applications
Access GitLab from multiple platforms with applications below.
These applications are maintained by contributors, GitLab B.V. does not offer support for them.
- [iPhone app](http://gitlabcontrol.com/)
- [Android app](https://play.google.com/store/apps/details?id=com.bd.gitlab&hl=en)
- [Chrome app](https://chrome.google.com/webstore/detail/chrome-gitlab-notifier/eageapgbnjicdjjihgclpclilenjbobi)
- [Command line client](https://github.com/drewblessing/gitlab-cli)
- [Ruby API wrapper](https://github.com/NARKOZ/gitlab)
## Requirements
- Ubuntu/Debian/CentOS/RHEL**
......@@ -61,7 +50,19 @@ These applications are maintained by contributors, GitLab B.V. does not offer su
## Installation
Please see [the installation page on the GitLab website](https://about.gitlab.com/installation/).
Please see [the installation page on the GitLab website](https://about.gitlab.com/installation/) for the various options.
Since a manual installation is a lot of work and error prone we strongly recommend fast and reliable Omnibus package installation (deb/rpm) on that page.
## Third-party applications
Access GitLab from multiple platforms with applications below.
These applications are maintained by contributors, GitLab B.V. does not offer support for them.
- [iPhone app](http://gitlabcontrol.com/)
- [Android app](https://play.google.com/store/apps/details?id=com.bd.gitlab&hl=en)
- [Chrome app](https://chrome.google.com/webstore/detail/chrome-gitlab-notifier/eageapgbnjicdjjihgclpclilenjbobi)
- [Command line client](https://github.com/drewblessing/gitlab-cli)
- [Ruby API wrapper](https://github.com/NARKOZ/gitlab)
### New versions
......
......@@ -46,10 +46,10 @@ class Admin
modal.hide()
$('.change-owner-link').show()
$('li.users_project').bind 'ajax:success', ->
$('li.project_member').bind 'ajax:success', ->
Turbolinks.visit(location.href)
$('li.users_group').bind 'ajax:success', ->
$('li.group_member').bind 'ajax:success', ->
Turbolinks.visit(location.href)
@Admin = Admin
......@@ -15,13 +15,14 @@
#= require jquery.atwho
#= require jquery.scrollTo
#= require jquery.blockUI
#= require turbolinks
#= require jquery.turbolinks
#= require turbolinks
#= require bootstrap
#= require select2
#= require raphael
#= require g.raphael-min
#= require g.bar-min
#= require chart-lib.min
#= require branch-graph
#= require highlight.pack
#= require ace/ace
......@@ -149,7 +150,6 @@ $ ->
if (flash = $(".flash-container")).length > 0
flash.click -> $(@).fadeOut()
flash.show()
setTimeout (-> flash.fadeOut()), 5000
# Disable form buttons while a form is submitting
$('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
......
......@@ -90,11 +90,15 @@ class @BranchGraph
renderPartialGraph: ->
start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10
start = 0 if start < 0
if start < 0
isGraphEdge = true
start = 0
end = start + 40
end = @commits.length if @commits.length < end
if @commits.length < end
isGraphEdge = true
end = @commits.length
if @prev_start == -1 or Math.abs(@prev_start - start) > 10
if @prev_start == -1 or Math.abs(@prev_start - start) > 10 or isGraphEdge
i = start
@prev_start = start
......
@Chart =
labels: []
values: []
init: (labels, values, title) ->
r = Raphael('activity-chart')
fin = ->
@flag = r.popup(@bar.x, @bar.y, @bar.value or "0").insertBefore(this)
fout = ->
@flag.animate
opacity: 0, 300, -> @remove()
r.text(160, 10, title).attr font: "13px sans-serif"
r.barchart(
10, 20, 560, 200,
[values],
{colors:["#456"]}
).label(labels, true)
.hover(fin, fout)
......@@ -10,6 +10,5 @@ class Flash
flash.click -> $(@).fadeOut()
flash.show()
setTimeout (-> flash.fadeOut()), 5000
@Flash = Flash
class GroupMembers
constructor: ->
$('li.users_group').bind 'ajax:success', ->
$('li.group_member').bind 'ajax:success', ->
$(this).fadeOut()
@GroupMembers = GroupMembers
......
......@@ -27,7 +27,7 @@ $(document).ready ->
dropzone = $(".div-dropzone").dropzone(
url: project_image_path_upload
dictDefaultMessage: ""
clickable: false
clickable: true
paramName: "markdown_img"
maxFilesize: 10
uploadMultiple: false
......@@ -190,7 +190,7 @@ $(document).ready ->
$(".markdown-selector").click (e) ->
e.preventDefault()
$(@).closest(".div-dropzone-wrapper").find(".div-dropzone").click()
$(@).closest('.gfm-form').find('.div-dropzone').click()
return
return
......@@ -6,6 +6,7 @@ class Notes
@notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root?
@note_ids = note_ids
@last_fetched_at = last_fetched_at
@noteable_url = document.URL
@initRefresh()
@setupMainTargetNoteForm()
@cleanBinding()
......@@ -95,7 +96,8 @@ class Notes
, 15000
refresh: ->
@getContent() unless document.hidden
unless document.hidden or (@noteable_url != document.URL)
@getContent()
getContent: ->
$.ajax
......
......@@ -59,3 +59,12 @@ $ ->
$(@).toggleClass('on').find('.count').html(data.star_count)
.on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
$("a[data-toggle='tab']").on "shown.bs.tab", (e) ->
$.cookie "default_view", $(e.target).attr("href")
defaultView = $.cookie("default_view")
if defaultView
$("a[href=" + defaultView + "]").tab "show"
else
$("a[data-toggle='tab']:first").tab "show"
......@@ -24,22 +24,7 @@ class window.ContributorsStatGraph
class: 'graph-author-commits-count'
})
commits.text(author.commits + " commits")
additions = $('<span/>', {
class: 'graph-additions'
})
additions.text(author.additions + " ++")
deletions = $('<span/>', {
class: 'graph-deletions'
})
deletions.text(author.deletions + " --")
$('<span/>').append(commits)
.append(" / ")
.append(additions)
.append(" / ")
.append(deletions)
create_author_header: (author) ->
list_item = $('<li/>', {
......
......@@ -32,6 +32,8 @@ class @ZenMode
@active_zen_area = @active_checkbox.parent().find('textarea')
@active_zen_area.focus()
window.location.hash = ZenMode.fullscreen_prefix + @active_checkbox.prop('id')
# Disable dropzone in ZEN mode
Dropzone.forElement('.div-dropzone').disable()
exitZenMode: =>
if @active_zen_area isnt null
......@@ -41,6 +43,8 @@ class @ZenMode
@active_checkbox = null
window.location.hash = ''
window.scrollTo(window.pageXOffset, @scroll_position)
# Enable dropzone when leaving ZEN mode
Dropzone.forElement('.div-dropzone').enable()
checkboxFromLocationHash: (e) ->
id = $.trim(window.location.hash.replace('#' + ZenMode.fullscreen_prefix, ''))
......
......@@ -60,7 +60,6 @@
.highlight {
margin-bottom: 9px;
@include border-radius(4px);
> pre {
margin: 0;
......
.flash-container {
display: none;
cursor: pointer;
margin: 0;
text-align: center;
color: #fff;
font-size: 14px;
position: fixed;
bottom: 0;
width: 100%;
opacity: 0.8;
z-index: 100;
.flash-notice {
background: #49C;
padding: 10px;
text-shadow: 0 1px 1px #178;
@extend .alert;
@extend .alert-info;
}
.flash-alert {
background: #C67;
text-shadow: 0 1px 1px #945;
padding: 10px;
@extend .alert;
@extend .alert-danger;
}
}
textarea {
resize: vertical;
}
input[type='search'].search-text-input {
background-image: image-url("icon-search.png");
background-repeat: no-repeat;
......
......@@ -8,43 +8,51 @@
*/
.issue-box {
color: #666;
color: #555;
margin:20px 0;
background: #FFF;
border: 1px solid #EEE;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05));
background: #f9f9f9;
border-top-left-radius: 5px;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09));
&.issue-box-closed {
border-color: $border_danger;
.state {
background-color: #F3CECE;
border-color: $border_danger;
}
.state-label {
background-color: $bg_danger;
color: #FFF;
border-color: $border_danger;
}
}
&.issue-box-merged {
border-color: $border_primary;
.state {
background-color: #B7CEE7;
border-color: $border_primary;
}
.state-label {
background-color: $bg_primary;
color: #FFF;
border-color: $border_primary;
}
}
&.issue-box-open {
border-color: $border_success;
.state {
border-color: $border_success;
background-color: #D6F1D7;
border-color: $bg_success;
}
.state-label {
background-color: $bg_success;
color: #FFF;
}
}
&.issue-box-expired {
border-color: #cea61b;
.state {
background-color: #EEE9B3;
border-color: #faebcc;
}
.state-label {
background: #cea61b;
color: #FFF;
}
......@@ -55,8 +63,7 @@
}
.state {
border-bottom: 1px solid #DDD;
padding: 10px 15px;
background-color: #f9f9f9;
}
.title {
......@@ -104,12 +111,14 @@
font-size: 14px;
float: left;
font-weight: bold;
padding: 10px 15px;
border-top-left-radius: 5px;
}
.creator {
float: right;
padding: 10px 15px;
a {
color: #FFF;
text-decoration: underline;
}
}
......
.white {
background-color: #fff;
.line.hll {
background: #FFA;
}
.highlight{
border-left: 1px solid #eee;
}
pre {
background-color: #fff;
color: #333;
......@@ -179,8 +173,16 @@
@include box-shadow(0 5px 15px #000);
}
.wiki, .note-body {
.file-content {
&.code .white {
.highlight {
border: 1px solid #DDD;
border-left: 1px solid #eee;
}
}
&.wiki .white {
.highlight, pre, .hljs {
background: #F9F9F9;
}
}
}
......@@ -244,7 +244,6 @@ li.commit {
font-family: inherit;
padding-left: $left;
position: relative;
resize: vertical;
z-index: 2;
}
}
......@@ -19,6 +19,10 @@ ul.notes {
@extend .cgray;
padding-bottom: 15px;
a:hover {
text-decoration: none;
}
.avatar {
float: left;
margin-right: 10px;
......@@ -143,8 +147,14 @@ ul.notes {
*/
.diff-file tr.line_holder {
@mixin show-add-diff-note {
filter: alpha(opacity=100);
opacity: 1.0;
}
.add-diff-note {
background: image-url("diff_note_add.png") no-repeat left 0;
border: none;
height: 22px;
margin-left: -65px;
position: absolute;
......@@ -156,8 +166,7 @@ ul.notes {
filter: alpha(opacity=0);
&:hover {
opacity: 1.0;
filter: alpha(opacity=100);
@include show-add-diff-note;
}
}
......@@ -166,8 +175,7 @@ ul.notes {
background: $hover !important;
.add-diff-note {
opacity: 1.0;
filter: alpha(opacity=100);
@include show-add-diff-note;
}
}
}
......
......@@ -48,6 +48,10 @@
float: right;
margin-left: 20px;
a:hover {
text-decoration: none;
}
.count {
margin-left: 5px;
}
......
......@@ -8,7 +8,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def show
@members = @group.members.order("group_access DESC").page(params[:members_page]).per(30)
@members = @group.members.order("access_level DESC").page(params[:members_page]).per(30)
@projects = @group.projects.page(params[:projects_page]).per(30)
end
......@@ -40,7 +40,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def project_teams_update
@group.add_users(params[:user_ids].split(','), params[:group_access])
@group.add_users(params[:user_ids].split(','), params[:access_level])
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
......
......@@ -16,10 +16,10 @@ class Admin::ProjectsController < Admin::ApplicationController
def show
if @group
@group_members = @group.members.order("group_access DESC").page(params[:group_members_page]).per(30)
@group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(30)
end
@project_members = @project.users_projects.page(params[:project_members_page]).per(30)
@project_members = @project.project_members.page(params[:project_members_page]).per(30)
end
def transfer
......
class UsersGroupsController < ApplicationController
class Groups::GroupMembersController < ApplicationController
before_filter :group
# Authorize
......@@ -7,18 +7,18 @@ class UsersGroupsController < ApplicationController
layout 'group'
def create
@group.add_users(params[:user_ids].split(','), params[:group_access])
@group.add_users(params[:user_ids].split(','), params[:access_level])
redirect_to members_group_path(@group), notice: 'Users were successfully added.'
end
def update
@member = @group.users_groups.find(params[:id])
@member = @group.group_members.find(params[:id])
@member.update_attributes(member_params)
end
def destroy
@users_group = @group.users_groups.find(params[:id])
@users_group = @group.group_members.find(params[:id])
if can?(current_user, :destroy, @users_group) # May fail if last owner.
@users_group.destroy
respond_to do |format|
......@@ -43,6 +43,6 @@ class UsersGroupsController < ApplicationController
end
def member_params
params.require(:users_group).permit(:group_access, :user_id)
params.require(:group_member).permit(:access_level, :user_id)
end
end
......@@ -67,15 +67,15 @@ class GroupsController < ApplicationController
def members
@project = group.projects.find(params[:project_id]) if params[:project_id]
@members = group.users_groups
@members = group.group_members
if params[:search].present?
users = group.users.search(params[:search]).to_a
@members = @members.where(user_id: users)
end
@members = @members.order('group_access DESC').page(params[:page]).per(50)
@users_group = UsersGroup.new
@members = @members.order('access_level DESC').page(params[:page]).per(50)
@users_group = GroupMember.new
end
def edit
......
......@@ -2,11 +2,11 @@ class Profiles::GroupsController < ApplicationController
layout "profile"
def index
@user_groups = current_user.users_groups.page(params[:page]).per(20)
@user_groups = current_user.group_members.page(params[:page]).per(20)
end
def leave
@users_group = group.users_groups.where(user_id: current_user.id).first
@users_group = group.group_members.where(user_id: current_user.id).first
if can?(current_user, :destroy, @users_group)
@users_group.destroy
redirect_to(profile_groups_path, info: "You left #{group.name} group.")
......
......@@ -3,8 +3,8 @@ class Profiles::NotificationsController < ApplicationController
def show
@notification = current_user.notification
@users_projects = current_user.users_projects
@users_groups = current_user.users_groups
@project_members = current_user.project_members
@group_members = current_user.group_members
end
def update
......@@ -14,13 +14,13 @@ class Profiles::NotificationsController < ApplicationController
current_user.notification_level = params[:notification_level]
current_user.save
elsif type == 'group'
users_group = current_user.users_groups.find(params[:notification_id])
users_group = current_user.group_members.find(params[:notification_id])
users_group.notification_level = params[:notification_level]
users_group.save
else
users_project = current_user.users_projects.find(params[:notification_id])
users_project.notification_level = params[:notification_level]
users_project.save
project_member = current_user.project_members.find(params[:notification_id])
project_member.notification_level = params[:notification_level]
project_member.save
end
end
end
......@@ -17,10 +17,8 @@ class Projects::BranchesController < Projects::ApplicationController
end
def create
result = CreateBranchService.new.execute(project,
params[:branch_name],
params[:ref],
current_user)
result = CreateBranchService.new(project, current_user).
execute(params[:branch_name], params[:ref])
if result[:status] == :success
@branch = result[:branch]
redirect_to project_tree_path(@project, @branch.name)
......@@ -31,7 +29,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
def destroy
DeleteBranchService.new.execute(project, params[:id], current_user)
DeleteBranchService.new(project, current_user).execute(params[:id])
@branch_name = params[:id]
respond_to do |format|
......
......@@ -12,20 +12,8 @@ class Projects::CommitController < Projects::ApplicationController
return git_not_found! unless @commit
@line_notes = project.notes.for_commit_id(commit.id).inline
@branches = begin
project.repository.branch_names_contains(commit.id)
rescue Grit::Git::GitTimeout
[]
end
begin
@branches = project.repository.branch_names_contains(commit.id)
@diffs = @commit.diffs
rescue Grit::Git::GitTimeout
@diffs = []
@diff_timeout = true
end
@note = project.build_commit_note(commit)
@notes_count = project.notes.for_commit_id(commit.id).count
@notes = project.notes.for_commit_id(@commit.id).not_inline.fresh
......
......@@ -10,7 +10,8 @@ class Projects::EditTreeController < Projects::BaseTreeController
end
def update
result = Files::UpdateService.new(@project, current_user, params, @ref, @path).execute
result = Files::UpdateService.
new(@project, current_user, params, @ref, @path).execute
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
......
......@@ -7,19 +7,34 @@ class Projects::GraphsController < Projects::ApplicationController
def show
respond_to do |format|
format.html
format.js do
format.json do
fetch_graph
end
end
end
def commits
@commits = @project.repository.commits(nil, nil, 2000, 0, true)
@commits_graph = Gitlab::Graphs::Commits.new(@commits)
@commits_per_week_days = @commits_graph.commits_per_week_days
@commits_per_time = @commits_graph.commits_per_time
@commits_per_month = @commits_graph.commits_per_month
end
private
def fetch_graph
@log = @project.repository.graph_log.to_json
@success = true
rescue => ex
@commits = @project.repository.commits(nil, nil, 6000, 0, true)
@log = []
@success = false
@commits.each do |commit|
@log << {
author_name: commit.author_name.force_encoding('UTF-8'),
author_email: commit.author_email.force_encoding('UTF-8'),
date: commit.committed_date.strftime("%Y-%m-%d")
}
end
render json: @log.to_json
end
end
......@@ -28,7 +28,7 @@ class Projects::IssuesController < Projects::ApplicationController
@milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
sort_param = params[:sort] || 'newest'
@sort = sort_param.humanize unless sort_param.empty?
@assignees = User.where(id: @project.issues.pluck(:assignee_id))
@assignees = User.where(id: @project.issues.pluck(:assignee_id)).active
respond_to do |format|
format.html
......
......@@ -143,7 +143,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def update_branches
@target_project = selected_target_project
@target_branches = @target_project.repository.branch_names
@target_branches
respond_to do |format|
format.js
......
......@@ -13,7 +13,7 @@ class Projects::NewTreeController < Projects::BaseTreeController
flash[:notice] = "Your changes have been successfully committed"
redirect_to project_blob_path(@project, File.join(@ref, file_path))
else
flash[:alert] = result[:error]
flash[:alert] = result[:message]
render :show
end
end
......
......@@ -4,11 +4,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
before_filter :authorize_code_access!
before_filter :require_non_empty_project
def stats
@stats = Gitlab::Git::Stats.new(@repository.raw, @repository.root_ref)
@graph = @stats.graph
end
def archive
unless can?(current_user, :download_code, @project)
render_404 and return
......
......@@ -13,9 +13,8 @@ class Projects::TagsController < Projects::ApplicationController
end
def create
result = CreateTagService.new.execute(@project, params[:tag_name],
params[:ref], params[:message],
current_user)
result = CreateTagService.new(@project, current_user).
execute(params[:tag_name], params[:ref], params[:message])
if result[:status] == :success
@tag = result[:tag]
redirect_to project_tags_path(@project)
......
......@@ -6,18 +6,18 @@ class Projects::TeamMembersController < Projects::ApplicationController
def index
@group = @project.group
@users_projects = @project.users_projects.order('project_access DESC')
@project_members = @project.project_members.order('access_level DESC')
@project_group_links = @project.project_group_links
end
def new
@user_project_relation = project.users_projects.new
@user_project_relation = project.project_members.new
end
def create
users = User.where(id: params[:user_ids].split(','))
@project.team << [users, params[:project_access]]
@project.team << [users, params[:access_level]]
if params[:redirect_to]
redirect_to params[:redirect_to]
......@@ -27,7 +27,7 @@ class Projects::TeamMembersController < Projects::ApplicationController
end
def update
@user_project_relation = project.users_projects.find_by(user_id: member)
@user_project_relation = project.project_members.find_by(user_id: member)
@user_project_relation.update_attributes(member_params)
unless @user_project_relation.valid?
......@@ -37,7 +37,7 @@ class Projects::TeamMembersController < Projects::ApplicationController
end
def destroy
@user_project_relation = project.users_projects.find_by(user_id: member)
@user_project_relation = project.project_members.find_by(user_id: member)
@user_project_relation.destroy
respond_to do |format|
......@@ -47,7 +47,7 @@ class Projects::TeamMembersController < Projects::ApplicationController
end
def leave
project.users_projects.find_by(user_id: current_user).destroy
project.project_members.find_by(user_id: current_user).destroy
respond_to do |format|
format.html { redirect_to :back }
......@@ -70,6 +70,6 @@ class Projects::TeamMembersController < Projects::ApplicationController
end
def member_params
params.require(:team_member).permit(:user_id, :project_access)
params.require(:project_member).permit(:user_id, :access_level)
end
end
......@@ -19,10 +19,8 @@ class ProjectsFinder
# Return ALL group projects
group.projects
else
projects_members = UsersProject.where(
project_id: group.projects,
user_id: current_user
)
projects_members = ProjectMember.in_projects(group.projects).
with_user(current_user)
if projects_members.any?
# User is a project member
......@@ -32,7 +30,11 @@ class ProjectsFinder
# internal projects
# joined projects
#
projects_ids = projects_members.pluck(:project_id)
group.projects.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
projects_members.pluck(:source_id),
Project.public_and_internal_levels
)
else
# User has no access to group or group projects
# or has access through shared project
......@@ -41,14 +43,12 @@ class ProjectsFinder
# public projects
# internal projects
# shared projects
projects_ids = []
ProjectGroupLink.where(project_id: group.projects).each do |shared_project|
if shared_project.group.users.include?(current_user) || shared_project.project.users.include?(current_user)
projects_ids << shared_project.project.id
end
end
end
group.projects.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
......@@ -56,6 +56,7 @@ class ProjectsFinder
Project.public_and_internal_levels
)
end
end
else
# Not authenticated
#
......
......@@ -151,12 +151,6 @@ module ApplicationHelper
sanitize(str, tags: %w(a span))
end
def image_url(source)
# prevent relative_root_path being added twice (it's part of root_url and path_to_image)
root_url.sub(/#{root_path}$/, path_to_image(source))
end
alias_method :url_to_image, :image_url
def body_data_page
path = controller.controller_path.split('/')
......@@ -187,13 +181,6 @@ module ApplicationHelper
end
end
def first_line(str)
lines = str.split("\n")
line = lines.first
line += "..." if lines.size > 1
line
end
def broadcast_message
BroadcastMessage.current
end
......
......@@ -136,7 +136,7 @@ module EventsHelper
end
def event_note(text)
text = first_line(text)
text = first_line_in_markdown(text)
text = truncate(text, length: 150)
sanitize(markdown(text), tags: %w(a img b pre p))
end
......
......@@ -51,6 +51,14 @@ module GitlabMarkdownHelper
@markdown.render(text).html_safe
end
def first_line_in_markdown(text)
line = text.split("\n").detect do |i|
i.present? && markdown(i).present?
end
line += '...' unless line.nil?
line
end
def render_wiki_content(wiki_page)
if wiki_page.format == :markdown
markdown(wiki_page.content)
......@@ -65,7 +73,12 @@ module GitlabMarkdownHelper
paths.uniq.each do |file_path|
# If project does not have repository
# its nothing to rebuild
if @repository.exists? && !@repository.empty?
#
# TODO: pass project variable to markdown helper instead of using
# instance variable. Right now it generates invalid path for pages out
# of project scope. Example: search results where can be rendered markdown
# from different projects
if @repository && @repository.exists? && !@repository.empty?
new_path = rebuild_path(file_path)
# Finds quoted path so we don't replace other mentions of the string
# eg. "doc/api" will be replaced and "/home/doc/api/text" won't
......
......@@ -52,8 +52,8 @@ module NotesHelper
discussion_id: discussion_id
}
link_to "", "javascript:;", class: "add-diff-note js-add-diff-note-button",
data: data, title: "Add a comment to this line"
button_tag '', class: 'btn add-diff-note js-add-diff-note-button',
data: data, title: 'Add a comment to this line'
end
def link_to_reply_diff(note)
......@@ -67,11 +67,10 @@ module NotesHelper
discussion_id: note.discussion_id
}
link_to "javascript:;", class: "btn reply-btn js-discussion-reply-button",
data: data, title: "Add a reply" do
link_text = ""
link_text < content_tag(:i, nil, class: 'icon-comment')
link_text << "Reply"
button_tag class: 'btn reply-btn js-discussion-reply-button',
data: data, title: 'Add a reply' do
link_text = content_tag(:i, nil, class: 'icon-comment')
link_text << ' Reply'
end
end
end
......@@ -156,6 +156,14 @@ module ProjectsHelper
end
end
def link_to_toggle_fork
out = content_tag(:i, '', class: 'icon-code-fork')
out << ' Fork'
out << content_tag(:span, class: 'count') do
@project.forks_count.to_s
end
end
private
def get_project_nav_tabs(project, current_user)
......
......@@ -80,7 +80,8 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5)
ProjectsFinder.new.execute(current_user).search_by_title(term).non_archived.limit(limit).map do |p|
ProjectsFinder.new.execute(current_user).search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p|
{
label: "project: #{search_result_sanitize(p.name_with_namespace)}",
url: project_path(p)
......
module Emails
module Groups
def group_access_granted_email(user_group_id)
@membership = UsersGroup.find(user_group_id)
@membership = GroupMember.find(user_group_id)
@group = @membership.group
@target_url = group_url(@group)
mail(to: @membership.user.email,
......
module Emails
module Projects
def project_access_granted_email(user_project_id)
@users_project = UsersProject.find user_project_id
@project = @users_project.project
@project_member = ProjectMember.find user_project_id
@project = @project_member.project
@target_url = project_url(@project)
mail(to: @users_project.user.email,
mail(to: @project_member.user.email,
subject: subject("Access to project was granted"))
end
......
......@@ -14,7 +14,7 @@ class Ability
when "MergeRequest" then merge_request_abilities(user, subject)
when "Group" then group_abilities(user, subject)
when "Namespace" then namespace_abilities(user, subject)
when "UsersGroup" then users_group_abilities(user, subject)
when "GroupMember" then users_group_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
......
......@@ -108,4 +108,8 @@ class Commit
super
end
def parents
@parents ||= Commit.decorate(super)
end
end
# == Notifiable concern
#
# Contains notification functionality shared between UsersProject and UsersGroup
# Contains notification functionality
#
module Notifiable
extend ActiveSupport::Concern
......
......@@ -17,8 +17,8 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Group < Namespace
has_many :users_groups, dependent: :destroy
has_many :users, through: :users_groups
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
has_many :users, through: :group_members
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy
......@@ -33,22 +33,22 @@ class Group < Namespace
end
def owners
@owners ||= users_groups.owners.map(&:user)
@owners ||= group_members.owners.map(&:user)
end
def add_users(user_ids, group_access)
def add_users(user_ids, access_level)
user_ids.compact.each do |user_id|
user = self.users_groups.find_or_initialize_by(user_id: user_id)
user.update_attributes(group_access: group_access)
user = self.group_members.find_or_initialize_by(user_id: user_id)
user.update_attributes(access_level: access_level)
end
end
def add_user(user, group_access)
self.users_groups.create(user_id: user.id, group_access: group_access)
def add_user(user, access_level)
self.group_members.create(user_id: user.id, access_level: access_level)
end
def add_owner(user)
self.add_user(user, UsersGroup::OWNER)
self.add_user(user, Gitlab::Access::OWNER)
end
def has_owner?(user)
......@@ -64,7 +64,7 @@ class Group < Namespace
end
def members
users_groups
group_members
end
def avatar_type
......
......@@ -65,4 +65,9 @@ class Issue < ActiveRecord::Base
def reset_events_cache
Event.reset_event_cache_for(self)
end
# To allow polymorphism with MergeRequest.
def source_project
project
end
end
class Member < ActiveRecord::Base
include Notifiable
include Gitlab::Access
belongs_to :user
belongs_to :source, polymorphic: true
validates :user, presence: true
validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
delegate :name, :username, :email, to: :user, prefix: true
end
class GroupMember < Member
SOURCE_TYPE = 'Namespace'
belongs_to :group, class_name: 'Group', foreign_key: 'source_id'
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
default_value_for :notification_level, Notification::N_GLOBAL
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
scope :with_ldap_dn, -> { references(:user).includes(:user).
where(users: { provider: 'ldap' }) }
after_create :notify_create
after_update :notify_update
def self.access_level_roles
Gitlab::Access.options_with_owner
end
def group
source
end
def access_field
access_level
end
def notify_create
notification_service.new_group_member(self)
end
def notify_update
if access_level_changed?
notification_service.update_group_member(self)
end
end
def notification_service
NotificationService.new
end
end
# == Schema Information
#
# Table name: users_projects
#
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# project_access :integer default(0), not null
# notification_level :integer default(3), not null
#
class UsersProject < ActiveRecord::Base
include Gitlab::ShellAdapter
include Notifiable
include Gitlab::Access
belongs_to :user
belongs_to :project
class ProjectMember < Member
SOURCE_TYPE = 'Project'
validates :user, presence: true
validates :user_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_access, inclusion: { in: Gitlab::Access.values }, presence: true
validates :project, presence: true
include Gitlab::ShellAdapter
delegate :name, :username, :email, to: :user, prefix: true
belongs_to :project, class_name: 'Project', foreign_key: 'source_id'
scope :guests, -> { where(project_access: GUEST) }
scope :reporters, -> { where(project_access: REPORTER) }
scope :developers, -> { where(project_access: DEVELOPER) }
scope :masters, -> { where(project_access: MASTER) }
scope :in_project, ->(project) { where(project_id: project.id) }
scope :in_projects, ->(projects) { where(project_id: projects.map { |p| p.id }) }
scope :with_user, ->(user) { where(user_id: user.id) }
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
default_value_for :notification_level, Notification::N_GLOBAL
validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) }
after_create :post_create_hook
after_update :post_update_hook
after_destroy :post_destroy_hook
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
scope :with_user, ->(user) { where(user_id: user.id) }
class << self
# Add users to project teams with passed access option
......@@ -50,7 +31,7 @@ class UsersProject < ActiveRecord::Base
# add_users_into_projects(
# project_ids,
# user_ids,
# UsersProject::MASTER
# ProjectMember::MASTER
# )
#
# add_users_into_projects(
......@@ -60,7 +41,7 @@ class UsersProject < ActiveRecord::Base
# )
#
def add_users_into_projects(project_ids, user_ids, access)
project_access = if roles_hash.has_key?(access)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
access
......@@ -68,12 +49,12 @@ class UsersProject < ActiveRecord::Base
raise "Non valid access"
end
UsersProject.transaction do
ProjectMember.transaction do
project_ids.each do |project_id|
user_ids.each do |user_id|
users_project = UsersProject.new(project_access: project_access, user_id: user_id)
users_project.project_id = project_id
users_project.save
member = ProjectMember.new(access_level: access_level, user_id: user_id)
member.source_id = project_id
member.save
end
end
end
......@@ -84,10 +65,10 @@ class UsersProject < ActiveRecord::Base
end
def truncate_teams(project_ids)
UsersProject.transaction do
users_projects = UsersProject.where(project_id: project_ids)
users_projects.each do |users_project|
users_project.destroy
ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids)
members.each do |member|
member.destroy
end
end
......@@ -110,7 +91,7 @@ class UsersProject < ActiveRecord::Base
end
def access_field
project_access
access_level
end
def owner?
......@@ -129,7 +110,7 @@ class UsersProject < ActiveRecord::Base
end
def post_update_hook
notification_service.update_team_member(self) if self.project_access_changed?
notification_service.update_team_member(self) if self.access_level_changed?
end
def post_destroy_hook
......@@ -149,4 +130,8 @@ class UsersProject < ActiveRecord::Base
def system_hook_service
SystemHooksService.new
end
def project
source
end
end
......@@ -122,9 +122,11 @@ class MergeRequest < ActiveRecord::Base
if opened? || reopened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
errors.add :base, "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
errors.add :validate_branches,
"Cannot Create: This merge request already exists: #{
similar_mrs.pluck(:title)
}"
end
end
end
......@@ -140,7 +142,8 @@ class MergeRequest < ActiveRecord::Base
if source_project.forked_from?(target_project)
true
else
errors.add :base, "Source project is not a fork of target project"
errors.add :validate_fork,
'Source project is not a fork of target project'
end
end
end
......
......@@ -83,8 +83,8 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet"
has_many :hooks, dependent: :destroy, class_name: "ProjectHook"
has_many :protected_branches, dependent: :destroy
has_many :users_projects, dependent: :destroy
has_many :users, through: :users_projects
has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
......@@ -129,6 +129,7 @@ class Project < ActiveRecord::Base
scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) }
scope :in_group_namespace, -> { joins(:group) }
scope :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") }
scope :sorted_by_stars, -> { reorder("projects.star_count DESC") }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) }
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
......@@ -368,12 +369,12 @@ class Project < ActiveRecord::Base
def team_member_by_name_or_email(name = nil, email = nil)
user = users.where("name like ? or email like ?", name, email).first
users_projects.where(user: user) if user
project_members.where(user: user) if user
end
# Get Team Member record by user id
def team_member_by_id(user_id)
users_projects.find_by(user_id: user_id)
project_members.find_by(user_id: user_id)
end
def name_with_namespace
......@@ -437,15 +438,19 @@ class Project < ActiveRecord::Base
end
# Add comment about pushing new commits to merge requests
mrs = self.merge_requests.opened.where(source_branch: branch_name).to_a
comment_mr_with_commits(branch_name, commits, user)
true
end
def comment_mr_with_commits(branch_name, commits, user)
mrs = self.origin_merge_requests.opened.where(source_branch: branch_name).to_a
mrs += self.fork_merge_requests.opened.where(source_branch: branch_name).to_a
mrs.uniq.each do |merge_request|
Note.create_new_commits_note(merge_request, merge_request.project,
user, commits)
end
true
end
def valid_repo?
......@@ -570,7 +575,7 @@ class Project < ActiveRecord::Base
end
def project_member(user)
users_projects.where(user_id: user).first
project_members.where(user_id: user).first
end
def default_branch
......@@ -614,4 +619,8 @@ class Project < ActiveRecord::Base
def find_label(name)
labels.find_by(name: name)
end
def origin_merge_requests
merge_requests.where(source_project_id: self.id)
end
end
......@@ -32,12 +32,12 @@ class ProjectTeam
end
def find_tm(user_id)
tm = project.users_projects.find_by(user_id: user_id)
tm = project.project_members.find_by(user_id: user_id)
# If user is not in project members
# we should check for group membership
if group && !tm
tm = group.users_groups.find_by(user_id: user_id)
tm = group.group_members.find_by(user_id: user_id)
end
tm
......@@ -52,7 +52,7 @@ class ProjectTeam
end
def add_users_ids(user_ids, access)
UsersProject.add_users_into_projects(
ProjectMember.add_users_into_projects(
[project.id],
user_ids,
access
......@@ -61,7 +61,7 @@ class ProjectTeam
# Remove all users from project team
def truncate
UsersProject.truncate_team(project)
ProjectMember.truncate_team(project)
end
def users
......@@ -91,8 +91,8 @@ class ProjectTeam
def import(source_project)
target_project = project
source_team = source_project.users_projects.to_a
target_user_ids = target_project.users_projects.pluck(:user_id)
source_team = source_project.project_members.to_a
target_user_ids = target_project.project_members.pluck(:user_id)
source_team.reject! do |tm|
# Skip if user already present in team
......@@ -102,11 +102,11 @@ class ProjectTeam
source_team.map! do |tm|
new_tm = tm.dup
new_tm.id = nil
new_tm.project_id = target_project.id
new_tm.source = target_project
new_tm
end
UsersProject.transaction do
ProjectMember.transaction do
source_team.each do |tm|
tm.save
end
......@@ -135,10 +135,10 @@ class ProjectTeam
def max_tm_access(user_id)
access = []
access << project.users_projects.find_by(user_id: user_id).try(:access_field)
access << project.project_members.find_by(user_id: user_id).try(:access_field)
if group
access << group.users_groups.find_by(user_id: user_id).try(:access_field)
access << group.group_members.find_by(user_id: user_id).try(:access_field)
end
if project.invited_groups.any?
......@@ -167,8 +167,8 @@ class ProjectTeam
private
def fetch_members(level = nil)
project_members = project.users_projects
group_members = group ? group.users_groups : []
project_members = project.project_members
group_members = group ? group.group_members : []
invited_members = []
if project.invited_groups.any?
......
......@@ -107,6 +107,18 @@ class ProjectWiki
[title.gsub(/\.[^.]*$/, ""), title_array.join("/")]
end
def search_files(query)
repository.search_files(query, default_branch)
end
def repository
Repository.new(path_with_namespace, default_branch)
end
def default_branch
wiki.class.default_ref
end
private
def create_repo!
......
......@@ -25,14 +25,14 @@ class Repository
raw_repository.empty?
end
def commit(id = nil)
def commit(id = 'HEAD')
return nil unless raw_repository
commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit) if commit
commit
end
def commits(ref, path = nil, limit = nil, offset = nil)
def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
commits = Gitlab::Git::Commit.where(
repo: raw_repository,
ref: ref,
......@@ -137,8 +137,18 @@ class Repository
def graph_log
Rails.cache.fetch(cache_key(:graph_log)) do
stats = Gitlab::Git::GitStats.new(raw, root_ref, Gitlab.config.git.timeout)
stats.parsed_log
commits = raw_repository.log(limit: 6000, skip_merges: true,
ref: root_ref)
commits.map do |rugged_commit|
commit = Gitlab::Git::Commit.new(rugged_commit)
{
author_name: commit.author_name.force_encoding('UTF-8'),
author_email: commit.author_email.force_encoding('UTF-8'),
additions: commit.stats.additions,
deletions: commit.stats.deletions
}
end
end
end
......@@ -223,12 +233,15 @@ class Repository
end
def last_commit_for_path(sha, path)
commits(sha, path, 1).last
args = %W(git rev-list --max-count 1 #{sha} -- #{path})
sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
commit(sha)
end
# Remove archives older than 2 hours
def clean_old_archives
Gitlab::Popen.popen(%W(find #{Gitlab.config.gitlab.repository_downloads_path} -mmin +120 -delete))
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
def branches_sorted_by(value)
......@@ -247,20 +260,18 @@ class Repository
end
def contributors
log = graph_log.group_by { |i| i[:author_email] }
commits = self.commits(nil, nil, 2000, 0, true)
log.map do |email, contributions|
commits.group_by(&:author_email).map do |email, commits|
contributor = Gitlab::Contributor.new
contributor.email = email
contributions.each do |contribution|
commits.each do |commit|
if contributor.name.blank?
contributor.name = contribution[:author_name]
contributor.name = commit.author_name
end
contributor.commits += 1
contributor.additions += contribution[:additions] || 0
contributor.deletions += contribution[:deletions] || 0
end
contributor
......@@ -282,4 +293,21 @@ class Repository
blob_at(commit.parent_id, diff.old_path)
end
end
def branch_names_contains(sha)
args = %W(git branch --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first
if names.respond_to?(:split)
names = names.split("\n").map(&:strip)
names.each do |name|
name.slice! '* '
end
names
else
[]
end
end
end
......@@ -81,21 +81,23 @@ class User < ActiveRecord::Base
has_many :emails, dependent: :destroy
# Groups
has_many :users_groups, dependent: :destroy
has_many :groups, through: :users_groups
has_many :owned_groups, -> { where users_groups: { group_access: UsersGroup::OWNER } }, through: :users_groups, source: :group
has_many :masters_groups, -> { where users_groups: { group_access: UsersGroup::MASTER } }, through: :users_groups, source: :group
has_many :members, dependent: :destroy
has_many :project_members, source: 'ProjectMember'
has_many :group_members, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :projects, through: :users_projects
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
has_many :users_projects, dependent: :destroy
has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
......@@ -140,7 +142,7 @@ class User < ActiveRecord::Base
state_machine :state, initial: :active do
after_transition any => :blocked do |user, transition|
# Remove user from all projects and
user.users_projects.find_each do |membership|
user.project_members.find_each do |membership|
# skip owned resources
next if membership.project.owner == user
......@@ -148,7 +150,7 @@ class User < ActiveRecord::Base
end
# Remove user from all groups
user.users_groups.find_each do |membership|
user.group_members.find_each do |membership|
# skip owned resources
next if membership.group.last_owner?(user)
......@@ -175,7 +177,7 @@ class User < ActiveRecord::Base
scope :in_team, ->(team){ where(id: team.member_ids) }
scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM users_projects)') }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
scope :ldap, -> { where(provider: 'ldap') }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
......@@ -297,7 +299,7 @@ class User < ActiveRecord::Base
# Team membership in authorized projects
def tm_in_authorized_projects
UsersProject.where(project_id: authorized_projects.map(&:id), user_id: self.id)
ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id)
end
def is_admin?
......
# == Schema Information
#
# Table name: users_groups
#
# id :integer not null, primary key
# group_access :integer not null
# group_id :integer not null
# user_id :integer not null
# created_at :datetime
# updated_at :datetime
# notification_level :integer default(3), not null
#
class UsersGroup < ActiveRecord::Base
include Notifiable
include Gitlab::Access
def self.group_access_roles
Gitlab::Access.options_with_owner
end
belongs_to :user
belongs_to :group
scope :guests, -> { where(group_access: GUEST) }
scope :reporters, -> { where(group_access: REPORTER) }
scope :developers, -> { where(group_access: DEVELOPER) }
scope :masters, -> { where(group_access: MASTER) }
scope :owners, -> { where(group_access: OWNER) }
scope :with_ldap_dn, -> { references(:user).includes(:user).
where(users: { provider: 'ldap' }) }
scope :with_group, ->(group) { where(group_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
after_create :notify_create
after_update :notify_update
validates :group_access, inclusion: { in: UsersGroup.group_access_roles.values }, presence: true
validates :user_id, presence: true
validates :group_id, presence: true
validates :user_id, uniqueness: { scope: [:group_id], message: "already exists in group" }
delegate :name, :username, :email, to: :user, prefix: true
def access_field
group_access
end
def notify_create
notification_service.new_group_member(self)
end
def notify_update
if group_access_changed?
notification_service.update_group_member(self)
end
end
def notification_service
NotificationService.new
end
end
......@@ -87,14 +87,14 @@ class WikiPage
def version
return nil unless persisted?
@version ||= Commit.new(Gitlab::Git::Commit.new(@page.version))
@version ||= @page.version
end
# Returns an array of Gitlab Commit instances.
def versions
return [] unless persisted?
@page.versions.map { |v| Commit.new(Gitlab::Git::Commit.new(v)) }
@page.versions
end
def commit
......
class BaseService
attr_accessor :project, :current_user, :params
def initialize(project, user, params)
def initialize(project, user, params = {})
@project, @current_user, @params = project, user, params.dup
end
......@@ -32,4 +32,19 @@ class BaseService
def system_hook_service
SystemHooksService.new
end
private
def error(message)
{
message: message,
status: :error
}
end
def success
{
status: :success
}
end
end
class CreateBranchService
def execute(project, branch_name, ref, current_user)
require_relative 'base_service'
class CreateBranchService < BaseService
def execute(branch_name, ref)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
if valid_branch == false
return error('Branch name invalid')
......@@ -22,17 +24,9 @@ class CreateBranchService
end
end
def error(message)
{
message: message,
status: :error
}
end
def success(branch)
{
branch: branch,
status: :success
}
out = super()
out[:branch] = branch
out
end
end
class CreateTagService
def execute(project, tag_name, ref, message, current_user)
require_relative 'base_service'
class CreateTagService < BaseService
def execute(tag_name, ref, message)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
if valid_tag == false
return error('Tag name invalid')
......@@ -26,17 +28,9 @@ class CreateTagService
end
end
def error(message)
{
message: message,
status: :error
}
end
def success(branch)
{
tag: branch,
status: :success
}
out = super()
out[:tag] = branch
out
end
end
class DeleteBranchService
def execute(project, branch_name, current_user)
require_relative 'base_service'
class DeleteBranchService < BaseService
def execute(branch_name)
repository = project.repository
branch = repository.find_branch(branch_name)
......@@ -31,17 +33,14 @@ class DeleteBranchService
end
def error(message, return_code = 400)
{
message: message,
return_code: return_code,
state: :error
}
out = super(message)
out[:return_code] = return_code
out
end
def success(message)
{
message: message,
state: :success
}
out = super()
out[:message] = message
out
end
end
......@@ -10,18 +10,10 @@ module Files
private
def error(message)
{
error: message,
status: :error
}
end
def success
{
error: '',
status: :success
}
out = super()
out[:error] = ''
out
end
def repository
......
......@@ -17,40 +17,39 @@ class GitPushService
def execute(project, user, oldrev, newrev, ref)
@project, @user = project, user
# Collect data for this git push
@push_commits = project.repository.commits_between(oldrev, newrev)
@push_data = post_receive_data(oldrev, newrev, ref)
create_push_event
project.ensure_satellite_exists
project.repository.expire_cache
project.update_repository_size
if push_to_existing_branch?(ref, oldrev)
project.update_merge_requests(oldrev, newrev, ref, @user)
process_commit_messages(ref)
end
if push_to_branch?(ref)
project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup)
end
if push_to_new_branch?(ref, oldrev)
if push_remove_branch?(ref, newrev)
@push_commits = []
elsif push_to_new_branch?(ref, oldrev)
# Re-find the pushed commits.
if is_default_branch?(ref)
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
@push_commits = project.repository.commits(newrev)
# Default branch is protected by default
project.protected_branches.create({ name: project.default_branch })
else
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually pushed, but
# that shouldn't matter because we check for existing cross-references later.
@push_commits = project.repository.commits_between(project.default_branch, newrev)
end
process_commit_messages(ref)
elsif push_to_existing_branch?(ref, oldrev)
# Collect data for this git push
@push_commits = project.repository.commits_between(oldrev, newrev)
project.update_merge_requests(oldrev, newrev, ref, @user)
process_commit_messages(ref)
end
@push_data = post_receive_data(oldrev, newrev, ref)
create_push_event(@push_data)
project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup)
end
end
# This method provide a sample data
......@@ -65,7 +64,7 @@ class GitPushService
protected
def create_push_event
def create_push_event(push_data)
Event.create!(
project: project,
action: Event::PUSHED,
......@@ -183,6 +182,12 @@ class GitPushService
ref_parts[1] =~ /heads/ && oldrev == "0000000000000000000000000000000000000000"
end
def push_remove_branch? ref, newrev
ref_parts = ref.split('/')
ref_parts[1] =~ /heads/ && newrev == "0000000000000000000000000000000000000000"
end
def push_to_branch? ref
ref =~ /refs\/heads/
end
......
......@@ -157,12 +157,12 @@ class NotificationService
end
end
def new_team_member(users_project)
mailer.project_access_granted_email(users_project.id)
def new_team_member(project_member)
mailer.project_access_granted_email(project_member.id)
end
def update_team_member(users_project)
mailer.project_access_granted_email(users_project.id)
def update_team_member(project_member)
mailer.project_access_granted_email(project_member.id)
end
def new_group_member(users_group)
......@@ -186,20 +186,20 @@ class NotificationService
# Get project users with WATCH notification level
def project_watchers(project)
project_members = users_project_notification(project)
project_members = project_member_notification(project)
users_with_project_level_global = users_project_notification(project, Notification::N_GLOBAL)
users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL)
users_with_group_level_global = users_group_notification(project, Notification::N_GLOBAL)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_users_project_setting(project, users_with_project_level_global, users)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
users_with_group_setting = select_users_group_setting(project, project_members, users_with_group_level_global, users)
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
def users_project_notification(project, notification_level=nil)
project_members = project.users_projects
def project_member_notification(project, notification_level=nil)
project_members = project.project_members
if notification_level
project_members.where(notification_level: notification_level).pluck(:user_id)
......@@ -210,7 +210,7 @@ class NotificationService
def users_group_notification(project, notification_level)
if project.group
project.group.users_groups.where(notification_level: notification_level).pluck(:user_id)
project.group.group_members.where(notification_level: notification_level).pluck(:user_id)
else
[]
end
......@@ -224,8 +224,8 @@ class NotificationService
end
# Build a list of users based on project notifcation settings
def select_users_project_setting(project, global_setting, users_global_level_watch)
users = users_project_notification(project, Notification::N_WATCH)
def select_project_member_setting(project, global_setting, users_global_level_watch)
users = project_member_notification(project, Notification::N_WATCH)
# If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id|
......@@ -267,10 +267,10 @@ class NotificationService
users.reject do |user|
next user.notification.disabled? unless project
tm = project.users_projects.find_by(user_id: user.id)
tm = project.project_members.find_by(user_id: user.id)
if !tm && project.group
tm = project.group.users_groups.find_by(user_id: user.id)
tm = project.group.group_members.find_by(user_id: user.id)
end
# reject users who globally disabled notification and has no membership
......
......@@ -42,10 +42,7 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
unless @project.group
@project.users_projects.create(
project_access: UsersProject::MASTER,
user: current_user
)
@project.team << [current_user, :master]
end
@project.update_column(:last_activity_at, @project.created_at)
......
......@@ -27,7 +27,7 @@ module Projects
#First save the DB entries as they can be rolled back if the repo fork fails
project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id)
if project.save
project.users_projects.create(project_access: UsersProject::MASTER, user: current_user)
project.team << [current_user, :master]
end
#Now fork the repo
unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path)
......
......@@ -50,14 +50,14 @@ class SystemHooksService
email: model.email,
user_id: model.id
})
when UsersProject
when ProjectMember
data.merge!({
project_name: model.project.name,
project_path: model.project.path,
project_id: model.project_id,
project_id: model.project.id,
user_name: model.user.name,
user_email: model.user.email,
project_access: model.human_access,
access_level: model.human_access,
project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
})
end
......@@ -65,7 +65,7 @@ class SystemHooksService
def build_event_name(model, event)
case model
when UsersProject
when ProjectMember
return "user_add_to_team" if event == :create
return "user_remove_from_team" if event == :destroy
else
......
......@@ -89,7 +89,7 @@
%div
= users_select_tag(:user_ids, multiple: true)
%div.prepend-top-10
= select_tag :group_access, options_for_select(UsersGroup.group_access_roles), class: "project-access-select select2"
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= submit_tag 'Add users into group', class: "btn btn-create"
.panel.panel-default
......@@ -97,7 +97,7 @@
%h3.panel-title
Members
%span.badge
#{@group.users_groups.count}
#{@group.group_members.count}
%ul.well-list.group-users-list
- @members.each do |member|
- user = member.user
......@@ -107,7 +107,7 @@
= link_to user.name, admin_user_path(user)
%span.pull-right.light
= member.human_access
= link_to group_users_group_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
= link_to group_group_members_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
%i.icon-minus.icon-white
.panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
......@@ -95,13 +95,13 @@
.panel.panel-default
.panel-heading
%strong #{@group.name}
group members (#{@group.users_groups.count})
group members (#{@group.group_members.count})
.pull-right
= link_to admin_group_path(@group), class: 'btn btn-small' do
%i.icon-edit
%ul.well-list
- @group_members.each do |member|
= render 'users_groups/users_group', member: member, show_controls: false
= render 'groups/group_members/group_member', member: member, show_controls: false
.panel-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
......@@ -115,17 +115,17 @@
%i.icon-edit
Manage Access
%ul.well-list.team_members
- @project_members.each do |users_project|
- user = users_project.user
%li.users_project
- @project_members.each do |project_member|
- user = project_member.user
%li.project_member
.list-item-name
%strong
= link_to user.name, admin_user_path(user)
.pull-right
- if users_project.owner?
- if project_member.owner?
%span.light Owner
- else
%span.light= users_project.human_access
%span.light= project_member.human_access
= link_to project_team_member_path(@project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-small btn-remove" do
%i.icon-remove
.panel-footer
......
......@@ -22,7 +22,7 @@
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control'
= button_tag type: 'submit', class: 'btn btn-primary' do
= button_tag class: 'btn btn-primary' do
%i.icon-search
%hr
= link_to 'Reset', admin_users_path, class: "btn btn-cancel"
......
......@@ -159,13 +159,13 @@
= render 'users/profile', user: @user
#groups.tab-pane
- if @user.users_groups.present?
- if @user.group_members.present?
.panel.panel-default
.panel-heading Groups:
%ul.well-list
- @user.users_groups.each do |user_group|
- @user.group_members.each do |user_group|
- group = user_group.group
%li.users_group
%li.group_member
%span{class: ("list-item-name" unless user_group.owner?)}
%strong= link_to group.name, admin_group_path(group)
.pull-right
......@@ -197,7 +197,7 @@
%ul.well-list
- @joined_projects.sort_by(&:name_with_namespace).each do |project|
- tm = project.team.find_tm(@user.id)
%li.users_project
%li.project_member
.list-item-name
= link_to admin_project_path(project), class: dom_class(project) do
= project.name_with_namespace
......
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- if issue.description.present?
= markdown issue.description
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- if merge_request.description.present?
= markdown merge_request.description
= form_for @users_group, url: group_users_groups_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
= form_for @users_group, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
.form-group
= f.label :user_ids, "People", class: 'control-label'
.col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large')
.form-group
= f.label :group_access, "Group Access", class: 'control-label'
.col-sm-10= select_tag :group_access, options_for_select(UsersGroup.group_access_roles, @users_group.group_access), class: "project-access-select select2"
= f.label :access_level, "Group Access", class: 'control-label'
.col-sm-10= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @users_group.access_level), class: "project-access-select select2"
.form-actions
= f.submit 'Add users into group', class: "btn btn-create"
......@@ -21,11 +21,11 @@
= link_to leave_profile_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
%i.icon-minus.icon-white
- else
= link_to group_users_group_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
= link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
%i.icon-minus.icon-white
.edit-member.hide.js-toggle-content
= form_for [@group, member], remote: true do |f|
.alert.prepend-top-20
= f.select :group_access, options_for_select(UsersGroup.group_access_roles, member.group_access)
= f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level)
= f.submit 'Save', class: 'btn btn-save btn-small'
......@@ -49,7 +49,7 @@
(#{@members.total_count})
%ul.well-list
- @members.each do |member|
= render 'users_groups/users_group', member: member, show_roles: show_roles, show_controls: true
= render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true
= paginate @members, theme: 'gitlab'
:coffeescript
......
......@@ -18,8 +18,7 @@
= javascript_include_tag "application"
= csrf_meta_tags
= include_gon
:erb
<meta name="viewport" content="width=device-width, initial-scale=1.0">
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1.0'}
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
......
......@@ -4,10 +4,10 @@
%body{class: "#{app_theme} admin", :'data-page' => body_data_page}
= render "layouts/broadcast"
= render "layouts/head_panel", title: "Admin area"
= render "layouts/flash"
%nav.main-nav.navbar-collapse.collapse
.container= render 'layouts/nav/admin'
.container
.content= yield
.content
= render "layouts/flash"
= yield
= yield :embedded_scripts
......@@ -4,9 +4,9 @@
%body{class: "#{app_theme} application", :'data-page' => body_data_page }
= render "layouts/broadcast"
= render "layouts/head_panel", title: "Dashboard"
= render "layouts/flash"
%nav.main-nav.navbar-collapse.collapse
.container= render 'layouts/nav/dashboard'
.container
.content= yield
.content
= render "layouts/flash"
= yield
......@@ -2,7 +2,6 @@
%html{ lang: "en"}
= render "layouts/head"
%body.ui_basic.login-page
= render "layouts/flash"
.container
.content
.login-title
......@@ -10,6 +9,7 @@
%hr
.container
.content
= render "layouts/flash"
.row
.col-md-7.brand-holder
- if brand_item
......
......@@ -3,7 +3,7 @@
= render "layouts/head", title: "Error"
%body{class: "#{app_theme} application"}
= render "layouts/head_panel", title: "" if current_user
= render "layouts/flash"
.container.navless-container
= render "layouts/flash"
.error-page
= yield
......@@ -4,9 +4,9 @@
%body{class: "#{app_theme} application", :'data-page' => body_data_page}
= render "layouts/broadcast"
= render "layouts/head_panel", title: "group: #{@group.name}"
= render "layouts/flash"
%nav.main-nav.navbar-collapse.collapse
.container= render 'layouts/nav/group'
.container
.content= yield
.content
= render "layouts/flash"
= yield
......@@ -4,8 +4,7 @@
%body{class: "#{app_theme} application", :'data-page' => body_data_page}
= render "layouts/broadcast"
= render "layouts/head_panel", title: @title
= render "layouts/flash"
.container.navless-container
.content
= render "layouts/flash"
= yield
......@@ -4,9 +4,9 @@
%body{class: "#{app_theme} profile", :'data-page' => body_data_page}
= render "layouts/broadcast"
= render "layouts/head_panel", title: "Profile"
= render "layouts/flash"
%nav.main-nav.navbar-collapse.collapse
.container= render 'layouts/nav/profile'
.container
.content= yield
.content
= render "layouts/flash"
= yield
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment