Commit 90ce0be4 authored by Jacob Vosmaer's avatar Jacob Vosmaer

Merge branch 'master' of dev.gitlab.org:gitlab/gitlab-ee into feature-120-multiple-ldap-groups

Conflicts:
	app/models/group.rb
	app/views/groups/members.html.haml
	db/schema.rb
parents 5238efd8 a81d806b
......@@ -17,8 +17,8 @@ env:
- TASK=jasmine:ci DB=postgresql
before_install:
- sudo apt-get install libicu-dev -y
install:
- "bundle install --deployment --without production"
install:
- "travis_retry bundle install --deployment --without production --retry 5"
branches:
only:
- 'master'
......
v 7.2.0
- Explore page
- Add project stars (Ciro Santilli)
- Log Sidekiq arguments
- Better labels: colors, ability to rename and remove
- Improve the way merge request collects diffs
- Improve compare page for large diffs
- Expose the full commit message via API
- Fix 500 error on repository rename
- Fix bug when MR download patch return invalid diff
- Test gitlab-shell integration
- Repository import timeout increased from 2 to 4 minutes allowing larger repos to be imported
- API for labels (Robert Schilling)
- API: ability to set an import url when creating project for specific user
v 7.1.1
- Fix cpu usage issue in Firefox
- Fix redirect loop when changing password by new user
- Fix 500 error on new merge request page
v 7.1.0
- Remove observers
- Improve MR discussions
......@@ -8,7 +28,7 @@ v 7.1.0
- Dont show reply button if user is not signed in
- Expose more information for issues with webhook
- Add a mention of the merge request into the default merge request commit message
- Imrpove code highlight, introduce support for more languages like Go, Clojure, Erlang etc
- Improve code highlight, introduce support for more languages like Go, Clojure, Erlang etc
- Fix concurrency issue in repository download
- Dont allow repository name start with ?
- Improve email threading (Pierre de La Morinerie)
......@@ -53,7 +73,7 @@ v 7.0.0
- Show notice if your profile is public
- UI improvements for mobile devices
- Improve diff rendering performance
- Drag-n-drop for issues and merge requests between states at milestone page
- Drag-n-drop for issues and merge requests between states at milestone page
- Fix '0 commits' message for huge repositories on project home page
- Prevent 500 error page when visit commit page from large repo
- Add notice about huge push over http to unicorn config
......@@ -63,7 +83,7 @@ v 7.0.0
- Be more selective when killing stray Sidekiqs
- Check LDAP user filter during sign-in
- Remove wall feature (no data loss - you can take it from database)
- Dont expose user emails via API unless you are admin
- Dont expose user emails via API unless you are admin
- Detect issues closed by Merge Request description
- Better email subject lines from email on push service (Alex Elman)
- Enable identicon for gravatar be default
......@@ -141,7 +161,7 @@ v 6.7.0
- Blob and tree gfm links to anchors work
- Piwik Integration (Sebastian Winkler)
- Show contribution guide link for new issue form (Jeroen van Baarsen)
- Fix CI status for merge requests from fork
- Fix CI status for merge requests from fork
- Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
- New page load indicator that includes a spinner that scrolls with the page
- Converted all the help sections into markdown
......@@ -332,7 +352,7 @@ v 6.2.0
- Store the sessions in Redis instead of the cookie store
- Fixed relative links in markdown
- User must confirm their email if signup enabled
- User must confirm changed email
- User must confirm changed email
v 6.1.0
- Project specific IDs for issues, mr, milestones
......
v 7.3.0
- Add an option to change the LDAP sync time from default 1 hour
- User will receive an email when unsubscribed from admin notifications
v 7.2.0
- Improve Redmine integration
- Better logging for the JIRA issue closing service
- Administrators can now send email to all users through the admin interface
- JIRA issue transition ID is now customizable
- LDAP group settings are now visible in admin group show page and group members page
v 7.1.0
- Synchronize LDAP-enabled GitLab administrators with an LDAP group (Marvin Frick, sponsored by SinnerSchrader)
- Synchronize SSH keys with LDAP (Oleg Girko (Jolla) and Marvin Frick (SinnerSchrader))
- Support Jenkins jobs with multiple modules (Marvin Frick, sponsored by SinnerSchrader)
v 7.0.0
- Fix: empty brand images are displayed as empty image_tag on login page (Marvin Frick, sponsored by SinnerSchrader)
- Fix: empty brand images are displayed as empty image_tag on login page (Marvin Frick, sponsored by SinnerSchrader)
v 6.9.4
- Fix bug in JIRA Issue closing triggered by commit messages
......
......@@ -80,16 +80,20 @@ gem "six"
gem "seed-fu"
# Markdown to HTML
gem "redcarpet", "~> 2.2.2"
gem "github-markup"
gem "org-ruby" # For rendering .org files
# Required markup gems by github-markdown
gem 'redcarpet', '~> 2.2.2'
gem 'RedCloth'
gem 'rdoc', '~>3.6'
gem 'org-ruby'
gem 'creole', '~>0.3.6'
gem 'wikicloth', '=0.8.1'
gem 'asciidoctor', '= 0.1.4'
# Diffs
gem 'diffy', '~> 3.0.3'
# Asciidoc to HTML
gem "asciidoctor"
# Application server
group :unicorn do
gem "unicorn", '~> 4.6.3'
......@@ -175,6 +179,7 @@ gem "gitlab_emoji", "~> 0.0.1.1"
gem "gon", '~> 5.0.0'
gem 'nprogress-rails'
gem 'request_store'
gem "virtus"
group :development do
gem "annotate", "~> 2.6.0.beta2"
......
GEM
remote: https://rubygems.org/
specs:
RedCloth (4.2.9)
ace-rails-ap (2.0.1)
actionmailer (4.1.1)
actionpack (= 4.1.1)
......@@ -86,6 +87,7 @@ GEM
thor
crack (0.4.1)
safe_yaml (~> 0.9.0)
creole (0.3.8)
d3_rails (3.1.10)
railties (>= 3.1.0)
daemons (1.1.9)
......@@ -121,12 +123,13 @@ GEM
eventmachine (1.0.3)
excon (0.32.1)
execjs (2.0.2)
expression_parser (0.9.0)
factory_girl (4.3.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.3.0)
factory_girl (~> 4.3.0)
railties (>= 3.0.0)
faraday (0.8.8)
faraday (0.8.9)
multipart-post (~> 1.2.0)
faraday_middleware (0.9.0)
faraday (>= 0.7.4, < 0.9)
......@@ -155,7 +158,7 @@ GEM
dotenv (>= 0.7)
thor (>= 0.13.6)
formatador (0.2.4)
gemnasium-gitlab-service (0.2.1)
gemnasium-gitlab-service (0.2.2)
rugged (~> 0.19)
gherkin-ruby (0.3.1)
racc
......@@ -176,12 +179,12 @@ GEM
mime-types (~> 1.19)
gitlab_emoji (0.0.1.1)
emoji (~> 1.0.1)
gitlab_git (6.0.1)
gitlab_git (6.2.1)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-grit (~> 2.6)
gitlab-linguist (~> 3.0)
rugged (~> 0.19.0)
rugged (~> 0.21.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.0.4)
net-ldap (~> 0.3.1)
......@@ -231,7 +234,7 @@ GEM
activesupport (>= 4.0.1)
haml (>= 3.1, < 5.0)
railties (>= 4.0.1)
hashie (2.0.5)
hashie (2.1.2)
hike (1.2.3)
hipchat (0.14.0)
httparty
......@@ -240,8 +243,8 @@ GEM
httparty (0.13.0)
json (~> 1.8)
multi_xml (>= 0.5.2)
httpauth (0.2.0)
i18n (0.6.9)
httpauth (0.2.1)
i18n (0.6.11)
ice_nine (0.10.0)
jasmine (2.0.2)
jasmine-core (~> 2.0.0)
......@@ -261,7 +264,7 @@ GEM
jquery-ui-rails (4.2.1)
railties (>= 3.2.16)
json (1.8.1)
jwt (0.1.8)
jwt (0.1.13)
multi_json (>= 1.5)
kaminari (0.15.1)
actionpack (>= 3.0.0)
......@@ -308,9 +311,9 @@ GEM
omniauth-github (1.1.1)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-google-oauth2 (0.2.1)
omniauth (~> 1.0)
omniauth-oauth2
omniauth-google-oauth2 (0.2.5)
omniauth (> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-oauth (1.0.1)
oauth
omniauth (~> 1.0)
......@@ -320,8 +323,8 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
org-ruby (0.9.6)
rubypants (>= 0.2.0)
org-ruby (0.9.8)
rubypants (~> 0.2)
orm_adapter (0.5.0)
pg (0.15.1)
phantomjs (1.9.2.0)
......@@ -331,7 +334,7 @@ GEM
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
polyglot (0.3.4)
posix-spawn (0.3.8)
posix-spawn (0.3.9)
pry (0.9.12.4)
coderay (~> 1.0)
method_source (~> 0.8)
......@@ -413,6 +416,7 @@ GEM
require_all (1.3.2)
rest-client (1.6.7)
mime-types (>= 1.16)
rinku (1.7.3)
rouge (1.3.3)
rspec (2.14.1)
rspec-core (~> 2.14.0)
......@@ -432,7 +436,7 @@ GEM
ruby-progressbar (1.2.0)
rubyntlm (0.1.1)
rubypants (0.2.0)
rugged (0.19.0)
rugged (0.21.0)
safe_yaml (0.9.7)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
......@@ -536,7 +540,7 @@ GEM
eventmachine (>= 0.12.8)
http_parser.rb (~> 0.5.1)
simple_oauth (~> 0.1.4)
tzinfo (1.2.1)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.3.2)
execjs (>= 0.3.0)
......@@ -563,6 +567,10 @@ GEM
addressable (>= 2.2.7)
crack (>= 0.3.2)
websocket-driver (0.3.3)
wikicloth (0.8.1)
builder
expression_parser
rinku
xpath (2.0.0)
nokogiri (~> 1.3)
......@@ -570,10 +578,11 @@ PLATFORMS
ruby
DEPENDENCIES
RedCloth
ace-rails-ap
acts-as-taggable-on
annotate (~> 2.6.0.beta2)
asciidoctor
asciidoctor (= 0.1.4)
awesome_print
better_errors
binding_of_caller
......@@ -583,6 +592,7 @@ DEPENDENCIES
coffee-rails
colored
coveralls
creole (~> 0.3.6)
d3_rails (~> 3.1.4)
database_cleaner
default_value_for (~> 3.0.0)
......@@ -647,6 +657,7 @@ DEPENDENCIES
raphael-rails (~> 2.1.2)
rb-fsevent
rb-inotify
rdoc (~> 3.6)
redcarpet (~> 2.2.2)
redis-rails
request_store
......@@ -682,4 +693,6 @@ DEPENDENCIES
unicorn (~> 4.6.3)
unicorn-worker-killer
version_sorter
virtus
webmock
wikicloth (= 0.8.1)
......@@ -34,7 +34,7 @@ The most important thing is making sure valid issues receive feedback from the d
## Workflow labels
Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to reevaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
- *Awaiting feedback*: Feedback pending from the reporter
- *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away)
......@@ -61,7 +61,7 @@ If an issue is complex and needs the attention of a specific person, assignment
## Be kind
Be kind to people trying to contribute. Be aware that people can be a non-native or a native English speaker, they might not understand thing or they might be very sensitive to how your word things. Use emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to merge requests is in the [Thoughtbot code review guide](https://github.com/thoughtbot/guides/tree/master/code-review).
Be kind to people trying to contribute. Be aware that people may be a non-native English speaker, they might not understand things or they might be very sensitive as to how you word things. Use Emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to merge requests is in the [Thoughtbot code review guide](https://github.com/thoughtbot/guides/tree/master/code-review).
## Copy & paste responses
......
......@@ -85,7 +85,7 @@ or by directly calling the script:
sudo /etc/init.d/gitlab start
Please login with `root` / `5iveL!fe`.
Please login with `root` / `5iveL!fe`
## Install a development environment
......
7.1.0-ee
7.3.0.pre-ee
......@@ -53,15 +53,40 @@ window.split = (val) ->
window.extractLast = (term) ->
return split( term ).pop()
window.rstrip = (val) ->
return val.replace(/\s+$/, '')
# Disable button if text field is empty
window.disableButtonIfEmptyField = (field_selector, button_selector) ->
field = $(field_selector)
closest_submit = field.closest("form").find(button_selector)
closest_submit = field.closest('form').find(button_selector)
closest_submit.disable() if rstrip(field.val()) is ""
field.on 'input', ->
if rstrip($(@).val()) is ""
closest_submit.disable()
else
closest_submit.enable()
# Disable button if any input field with given selector is empty
window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) ->
closest_submit = form.find(button_selector)
empty = false
form.find('input').filter(form_selector).each ->
empty = true if rstrip($(this).val()) is ""
if empty
closest_submit.disable()
else
closest_submit.enable()
closest_submit.disable() if field.val() is ""
form.keyup ->
empty = false
form.find('input').filter(form_selector).each ->
empty = true if rstrip($(this).val()) is ""
field.on "input", ->
if $(@).val() is ""
if empty
closest_submit.disable()
else
closest_submit.enable()
......
class Diff
UNFOLD_COUNT = 20
constructor: ->
$(document).on('click', '.js-unfold', (event) =>
target = $(event.target)
unfoldBottom = target.hasClass('js-unfold-bottom')
unfold = true
[old_line, line_number] = @lineNumbers(target.parent())
offset = line_number - old_line
if unfoldBottom
line_number += 1
since = line_number
to = line_number + UNFOLD_COUNT
else
[prev_old_line, prev_new_line] = @lineNumbers(target.parent().prev())
line_number -= 1
to = line_number
if line_number - UNFOLD_COUNT > prev_new_line + 1
since = line_number - UNFOLD_COUNT
else
since = prev_new_line + 1
unfold = false
link = target.parents('.diff-file').attr('data-blob-diff-path')
params =
since: since
to: to
bottom: unfoldBottom
offset: offset
unfold: unfold
$.get(link, params, (response) =>
target.parent().replaceWith(response)
)
)
lineNumbers: (line) ->
return ([0, 0]) unless line.children().length
lines = line.children().slice(0, 2)
line_numbers = ($(l).attr('data-linenumber') for l in lines)
(parseInt(line_number) for line_number in line_numbers)
@Diff = Diff
......@@ -23,13 +23,21 @@ class Dispatcher
new Issue()
when 'projects:milestones:show'
new Milestone()
when 'projects:issues:new', 'projects:merge_requests:new'
when 'projects:issues:new'
GitLab.GfmAutoComplete.setup()
when 'projects:merge_requests:new'
GitLab.GfmAutoComplete.setup()
new Diff()
when 'projects:merge_requests:show'
new Diff()
when "projects:merge_requests:diffs"
new Diff()
when 'dashboard:show'
new Dashboard()
new Activities()
when 'projects:commit:show'
new Commit()
new Diff()
when 'groups:show', 'projects:show'
new Activities()
when 'projects:new', 'projects:edit'
......@@ -42,6 +50,8 @@ class Dispatcher
new TreeView()
when 'projects:blob:show'
new BlobView()
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
switch path.first()
when 'admin' then new Admin()
......
class Labels
constructor: ->
form = $('.label-form')
@setupLabelForm(form)
@cleanBinding()
@addBinding()
@updateColorPreview()
addBinding: ->
$(document).on 'click', '.suggest-colors a', @setSuggestedColor
$(document).on 'input', 'input#label_color', @updateColorPreview
cleanBinding: ->
$(document).off 'click', '.suggest-colors a'
$(document).off 'input', 'input#label_color'
# Initializes the form to disable the save button if no color or title is entered
setupLabelForm: (form) ->
disableButtonIfAnyEmptyField form, '.form-control', form.find('.js-save-button')
# Updates the the preview color with the hex-color input
updateColorPreview: =>
previewColor = $('input#label_color').val()
$('div.label-color-preview').css('background-color', previewColor)
# Updates the preview color with a click on a suggested color
setSuggestedColor: (e) =>
color = $(e.currentTarget).data('color')
$('input#label_color').val(color)
@updateColorPreview()
# Notify the form, that color has changed
$('.label-form').trigger('keyup')
e.preventDefault()
@Labels = Labels
......@@ -24,3 +24,5 @@ $ ->
formatResult: ldapGroupResult
formatSelection: groupFormatSelection
dropdownCssClass: "ajax-groups-dropdown"
formatNoMatches: (nomatch) ->
"Match not found; try refining your search query."
......@@ -20,6 +20,9 @@ $(document).ready ->
$(".div-dropzone-hover").append iconPicture
$(".div-dropzone").append divSpinner
$(".div-dropzone-spinner").append iconSpinner
$(".div-dropzone-spinner").css
"opacity": 0
"display": "none"
dropzone = $(".div-dropzone").dropzone(
url: project_image_path_upload
......@@ -66,13 +69,17 @@ $(document).ready ->
return
sending: ->
$(".div-dropzone-spinner").css "opacity", 0.7
$(".div-dropzone-spinner").css
"opacity": 0.7
"display": "inherit"
return
complete: ->
$(".dz-preview").remove()
$(".markdown-area").trigger "input"
$(".div-dropzone-spinner").css "opacity", 0
$(".div-dropzone-spinner").css
"opacity": 0
"display": "none"
return
)
......@@ -163,10 +170,14 @@ $(document).ready ->
val + url + "\n"
showSpinner = (e) ->
$(".div-dropzone-spinner").css "opacity", 0.7
$(".div-dropzone-spinner").css
"opacity": 0.7
"display": "inherit"
closeSpinner = ->
$(".div-dropzone-spinner").css "opacity", 0
$(".div-dropzone-spinner").css
"opacity": 0
"display": "none"
showError = (message) ->
checkIfMsgExists = $(".error-alert").children().length
......
......@@ -321,7 +321,9 @@ class Notes
GitLab.GfmAutoComplete.setup()
form = note.find(".note-edit-form")
form.show()
form.find("textarea").focus()
textarea = form.find("textarea")
textarea.focus()
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
......
@Pager =
limit: 0
offset: 0
disable: false
init: (limit, preload) ->
@limit = limit
init: (@limit = 0, preload, @disable = false) ->
@loading = $(".loading")
if preload
@offset = 0
@getOld()
else
@offset = limit
@offset = @limit
@initLoadMore()
getOld: ->
$(".loading").show()
@loading.show()
$.ajax
type: "GET"
url: location.href
data: "limit=" + @limit + "&offset=" + @offset
complete: ->
$(".loading").hide()
complete: =>
@loading.hide()
success: (data) ->
Pager.append(data.count, data.html)
dataType: "json"
......@@ -39,6 +36,7 @@
ceaseFire: ->
Pager.disable
callback: (i) ->
$(".loading").show()
Pager.getOld()
callback: (i) =>
unless @loading.is(':visible')
@loading.show()
Pager.getOld()
......@@ -54,3 +54,8 @@ $ ->
$.cookie('hide_no_ssh_message', 'false', { path: path })
$(@).parents('.no-ssh-key-message').hide()
e.preventDefault()
$('.project-side .star').on 'ajax:success', (e, data, status, xhr) ->
$(@).toggleClass('on').find('.count').html(data.star_count)
.on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
......@@ -59,4 +59,4 @@
/**
* Styles for responsive sidebar
*/
@import "semantic-ui/modules/sidebar"
@import "semantic-ui/modules/sidebar";
......@@ -4,3 +4,9 @@
.js-details-container .content.hide { display: block; }
.js-details-container.open .content { display: block; }
.js-details-container.open .content.hide { display: none; }
// Toggle between two states.
.js-toggler-container .turn-on { display: block; }
.js-toggler-container .turn-off { display: none; }
.js-toggler-container.on .turn-on { display: none; }
.js-toggler-container.on .turn-off { display: block; }
......@@ -6,7 +6,7 @@
vertical-align: middle;
cursor: pointer;
background-image: none;
border: 1px solid transparent;
border: $btn-border;
white-space: nowrap;
padding: 6px 12px;
font-size: 13px;
......@@ -19,7 +19,6 @@
user-select: none;
color: #444444;
background-color: #fff;
border-color: #ccc;
text-shadow: none;
&.hover,
......
......@@ -350,3 +350,18 @@ table {
.footer-links a {
margin-right: 15px;
}
.search_box {
position: relative;
padding: 30px;
text-align: center;
background-color: #F9F9F9;
border: 1px solid #DDDDDD;
border-radius: 0px;
}
.search_glyph {
color: #555;
font-size: 42px;
}
......@@ -10,6 +10,8 @@ $hover: #D9EDF7;
$link_color: #446e9b;
$link_hover_color: #2FA0BB;
$btn-border: 1px solid #ccc;
/*
* Success colors (green)
*/
......
......@@ -244,6 +244,7 @@ li.commit {
font-family: inherit;
padding-left: $left;
position: relative;
resize: vertical;
z-index: 2;
}
}
......@@ -89,6 +89,10 @@
}
}
.project-description {
overflow: hidden;
}
.project-access-icon {
margin-left: 10px;
float: left;
......
......@@ -40,14 +40,17 @@
font-size: 12px;
.old {
span.idiff {
background-color: #FAA;
background-color: #F99;
}
}
.new {
span.idiff {
background-color: #AFA;
background-color: #8F8;
}
}
.unfold {
cursor: pointer;
}
.file-mode-changed {
padding: 10px;
......
.explore-title {
text-align: center;
h3 {
font-weight: normal;
font-size: 30px;
}
}
......@@ -63,26 +63,10 @@
@media (min-width: 800px) { .issues_bulk_update .select2-container { min-width: 120px; } }
@media (min-width: 1200px) { .issues_bulk_update .select2-container { min-width: 160px; } }
.issues-holder {
.issues_filters {
}
.issues_bulk_update {
margin: 0;
form {
float:left;
}
.update_selected_issues {
margin-left: 4px;
}
.select2-container .select2-choice {
height: 32px;
line-height: 28px;
color: #444 !important;
font-weight: 500;
}
.issues_bulk_update {
.select2-container .select2-choice {
color: #444 !important;
font-weight: 500;
}
}
......@@ -110,7 +94,7 @@
}
}
.issue-show-labels .label {
.issue-show-labels .color-label {
padding: 6px 10px;
}
......
.suggest-colors {
margin-top: 5px;
a {
@include border-radius(4px);
width: 30px;
height: 30px;
display: inline-block;
margin-right: 10px;
}
}
.manage-labels-list {
.label {
padding: 9px;
font-size: 14px;
}
}
.color-label {
padding: 3px 4px;
}
......@@ -13,6 +13,10 @@
max-width: 100%;
margin-bottom: 20px;
}
&.default-brand-image {
margin: 0 80px;
}
}
.login-logo{
......
/**
* MR -> show: Automerge widget
/**
* MR -> show: Automerge widget
*
*/
.automerge_widget {
......@@ -48,10 +48,10 @@
.label-branch {
@include border-radius(4px);
padding: 2px 4px;
padding: 3px 4px;
border: none;
background: #555;
color: #fff;
background: $hover;
color: #333;
font-family: $monospace_font;
font-weight: normal;
overflow: hidden;
......
......@@ -190,28 +190,40 @@ ul.nav.nav-projects-tabs {
.project-side {
.btn-block {
background-image: none;
.btn,
&.btn,
&.btn-group ul.dropdown-menu {
.btn, &.btn {
white-space: normal;
text-align: left;
padding: 10px 15px;
background-color: #F1f1f1;
border-color: #EEE;
&:hover {
background-color: #eee;
border-color: #DDD;
}
}
&.btn-group-justified {
.btn {
width: 100%;
}
.dropdown-toggle {
width: 26px;
}
.count {
float: right;
font-weight: 500;
text-shadow: 0 1px #FFF;
}
ul {
&.btn-group-justified {
.btn {
width: 100%;
}
.dropdown-toggle {
width: 30px;
padding: 10px;
}
ul {
width: 100%;
}
}
}
.project-fork-icon {
float: left;
font-size: 26px;
......
class Admin::EmailsController < Admin::ApplicationController
def show
end
def create
AdminEmailsWorker.perform_async(params[:recipients], params[:subject], params[:body])
redirect_to admin_email_path, notice: 'Email sent'
end
end
......@@ -39,12 +39,13 @@ class Admin::UsersController < Admin::ApplicationController
def create
opts = {
force_random_password: true,
password_expires_at: Time.now
password_expires_at: nil
}
@user = User.new(user_params.merge(opts))
@user.created_by_id = current_user.id
@user.generate_password
@user.generate_reset_token
@user.skip_confirmation!
respond_to do |format|
......
......@@ -201,16 +201,10 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
gitlab_ldap_access do |access|
if access.allowed?(current_user)
access.update_permissions(current_user)
current_user.last_credential_check_at = Time.now
current_user.save
else
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
end
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
end
end
end
......
......@@ -46,11 +46,11 @@ class DashboardController < ApplicationController
@projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.includes(:namespace)
@projects = @projects.tagged_with(params[:label]) if params[:label].present?
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(30)
@labels = current_user.authorized_projects.tags_on(:labels)
@tags = current_user.authorized_projects.tags_on(:tags)
@groups = current_user.authorized_groups
end
......
class Public::ProjectsController < ApplicationController
class Explore::GroupsController < ApplicationController
skip_before_filter :authenticate_user!,
:reject_blocked, :set_current_user_for_observers,
:add_abilities
layout 'public'
layout "explore"
def index
@projects = Project.publicish(current_user)
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(20)
@groups = GroupsFinder.new.execute(current_user)
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(20)
end
end
class Explore::ProjectsController < ApplicationController
skip_before_filter :authenticate_user!,
:reject_blocked,
:add_abilities
layout 'explore'
def index
@projects = ProjectsFinder.new.execute(current_user)
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(20)
end
def trending
@trending_projects = TrendingProjectsFinder.new.execute(current_user)
@trending_projects = @trending_projects.page(params[:page]).per(10)
end
def starred
@starred_projects = ProjectsFinder.new.execute(current_user)
@starred_projects = @starred_projects.order('star_count DESC')
@starred_projects = @starred_projects.page(params[:page]).per(10)
end
end
......@@ -21,15 +21,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@user = Gitlab::LDAP::User.find_or_create(oauth)
@user.remember_me = true if @user.persisted?
gitlab_ldap_access do |access|
if access.allowed?(@user)
access.update_permissions(@user)
access.update_email(@user)
sign_in_and_redirect(@user)
else
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
end
# Do additional LDAP checks for the user filter and EE features
if Gitlab::LDAP::Access.allowed?(@user)
sign_in_and_redirect(@user)
else
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
end
end
......
......@@ -11,6 +11,11 @@ class Profiles::PasswordsController < ApplicationController
end
def create
unless @user.valid_password?(user_params[:current_password])
redirect_to new_profile_password_path, alert: 'You must provide a valid current password'
return
end
new_password = user_params[:password]
new_password_confirmation = user_params[:password_confirmation]
......
......@@ -25,6 +25,21 @@ class Projects::BlobController < Projects::ApplicationController
end
end
def diff
@form = UnfoldForm.new(params)
@lines = @blob.data.lines[@form.since - 1..@form.to - 1]
if @form.bottom?
@match_line = ''
else
lines_length = @lines.length - 1
line = [@form.since, lines_length].join(',')
@match_line = "@@ -#{line}+#{line} @@"
end
render layout: false
end
private
def blob
......
......@@ -8,14 +8,21 @@ class Projects::CompareController < Projects::ApplicationController
end
def show
compare = Gitlab::Git::Compare.new(@repository.raw_repository, params[:from], params[:to], MergeRequestDiff::COMMITS_SAFE_SIZE)
base_ref = params[:from]
head_ref = params[:to]
@commits = compare.commits
@commit = compare.commit
@diffs = compare.diffs
@refs_are_same = compare.same
@line_notes = []
@diff_timeout = compare.timeout
compare_result = CompareService.new.execute(
current_user,
@project,
head_ref,
@project,
base_ref
)
@commits = compare_result.commits
@diffs = compare_result.diffs
@commit = @commits.last
@line_notes = []
end
def create
......
......@@ -24,7 +24,17 @@ class Projects::HooksController < Projects::ApplicationController
end
def test
TestHookService.new.execute(hook, current_user)
if !@project.empty_repo?
status = TestHookService.new.execute(hook, current_user)
if status
flash[:notice] = 'Hook successfully executed.'
else
flash[:alert] = 'Hook execution failed. '\
'Ensure hook URL is correct and service is up.'
end
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end
redirect_to :back
end
......
......@@ -152,7 +152,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description,
:milestone_id, :label_list, :state_event
:milestone_id, :state_event, label_ids: []
)
end
end
class Projects::LabelsController < Projects::ApplicationController
before_filter :module_enabled
before_filter :label, only: [:edit, :update, :destroy]
before_filter :authorize_labels!
before_filter :authorize_admin_labels!, except: [:index]
respond_to :js, :html
def index
@labels = @project.issues_labels
@labels = @project.labels.order_by_name.page(params[:page]).per(20)
end
def new
@label = @project.labels.new
end
def create
@label = @project.labels.create(label_params)
if @label.valid?
redirect_to project_labels_path(@project)
else
render 'new'
end
end
def edit
end
def update
if @label.update_attributes(label_params)
redirect_to project_labels_path(@project)
else
render 'edit'
end
end
def generate
......@@ -21,6 +47,15 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
def destroy
@label.destroy
respond_to do |format|
format.html { redirect_to project_labels_path(@project), notice: 'Label was removed' }
format.js { render nothing: true }
end
end
protected
def module_enabled
......@@ -28,4 +63,16 @@ class Projects::LabelsController < Projects::ApplicationController
return render_404
end
end
def label_params
params.require(:label).permit(:title, :color)
end
def label
@label = @project.labels.find(params[:id])
end
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
end
......@@ -70,7 +70,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits
@commit = @merge_request.compare_base_commit
@commit = @merge_request.compare_commits.last
@diffs = @merge_request.compare_diffs
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
......@@ -242,7 +242,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :label_list
:state_event, :description, label_ids: []
)
end
end
......@@ -14,11 +14,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
render_404 and return
end
storage_path = Gitlab.config.gitlab.repository_downloads_path
@repository.clean_old_archives
file_path = @repository.archive_repo(params[:ref], storage_path, params[:format].downcase)
file_path = ArchiveRepositoryService.new.execute(@project, params[:ref], params[:format])
if file_path
# Send file to user
......
......@@ -7,6 +7,7 @@ class Projects::TeamMembersController < Projects::ApplicationController
def index
@group = @project.group
@users_projects = @project.users_projects.order('project_access DESC')
@project_group_links = @project.project_group_links
end
def new
......
......@@ -12,12 +12,10 @@ class Projects::WikisController < Projects::ApplicationController
def show
@page = @project_wiki.find_page(params[:id], params[:version_id])
gollum_wiki = @project_wiki.wiki
file = gollum_wiki.file(params[:id], gollum_wiki.ref, true)
if @page
render 'show'
elsif file
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
if file.on_disk?
send_file file.on_disk_path, disposition: 'inline'
else
......
......@@ -60,6 +60,8 @@ class ProjectsController < ApplicationController
@events = event_filter.apply_filter(@events)
@events = @events.limit(limit).offset(params[:offset] || 0)
@show_star = !(current_user && current_user.starred?(@project))
respond_to do |format|
format.html do
if @project.empty_repo?
......@@ -167,6 +169,12 @@ class ProjectsController < ApplicationController
end
end
def toggle_star
current_user.toggle_star(@project)
@project.reload
render json: { star_count: @project.star_count }
end
private
def upload_path
......@@ -188,7 +196,7 @@ class ProjectsController < ApplicationController
def project_params
params.require(:project).permit(
:name, :path, :description, :issues_tracker, :label_list,
:name, :path, :description, :issues_tracker, :tag_list,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :merge_requests_template
)
......
class SessionsController < Devise::SessionsController
def new
redirect_url = if request.referer.present?
redirect_path = if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host
referer_uri.path
......@@ -12,7 +12,11 @@ class SessionsController < Devise::SessionsController
request.fullpath
end
store_location_for(:redirect, redirect_url)
# Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully.
unless redirect_path == '/users/sign_in'
store_location_for(:redirect, redirect_path)
end
super
end
......
class UnsubscribesController < ApplicationController
skip_before_filter :authenticate_user!,
:reject_blocked, :set_current_user_for_observers,
:add_abilities
layout 'public_users'
def show
@user = get_user
end
def create
@user = get_user
if @user
@user.admin_unsubscribe!
Notify.send_unsubscribed_notification(@user).deliver
end
redirect_to new_user_session_path, notice: 'You have been unsubscribed'
end
protected
def get_user
@email = Base64.urlsafe_decode64(params[:email])
User.where(email: @email).first
end
end
......@@ -125,7 +125,13 @@ class BaseFinder
def by_label(items)
if params[:label_name].present?
items = items.tagged_with(params[:label_name])
label_names = params[:label_name].split(",")
item_ids = LabelLink.joins(:label).
where('labels.title in (?)', label_names).
where(target_type: klass.name).pluck(:target_id)
items = items.where(id: item_ids)
end
items
......
......@@ -32,20 +32,29 @@ class ProjectsFinder
# internal projects
# joined projects
#
group.projects.where(
"projects.id IN (?) OR projects.visibility_level IN (?)",
projects_members.pluck(:project_id),
Project.public_and_internal_levels
)
projects_ids = projects_members.pluck(:project_id)
else
# User has no access to group or group projects
# or has access through shared project
#
# Return only:
# public projects
# internal projects
#
group.projects.public_and_internal_only
# 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 (?)",
projects_ids,
Project.public_and_internal_levels
)
end
else
# Not authenticated
......
class TrendingProjectsFinder
def execute(current_user, start_date = nil)
start_date ||= Date.today - 1.month
projects = projects_for(current_user)
# Determine trending projects based on comments count
# for period of time - ex. month
projects.joins(:notes).where('notes.created_at > ?', start_date).
select("projects.*, count(notes.id) as ncount").
group("projects.id").order("ncount DESC")
end
private
def projects_for(current_user)
ProjectsFinder.new.execute(current_user)
end
end
module AdminEmailHelper
def admin_email_grouped_recipient_options
options_for_select([['All GitLab users', 'all']]) +
grouped_options_for_select(
'Groups' => Group.pluck(:name, :id).map{ |name, id| [name, "group-#{id}"] },
'Projects' => grouped_project_list
)
end
protected
def grouped_project_list
Group.includes(:projects).flat_map do |group|
group.human_name
group.projects.map do |project|
["#{group.human_name} / #{project.name}", "project-#{project.id}"]
end
end
end
end
\ No newline at end of file
......@@ -221,7 +221,18 @@ module ApplicationHelper
end
def render_markup(file_name, file_content)
GitHub::Markup.render(file_name, file_content).html_safe
GitHub::Markup.render(file_name, file_content).
force_encoding(file_content.encoding).html_safe
rescue RuntimeError
simple_format(file_content)
end
def markup?(filename)
Gitlab::MarkdownHelper.markup?(filename)
end
def gitlab_markdown?(filename)
Gitlab::MarkdownHelper.gitlab_markdown?(filename)
end
def spinner(text = nil, visible = false)
......
......@@ -232,4 +232,16 @@ module CommitsHelper
def diff_file_mode_changed?(diff)
diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
end
def unfold_bottom_class(bottom)
(bottom) ? 'js-unfold-bottom' : ''
end
def view_file_btn(commit_sha, diff, project)
link_to project_blob_path(project, tree_join(commit_sha, diff.new_path)),
class: 'btn btn-small view-file js-view-file' do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
end
end
end
......@@ -179,7 +179,11 @@ module GitlabMarkdownHelper
if @commit
@commit.id
elsif @repository && !@repository.empty?
@repository.head_commit.sha
if @ref
@repository.commit(@ref).try(:sha)
else
@repository.head_commit.sha
end
end
end
......
module LabelsHelper
def issue_label_names
@project.issues_labels.map(&:name)
def project_label_names
@project.labels.pluck(:title)
end
def labels_autocomplete_source
labels = @project.issues_labels
labels = labels.map{ |l| { label: l.name, value: l.name } }
labels.to_json
def render_colored_label(label)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
content_tag :span, class: 'label color-label', style: "background:#{label_color};color:#{text_color}" do
label.name
end
end
def suggested_colors
[
'#D9534F',
'#F0AD4E',
'#428BCA',
'#5CB85C',
'#34495E',
'#7F8C8D',
'#8E44AD',
'#FFECDB'
]
end
def label_css_class(name)
klass = Gitlab::IssuesLabels
def text_color_for_bg(bg_color)
r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
case name.downcase
when *klass.warning_labels
'label-warning'
when *klass.neutral_labels
'label-primary'
when *klass.positive_labels
'label-success'
when *klass.important_labels
'label-danger'
if (r + g + b) > 500
"#333"
else
'label-info'
"#FFF"
end
end
end
......@@ -122,6 +122,40 @@ module ProjectsHelper
options_for_select(values, current_tracker)
end
def link_to_toggle_star(title, starred, signed_in)
cls = 'btn btn-block'
cls += ' disabled' unless signed_in
toggle_html = content_tag('span', class: 'toggle') do
toggle_text = if starred
'Unstar'
else
'Star'
end
content_tag('i', ' ', class: 'icon-star') + toggle_text
end
count_html = content_tag('span', class: 'count') do
@project.star_count.to_s
end
link_opts = {
title: title,
class: cls,
method: :post,
remote: true,
data: {type: 'json'}
}
content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do
link_to toggle_star_project_path(@project), link_opts do
toggle_html + count_html
end
end
end
private
def get_project_nav_tabs(project, current_user)
......
......@@ -21,6 +21,16 @@ module TreeHelper
tree.html_safe
end
def render_readme(readme)
if gitlab_markdown?(readme.name)
preserve(markdown(readme.data))
elsif markup?(readme.name)
render_markup(readme.name, readme.data)
else
simple_format(readme.data)
end
end
# Return an image icon depending on the file type
#
# type - String type of the tree item; either 'folder' or 'file'
......@@ -38,24 +48,6 @@ module TreeHelper
"file_#{hexdigest(content.name)}"
end
# Public: Determines if a given filename is compatible with GitHub::Markup.
#
# filename - Filename string to check
#
# Returns boolean
def markup?(filename)
filename.downcase.end_with?(*%w(.textile .rdoc .org .creole
.mediawiki .rst .adoc .asciidoc .pod))
end
def gitlab_markdown?(filename)
filename.downcase.end_with?(*%w(.mdown .md .markdown))
end
def plain_text_readme? filename
filename =~ /^README(.txt)?$/i
end
# Simple shortcut to File.join
def tree_join(*args)
File.join(*args)
......
module Emails
module AdminNotification
def send_admin_notification(user_id, subject, body)
email = recipient(user_id)
@unsubscribe_url = unsubscribe_url(email: Base64.urlsafe_encode64(email))
@body = body
mail to: email, subject: subject
end
def send_unsubscribed_notification(user_id)
email = recipient(user_id)
mail to: email, subject: "Unsubscribed from GitLab administrator notifications"
end
end
end
......@@ -49,9 +49,10 @@ module Emails
@updated_by = User.find updated_by_user_id
@target_url = project_merge_request_url(@project, @merge_request)
set_reference("merge_request_#{merge_request_id}")
mail(from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid}) #{@mr_status}"))
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid}) #{@mr_status}"))
end
end
......
module Emails
module Profile
def new_user_email(user_id, password)
def new_user_email(user_id, password, token = nil)
@user = User.find(user_id)
@password = password
@target_url = user_url(@user)
@token = token
mail(to: @user.email, subject: subject("Account was created for you"))
end
......
class Notify < ActionMailer::Base
include ActionDispatch::Routing::PolymorphicRoutes
include Emails::AdminNotification
include Emails::Issues
include Emails::MergeRequests
include Emails::Notes
......
......@@ -142,6 +142,7 @@ class Ability
:write_wiki,
:modify_issue,
:admin_issue,
:admin_label,
:push_code
]
end
......
......@@ -13,6 +13,8 @@ module Issuable
belongs_to :assignee, class_name: "User"
belongs_to :milestone
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
......@@ -131,4 +133,20 @@ module Issuable
object_attributes: self.attributes
}
end
def label_names
labels.order('title ASC').pluck(:title)
end
def remove_labels
labels.delete_all
end
def add_labels_by_names(label_names)
label_names.each do |label_name|
label = project.labels.create_with(
color: Label::DEFAULT_COLOR).find_or_create_by(title: label_name.strip)
self.labels << label
end
end
end
......@@ -70,6 +70,12 @@ class Event < ActiveRecord::Base
author_id: user.id
)
end
def reset_event_cache_for(target)
Event.where(target_id: target.id, target_type: target.class.to_s).
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
end
def proper?
......
......@@ -89,4 +89,20 @@ class Group < Namespace
def ldap_access
ldap_group_links.first.try(:group_access)
end
class << self
def search(query)
where("LOWER(namespaces.name) LIKE :query", query: "%#{query.downcase}%")
end
def sort(method)
case method.to_s
when "newest" then reorder("namespaces.created_at DESC")
when "oldest" then reorder("namespaces.created_at ASC")
when "recently_updated" then reorder("namespaces.updated_at DESC")
when "last_updated" then reorder("namespaces.updated_at ASC")
else reorder("namespaces.path, namespaces.name ASC")
end
end
end
end
......@@ -32,9 +32,6 @@ class Issue < ActiveRecord::Base
scope :of_group, ->(group) { where(project_id: group.project_ids) }
scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) }
acts_as_taggable_on :labels
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
......@@ -67,8 +64,6 @@ class Issue < ActiveRecord::Base
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(target_id: self.id, target_type: 'Issue').
order('id DESC').limit(100).
update_all(updated_at: Time.now)
Event.reset_event_cache_for(self)
end
end
class Label < ActiveRecord::Base
DEFAULT_COLOR = '#428BCA'
belongs_to :project
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
validates :color,
format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
allow_blank: false
validates :project, presence: true
# Don't allow '?', '&', and ',' for label titles
validates :title,
presence: true,
format: { with: /\A[^&\?,&]+\z/ },
uniqueness: { scope: :project_id }
scope :order_by_name, -> { reorder("labels.title ASC") }
alias_attribute :name, :title
def open_issues_count
issues.opened.count
end
end
class LabelLink < ActiveRecord::Base
belongs_to :target, polymorphic: true
belongs_to :label
validates :target, presence: true
validates :label, presence: true
end
......@@ -44,12 +44,9 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
attr_accessor :can_be_created, :compare_failed, :compare_base_commit,
attr_accessor :can_be_created, :compare_failed,
:compare_commits, :compare_diffs
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :labels
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
......@@ -287,9 +284,7 @@ class MergeRequest < ActiveRecord::Base
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(target_id: self.id, target_type: 'MergeRequest').
order('id DESC').limit(100).
update_all(updated_at: Time.now)
Event.reset_event_cache_for(self)
end
def merge_commit_message
......
......@@ -83,11 +83,7 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect array of Git::Commit objects
# between target and source branches
def unmerged_commits
commits = if merge_request.for_fork?
compare_action.commits
else
repository.commits_between(target_branch, source_branch)
end
commits = compare_result.commits
if commits.present?
commits = Commit.decorate(commits).
......@@ -147,12 +143,7 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
diffs = if merge_request.for_fork?
compare_action.diffs
else
Gitlab::Git::Diff.between(repository, source_branch, target_branch)
end
diffs = compare_result.diffs
diffs ||= []
diffs
rescue Gitlab::Git::Diff::TimeoutError => ex
......@@ -166,13 +157,13 @@ class MergeRequestDiff < ActiveRecord::Base
private
def compare_action
Gitlab::Satellite::CompareAction.new(
def compare_result
@compare_result ||= CompareService.new.execute(
merge_request.author,
merge_request.source_project,
merge_request.source_branch,
merge_request.target_project,
merge_request.target_branch,
merge_request.source_project,
merge_request.source_branch
)
end
end
......@@ -327,9 +327,7 @@ class Note < ActiveRecord::Base
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
Event.where(target_id: self.id, target_type: 'Note').
order('id DESC').limit(100).
update_all(updated_at: Time.now)
Event.reset_event_cache_for(self)
end
def set_references
......
......@@ -22,6 +22,7 @@
# visibility_level :integer default(0), not null
# archived :boolean default(FALSE), not null
# import_status :string(255)
# star_count :integer
#
class Project < ActiveRecord::Base
......@@ -40,8 +41,7 @@ class Project < ActiveRecord::Base
default_value_for :snippets_enabled, gitlab_config_features.snippets
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :labels, :issues_default_labels
acts_as_taggable_on :tags
attr_accessor :new_default_branch
......@@ -74,6 +74,7 @@ class Project < ActiveRecord::Base
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest
has_many :issues, -> { order "state DESC, created_at DESC" }, dependent: :destroy
has_many :labels, dependent: :destroy
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy
......@@ -85,6 +86,8 @@ class Project < ActiveRecord::Base
has_many :users, through: :users_projects
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
......@@ -114,6 +117,7 @@ class Project < ActiveRecord::Base
validates :import_url,
format: { with: URI::regexp(%w(git http https)), message: "should be a valid url" },
if: :import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
# Scopes
......@@ -285,13 +289,6 @@ class Project < ActiveRecord::Base
self.id
end
# Tags are shared by issues and merge requests
def issues_labels
@issues_labels ||= (issues_default_labels +
merge_requests.tags_on(:labels) +
issues.tags_on(:labels)).uniq.sort_by(&:name)
end
def issue_exists?(issue_id)
if used_default_issues_tracker?
self.issues.where(iid: issue_id).first.present?
......@@ -338,6 +335,10 @@ class Project < ActiveRecord::Base
self.issues_tracker == "jira"
end
def redmine_tracker?
self.issues_tracker == "redmine"
end
# For compatibility with old code
def code
path
......@@ -508,6 +509,7 @@ class Project < ActiveRecord::Base
end
def rename_repo
path_was = previous_changes['path'].first
old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path)
......@@ -586,4 +588,12 @@ class Project < ActiveRecord::Base
def update_repository_size
update_attribute(:repository_size, repository.size)
end
def forks_count
ForkedProjectLink.where(forked_from_project_id: self.id).count
end
def find_label(name)
labels.find_by(name: name)
end
end
......@@ -18,6 +18,8 @@
#
class HipchatService < Service
MAX_COMMITS = 3
validates :token, presence: true, if: :activated?
def title
......@@ -57,15 +59,24 @@ class HipchatService < Service
message = ""
message << "#{push[:user_name]} "
if before =~ /000000/
message << "pushed new branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> to <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a>\n"
message << "pushed new branch <a href=\""\
"#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\
" to <a href=\"#{project.web_url}\">"\
"#{project.name_with_namespace.gsub!(/\s/, "")}</a>\n"
elsif after =~ /000000/
message << "removed branch #{ref} from <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> \n"
else
message << "pushed to branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> "
message << "pushed to branch <a href=\""\
"#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a> "
message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
for commit in push[:commits] do
message << "<br /> - #{commit[:message]} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
push[:commits].take(MAX_COMMITS).each do |commit|
message << "<br /> - #{commit[:message].lines.first} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
end
if push[:commits].count > MAX_COMMITS
message << "<br />... #{push[:commits].count - MAX_COMMITS} more commits"
end
end
......
......@@ -74,9 +74,12 @@ class JiraService < Service
}
}
json_body = message.to_json
Rails.logger.info("#{self.class.name}: sending POST with body #{json_body} to #{url}")
JiraService.post(
url,
body: message.to_json,
body: json_body,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}"
......
......@@ -72,6 +72,15 @@ class ProjectWiki
end
end
def find_file(name, version = nil, try_on_disk = true)
version = wiki.ref if version.nil? # Gollum::Wiki#file ?
if wiki_file = wiki.file(name, version, try_on_disk)
wiki_file
else
nil
end
end
def create_page(title, content, format = :markdown, message = nil)
commit = commit_details(:created, message, title)
......
......@@ -10,8 +10,11 @@ class Repository
nil
end
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, path_with_namespace + ".git")
@path_to_repo ||= File.expand_path(
File.join(Gitlab.config.gitlab_shell.repos_path, path_with_namespace + ".git")
)
end
def exists?
......@@ -134,7 +137,7 @@ class Repository
def graph_log
Rails.cache.fetch(cache_key(:graph_log)) do
stats = Gitlab::Git::GitStats.new(raw, root_ref)
stats = Gitlab::Git::GitStats.new(raw, root_ref, Gitlab.config.git.timeout)
stats.parsed_log
end
end
......@@ -263,4 +266,20 @@ class Repository
contributor
end
end
def blob_for_diff(commit, diff)
file = blob_at(commit.id, diff.new_path)
unless file
file = prev_blob_for_diff(commit, diff)
end
file
end
def prev_blob_for_diff(commit, diff)
if commit.parent_id
blob_at(commit.parent_id, diff.old_path)
end
end
end
class Tree
include Gitlab::MarkdownHelper
attr_accessor :entries, :readme, :contribution_guide
def initialize(repository, sha, path = '/')
......@@ -6,7 +8,23 @@ class Tree
git_repo = repository.raw_repository
@entries = Gitlab::Git::Tree.where(git_repo, sha, path)
if readme_tree = @entries.find(&:readme?)
available_readmes = @entries.select(&:readme?)
if available_readmes.count > 0
# If there is more than 1 readme in tree, find readme which is supported
# by markup renderer.
if available_readmes.length > 1
supported_readmes = available_readmes.select do |readme|
gitlab_markdown?(readme.name) || markup?(readme.name)
end
# Take the first supported readme, or the first available readme, if we
# don't support any of them
readme_tree = supported_readmes.first || available_readmes.first
else
readme_tree = available_readmes.first
end
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
@readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path)
end
......
......@@ -91,6 +91,8 @@ class User < ActiveRecord::Base
has_many :personal_projects, through: :namespace, source: :projects
has_many :projects, through: :users_projects
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
......@@ -175,6 +177,7 @@ class User < ActiveRecord::Base
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 :ldap, -> { where(provider: 'ldap') }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active }
......@@ -240,6 +243,15 @@ class User < ActiveRecord::Base
end
end
def generate_reset_token
@reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
self.reset_password_token = enc
self.reset_password_sent_at = Time.now.utc
@reset_token
end
def namespace_uniq
namespace_name = self.username
if Namespace.find_by(path: namespace_name)
......@@ -404,8 +416,10 @@ class User < ActiveRecord::Base
end
def requires_ldap_check?
if ldap_user?
!last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
!last_credential_check_at || (last_credential_check_at + Gitlab.config.ldap['sync_time']) < Time.now
else
false
end
......@@ -489,7 +503,7 @@ class User < ActiveRecord::Base
def post_create_hook
log_info("User \"#{self.name}\" (#{self.email}) was created")
notification_service.new_user(self)
notification_service.new_user(self, @reset_token)
system_hook_service.execute_hooks_for(self, :create)
end
......@@ -509,4 +523,22 @@ class User < ActiveRecord::Base
def system_hook_service
SystemHooksService.new
end
def admin_unsubscribe!
update_column :admin_email_unsubscribed_at, Time.now
end
def starred?(project)
starred_projects.exists?(project)
end
def toggle_star(project)
user_star_project = users_star_projects.
where(project: project, user: self).take
if user_star_project
user_star_project.destroy
else
UsersStarProject.create!(project: project, user: self)
end
end
end
# == Schema Information
#
# Table name: users_star_projects
#
# id :integer not null, primary key
# starrer_id :integer not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
#
class UsersStarProject < ActiveRecord::Base
belongs_to :project, counter_cache: :star_count
belongs_to :user
validates :user, presence: true
validates :user_id, uniqueness: { scope: [:project_id] }
validates :project, presence: true
end
class ArchiveRepositoryService
def execute(project, ref, format)
storage_path = Gitlab.config.gitlab.repository_downloads_path
unless File.directory?(storage_path)
FileUtils.mkdir_p(storage_path)
end
format ||= 'tar.gz'
repository = project.repository
repository.clean_old_archives
repository.archive_repo(ref, storage_path, format.downcase)
end
end
# Compare 2 branches for one repo or between repositories
# and return Gitlab::CompareResult object that responds to commits and diffs
class CompareService
def execute(current_user, source_project, source_branch, target_project, target_branch)
# Try to compare branches to get commits list and diffs
#
# Note: Use satellite only when need to compare between to repos
# because satellites are slower then operations on bare repo
if target_project == source_project
Gitlab::CompareResult.new(
Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
source_branch,
)
)
else
Gitlab::Satellite::CompareAction.new(
current_user,
target_project,
target_branch,
source_project,
source_branch
).result
end
end
end
......@@ -37,7 +37,7 @@ module Files
if created_successfully
success
else
error("Your changes could not be committed, because the file has been changed")
error("Your changes could not be committed. Maybe the file was changed by another process or there was nothing to commit?")
end
end
end
......
module Issues
class CreateService < Issues::BaseService
def execute
issue = project.issues.new(params)
label_params = params[:label_ids]
issue = project.issues.new(params.except(:label_ids))
issue.author = current_user
if issue.save
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
issue.create_cross_references!(issue.project, current_user)
......
......@@ -22,27 +22,25 @@ module MergeRequests
# Set MR description based on project template
merge_request.description = merge_request.target_project.merge_requests_template
# Try to compare branches to get commits list and diffs
compare_action = Gitlab::Satellite::CompareAction.new(
compare_result = CompareService.new.execute(
current_user,
merge_request.source_project,
merge_request.source_branch,
merge_request.target_project,
merge_request.target_branch,
merge_request.source_project,
merge_request.source_branch
)
commits = compare_action.commits
commits = compare_result.commits
# At this point we decide if merge request can be created
# If we have at least one commit to merge -> creation allowed
if commits.present?
merge_request.compare_commits = Commit.decorate(commits)
merge_request.compare_base_commit = Commit.new(commits.first)
merge_request.can_be_created = true
merge_request.compare_failed = false
# Try to collect diff for merge request.
diffs = compare_action.diffs
diffs = compare_result.diffs
if diffs.present?
merge_request.compare_diffs = diffs
......
module MergeRequests
class CreateService < MergeRequests::BaseService
def execute
merge_request = MergeRequest.new(params)
label_params = params[:label_ids]
merge_request = MergeRequest.new(params.except(:label_ids))
merge_request.source_project = project
merge_request.target_project ||= project
merge_request.author = current_user
if merge_request.save
merge_request.update_attributes(label_ids: label_params)
event_service.open_mr(merge_request, current_user)
notification_service.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(merge_request.project, current_user)
......
......@@ -105,9 +105,9 @@ class NotificationService
end
# Notify new user with email after creation
def new_user(user)
def new_user(user, token = nil)
# Don't email omniauth created users
mailer.new_user_email(user.id, user.password) unless user.extern_uid?
mailer.new_user_email(user.id, user.password, token) unless user.extern_uid?
end
# Notify users on new note in system
......
......@@ -57,7 +57,6 @@ module Projects
:add_repository,
@project.path_with_namespace
)
end
if @project.wiki_enabled?
......
......@@ -12,7 +12,13 @@ module Search
return result unless query.present?
if params[:search_code].present?
blobs = project.repository.search_files(query, params[:repository_ref]) unless project.empty_repo?
if !@project.empty_repo?
blobs = project.repository.search_files(query,
params[:repository_ref])
else
blobs = Array.new
end
blobs = Kaminari.paginate_array(blobs).page(params[:page]).per(20)
result[:blobs] = blobs
result[:total_results] = blobs.total_count
......
......@@ -2,5 +2,8 @@ class TestHookService
def execute(hook, current_user)
data = GitPushService.new.sample_data(hook.project, current_user)
hook.execute(data)
true
rescue SocketError
false
end
end
%h3.page-title
Send email notication
%p.light
You can notify the app / group or a project by sending them an email notification
= form_tag admin_email_path, class: 'form-horizontal', id: 'new-admin-email' do
.form-group
%label.control-label{for: :subject} Subject
.col-sm-10
= text_field_tag :subject, '', class: 'form-control', required: true
.form-group
%label.control-label{for: :body} Body
.col-sm-10
= text_area_tag :body, '', class: 'form-control', rows: 15, required: true
.form-group
%label.control-label{for: :recipients} Recipient group
.col-sm-10
= select_tag :recipients, admin_email_grouped_recipient_options, class: :select2, required: true
.form-actions
= submit_tag 'Send message', class: 'btn btn-create'
......@@ -31,9 +31,9 @@
= f.label :password, class: 'control-label'
.col-sm-10
%strong
A temporary password will be generated and sent to user.
Reset link will be generated and sent to the user.
%br
User will be forced to change it after first sign in
User will be forced to set the password on first sign in.
- else
%fieldset
%legend Password
......
......@@ -32,6 +32,7 @@
.panel-heading
Users (#{@users.total_count})
.panel-head-actions
= link_to 'Send email to users', admin_email_path, class: 'btn btn-info'
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
%ul.well-list
- @users.each do |user|
......
......@@ -44,12 +44,12 @@
- if @labels.present?
- if @tags.present?
%fieldset
%legend Labels
%legend Tags
%ul.nav.nav-pills.nav-stacked.nav-small
- @labels.each do |label|
%li{ class: (label.name == params[:label]) ? 'active' : 'light' }
= link_to projects_dashboard_filter_path(scope: params[:scope], label: label.name) do
- @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
= link_to projects_dashboard_filter_path(scope: params[:scope], tag: tag.name) do
%i.icon-tag
= label.name
= tag.name
......@@ -46,5 +46,5 @@
%br
Public projects are an easy way to allow everyone to have read-only access.
.link_holder
= link_to public_projects_path, class: "btn btn-new" do
= link_to explore_projects_path, class: "btn btn-new" do
Browse public projects »
......@@ -54,10 +54,10 @@
%span.label
%i.icon-archive
Archived
- project.labels.each do |label|
- project.tags.each do |tag|
%span.label.label-info
%i.icon-tag
= label.name
= tag.name
- if project.description.present?
%p= truncate project.description, length: 100
.last-activity
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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