Commit af978cd5 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' into avatar-cropping

parents 0701b70c 9f80118e
image: "ruby:2.2"
image: "ruby:2.1"
services:
- mysql:latest
......@@ -6,7 +6,7 @@ services:
- redis:latest
cache:
key: "ruby22"
key: "ruby21"
paths:
- vendor
......@@ -140,87 +140,87 @@ bundler:audit:
- mysql
allow_failure: true
# Ruby 2.1 jobs
# Ruby 2.2 jobs
spec:feature:ruby21:
image: ruby:2.1
spec:feature:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spec:api:ruby21:
image: ruby:2.1
spec:api:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spec:models:ruby21:
image: ruby:2.1
spec:models:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spec:lib:ruby21:
image: ruby:2.1
spec:lib:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spec:services:ruby21:
image: ruby:2.1
spec:services:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spec:benchmark:ruby21:
image: ruby:2.1
spec:benchmark:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
......@@ -228,56 +228,56 @@ spec:benchmark:ruby21:
- mysql
allow_failure: true
spec:other:ruby21:
image: ruby:2.1
spec:other:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spinach:project:half:ruby21:
image: ruby:2.1
spinach:project:half:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spinach:project:rest:ruby21:
image: ruby:2.1
spinach:project:rest:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
spinach:other:ruby21:
image: ruby:2.1
spinach:other:ruby22:
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
cache:
key: "ruby21"
key: "ruby22"
paths:
- vendor
tags:
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.5.0 (unreleased)
v 8.6.0 (unreleased)
- Show Crowd login tab when sign in is disabled and Crowd is enabled (Peter Hudec)
v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
- Cache various Repository methods to improve performance (Yorick Peterse)
- Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu)
......@@ -46,22 +49,38 @@ v 8.5.0 (unreleased)
- Deprecate API "merge_request/:merge_request_id/comments". Use "merge_requests/:merge_request_id/notes" instead
- Deprecate API "merge_request/:merge_request_id/...". Use "merge_requests/:merge_request_id/..." instead
- Prevent parse error when name of project ends with .atom and prevent path issues
- Discover branches for commit statuses ref-less when doing merge when succeeded
- Mark inline difference between old and new paths when a file is renamed
- Support Akismet spam checking for creation of issues via API (Stan Hu)
- API: Allow to set or update a merge-request's milestone (Kirill Skachkov)
- Improve UI consistency between projects and groups lists
- Add sort dropdown to dashboard projects page
- Fixed logo animation on Safari (Roman Rott)
- Fix Merge When Succeeded when multiple stages
- Hide remove source branch button when the MR is merged but new commits are pushed (Zeger-Jan van de Weg)
- In seach autocomplete show only groups and projects you are member of
- Don't process cross-reference notes from forks
- Fix: init.d script not working on OS X
- Faster snippet search
- Added API to download build artifacts
- Title for milestones should be unique (Zeger-Jan van de Weg)
- Validate correctness of maximum attachment size application setting
- Replaces "Create merge request" link with one to the "Merge Request" when one exists
- Fix CI builds badge, add a new link to builds badge, deprecate the old one
- Fix broken link to project in build notification emails
- Ability to see and sort on vote count from Issues and MR lists
- Fix builds scheduler when first build in stage was allowed to fail
- User project limit is reached notice is hidden if the projects limit is zero
- Add API support for managing runners and project's runners
- Allow SAML users to login with no previous account without having to allow
all Omniauth providers to do so.
- Allow existing users to auto link their SAML credentials by logging in via SAML
- Make it possible to erase a build (trace, artifacts) using UI and API
- Ability to revert changes from a Merge Request or Commit
- Emoji comment on diffs are not award emoji
- Add label description (Nuttanart Pornprasitsakul)
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
v 8.4.4
- Update omniauth-saml gem to 1.4.2
......
......@@ -34,7 +34,7 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
abbreviation.
If you have read this guide and want to know how the GitLab [core-team][]
If you have read this guide and want to know how the GitLab [core team][core-team]
operates please see [the GitLab contributing process](PROCESS.md).
## Contributor license agreement
......@@ -68,10 +68,10 @@ for audiences of all ages.
## Helping others
Please help other GitLab users when you can. The channels people will reach out
on can be found on the [getting help page][].
on can be found on the [getting help page][getting-help].
Sign up for the mailing list, answer GitLab questions on StackOverflow or
respond in the IRC channel. You can also sign up on [CodeTriage][] to help with
respond in the IRC channel. You can also sign up on [CodeTriage][codetriage] to help with
the remaining issues on the GitHub issue tracker.
## I want to contribute!
......@@ -115,7 +115,7 @@ For feature proposals for EE, open an issue on the
In order to help track the feature proposals, we have created a
[`feature proposal`][fpl] label. For the time being, users that are not members
of the project cannot add labels. You can instead ask one of the [core team][]
of the project cannot add labels. You can instead ask one of the [core team][core-team]
members to add the label `feature proposal` to the issue.
Please keep feature proposals as small and simple as possible, complex ones
......@@ -299,8 +299,8 @@ to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it.
For examples of feedback on merge requests please look at already
[closed merge requests][]. If you would like quick feedback on your merge
request feel free to mention one of the Merge Marshalls of the [core team][].
[closed merge requests][closed-merge-requests]. If you would like quick feedback on your merge
request feel free to mention one of the Merge Marshalls of the [core team][core-team].
Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the
......@@ -369,7 +369,7 @@ Like all merge requests the target should be master so all bugfixes are in maste
## Definition of done
If you contribute to GitLab please know that changes involve more than just
code. We have the following [definition of done][]. Please ensure you support
code. We have the following [definition of done][definition-of-done]. Please ensure you support
the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
......@@ -448,12 +448,12 @@ when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0,
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
[core-team]: https://about.gitlab.com/core-team/
[getting help page]: https://about.gitlab.com/getting-help/
[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[getting-help]: https://about.gitlab.com/getting-help/
[codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
......@@ -467,9 +467,9 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed merge requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
[definition of done]: http://guide.agilealliance.org/guide/definition-of-done.html
[Contributor Covenant]: http://contributor-covenant.org
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
[contributor-covenant]: http://contributor-covenant.org
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
......@@ -50,7 +50,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", '~> 8.1'
gem "gitlab_git", '~> 8.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
......@@ -115,7 +115,7 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
gem "unicorn", '~> 4.8.2'
gem "unicorn", '~> 4.9.0'
gem 'unicorn-worker-killer', '~> 0.4.2'
end
......@@ -207,7 +207,7 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.2.0'
gem 'gitlab_emoji', '~> 0.3.0'
gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.0.0'
......@@ -221,7 +221,7 @@ gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
# Sentry integration
gem 'sentry-raven'
gem 'sentry-raven', '~> 0.15'
# Metrics
group :metrics do
......
......@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.3)
allocations (1.0.4)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
......@@ -336,11 +336,11 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (2.1.1)
gemojione (2.2.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
github-linguist (4.7.3)
github-linguist (4.7.5)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
......@@ -355,13 +355,13 @@ GEM
diff-lcs (~> 1.1)
mime-types (~> 1.15)
posix-spawn (~> 0.3)
gitlab_emoji (0.2.0)
gemojione (~> 2.1)
gitlab_git (8.1.0)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (8.2.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
rugged (~> 0.23.3)
rugged (~> 0.24.0b13)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
......@@ -701,7 +701,7 @@ GEM
rubyntlm (0.5.2)
rubypants (0.2.0)
rufus-scheduler (3.1.10)
rugged (0.23.3)
rugged (0.24.0b13)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
......@@ -723,7 +723,7 @@ GEM
activesupport (>= 3.1, < 4.3)
select2-rails (3.5.9.3)
thor (~> 0.14)
sentry-raven (0.15.4)
sentry-raven (0.15.6)
faraday (>= 0.7.6)
settingslogic (2.0.9)
sexp_processor (4.6.0)
......@@ -834,7 +834,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
unicorn (4.8.3)
unicorn (4.9.0)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
......@@ -932,8 +932,8 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.2.0)
gitlab_git (~> 8.1)
gitlab_emoji (~> 0.3.0)
gitlab_git (~> 8.2)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
......@@ -1009,7 +1009,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
sentry-raven
sentry-raven (~> 0.15)
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
......@@ -1036,7 +1036,7 @@ DEPENDENCIES
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 4.8.2)
unicorn (~> 4.9.0)
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.0.0)
virtus (~> 1.0.1)
......
......@@ -67,7 +67,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the
GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1 or 2.2
- Ruby (MRI) 2.1
- Git 1.7.10+
- Redis 2.8+
- MySQL or PostgreSQL
......
8.5.0-pre
8.6.0-pre
app/assets/images/emoji.png

813 KB | W: | H:

app/assets/images/emoji.png

257 KB | W: | H:

app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -76,6 +76,8 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length
when 'groups:show'
new Activities()
shortcut_handler = new ShortcutsNavigation()
......@@ -91,7 +93,7 @@ class Dispatcher
new TreeView()
when 'projects:find_file:show'
shortcut_handler = true
when 'projects:blob:show'
when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit'
......
......@@ -15,3 +15,5 @@ class @IssuableContext
block.find('.selectbox').show()
block.find('.value').hide()
block.find('.js-select2').select2("open")
$(".right-sidebar").niceScroll()
......@@ -146,6 +146,7 @@ class @MergeRequestTabs
success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html
$('div#diffs .js-syntax-highlight').syntaxHighlight()
@expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true
@scrollToElement("#diffs")
......@@ -177,3 +178,10 @@ class @MergeRequestTabs
options = $.extend({}, defaults, options)
$.ajax(options)
# Returns diff view type
diffViewType: ->
$('.inline-parallel-buttons a.active').data('view-type')
expandViewContainer: ->
$('.container-fluid').removeClass('container-limited')
......@@ -62,14 +62,24 @@ class @Milestone
dataType: "json"
constructor: ->
oldMouseStart = $.ui.sortable.prototype._mouseStart
$.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
this._trigger "beforeStart", event, this._uiHash()
oldMouseStart.apply this, [event, overrideHandle, noActivation]
@bindIssuesSorting()
@bindMergeRequestSorting()
@bindTabsSwitching
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
connectWith: ".issues-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
beforeStart: (event, ui) ->
$(".issues-sortable-list").css "min-height", ui.item.outerHeight()
stop: (event, ui) ->
$(".issues-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortIssues(data)
......@@ -95,10 +105,22 @@ class @Milestone
).disableSelection()
bindMergeRequestSorting: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show')
$(previousTabClass).hide()
$(currentTabClass).removeClass('hidden')
$(currentTabClass).show()
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
items: "li:not(.ui-sort-disabled)",
beforeStart: (event, ui) ->
$(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight()
stop: (event, ui) ->
$(".merge_requests-sortable-list").css "min-height", "0px"
update: (event, ui) ->
data = $(this).sortable("serialize")
Milestone.sortMergeRequests(data)
......
......@@ -24,6 +24,10 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@nextIssue()
return false
)
Mousetrap.bind('e', =>
@editIssue()
return false
)
if isMergeRequest
......@@ -63,3 +67,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation
# Focus the input field
replyField.focus()
editIssue: ->
$editBtn = $('.issuable-edit')
Turbolinks.visit($editBtn.attr('href'))
......@@ -2,7 +2,7 @@
class @Wikis
constructor: ->
$('.build-new-wiki').bind 'click', (e) =>
$('.new-wiki-page').on 'submit', (e) =>
$('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
slug = @slugify(field.val())
......@@ -10,6 +10,7 @@ class @Wikis
if (slug.length > 0)
path = field.attr('data-wikis-path')
location.href = path + '/' + slug
e.preventDefault()
dasherize: (value) ->
value.replace(/[_\s]+/g, '-')
......
......@@ -7,7 +7,7 @@
&:focus,
&:active {
outline: none;
@include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
@include box-shadow($gl-btn-active-background);
}
}
......@@ -28,7 +28,7 @@
}
&:active {
@include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
@include box-shadow ($gl-btn-active-background);
background-color: $dark;
border-color: $border-dark;
......@@ -68,6 +68,12 @@
@include btn-default;
@include btn-white;
color: $gl-text-color;
&:focus:active {
outline: 0;
}
&.btn-small,
&.btn-sm {
padding: 4px 10px;
......@@ -130,6 +136,11 @@
&.disabled {
pointer-events: auto !important;
}
.caret {
margin-left: 5px;
color: $gray-darkest;
}
}
.btn-block {
......@@ -179,7 +190,7 @@
}
.active {
@include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
@include box-shadow($gl-btn-active-background);
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
......
......@@ -56,6 +56,10 @@ hr {
margin: $gl-padding 0;
}
.dropdown-menu {
margin: 6px 0 0;
}
.dropdown-menu > li > a {
text-shadow: none;
}
......
......@@ -21,10 +21,3 @@
}
}
}
.issues-filters,
.issues_bulk_update {
.select2-container .select2-choice {
color: #444 !important;
}
}
......@@ -77,6 +77,7 @@ header {
line-height: $header-height;
font-weight: normal;
color: #4c4e54;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
......
......@@ -44,8 +44,10 @@
white-space: nowrap;
i {
float: left;
margin-top: 3px;
margin-right: 5px;
visibility: hidden;
@extend .pull-left;
}
&:hover i {
......
......@@ -6,31 +6,28 @@
.status-box {
@include border-radius(3px);
display: block;
float: left;
padding: 0 $gl-btn-padding;
font-weight: normal;
margin-top: 5px;
margin-right: 10px;
color: #FFF;
font-size: $gl-font-size;
line-height: 25px;
&.status-box-closed {
background-color: $gl-danger;
color: #FFF;
}
&.status-box-merged {
background-color: $gl-primary;
color: #FFF;
}
&.status-box-open {
background-color: $green-light;
color: #FFF;
}
&.status-box-expired {
background: #cea61b;
color: #FFF;
}
}
......@@ -77,12 +77,21 @@
margin-bottom: 0px;
> .dropdown {
margin-right: 10px;
margin-right: $gl-padding-top;
display: inline-block;
}
> .btn {
margin-right: $gl-padding-top;
display: inline-block;
&:last-child {
margin-right: 0;
}
}
> .btn-grouped {
float: none;
}
> form {
......@@ -94,7 +103,7 @@
display: inline-block;
position: relative;
top: 1px;
margin-right: 10px;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; }
......
/** Select2 selectbox style override **/
.select2-container {
width: 100% !important;
}
.select2-container, .select2-container.select2-drop-above {
.select2-choice {
background: #FFF;
border-color: #DDD;
height: 36px;
padding: 6px $gl-padding;
background: #fff;
border-color: $input-border;
border-color: $border-white-light;
height: 35px;
padding: $gl-vert-padding $gl-btn-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
@include border-radius(2px);
@include border-radius($border-radius-default);
.select2-arrow {
background: #FFF;
border-left: none;
padding-top: 5px;
}
background-image: none;
background-color: transparent;
border: none;
padding-top: 6px;
padding-right: 10px;
.select2-chosen {
color: $gl-text-color;
b {
@extend .caret;
color: $gray-darkest;
}
}
&.select2-default {
.select2-chosen {
color: #999;
margin-right: 15px;
}
&:hover {
background-color: $gray-dark;
border-color: $border-white-normal;
color: $gl-text-color;
}
}
}
.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice{
color: #7f8fa4;
border: 1px solid #e7e9ed;
}
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px);
padding: 16px;
border: none !important;
@include border-radius ($border-radius-default);
border: none;
}
.select2-results .select2-result-label {
padding: 9px;
padding: 10px 15px;
}
.select2-drop{
......@@ -56,15 +60,30 @@
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: 600;
color: #313236;
color: $gl-text-color;
}
.select2-container-active {
.select2-choice, .select2-choices {
@include box-shadow(none);
}
}
.select2-dropdown-open {
.select2-choice {
border-color: $border-white-normal;
outline: 0;
background-image: none;
background-color: $white-dark;
@include box-shadow($gl-btn-active-gradient);
}
}
.select2-container-multi {
.select2-choices {
@include border-radius(2px);
@include border-radius($border-radius-default);
border-color: $input-border;
background: white;
padding-left: $gl-padding / 2;
background: none;
.select2-search-field input {
padding: $gl-padding / 2;
......@@ -76,14 +95,16 @@
.select2-search-choice {
margin: 8px 0 0 8px;
background: white;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
background-color: $background-color;
background-image: none;
.select2-search-choice-close {
top: 5px;
top: 4px;
left: 3px;
}
&.select2-search-choice-focus {
......@@ -91,22 +112,25 @@
}
}
}
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
@include box-shadow($gl-btn-active-gradient);
}
}
.select2-container-multi .select2-choices .select2-search-choice {
}
.select2-drop-active {
border: 1px solid #BBB !important;
margin-top: 4px;
font-size: 13px;
margin-top: 6px;
font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
.select2-search input {
background: #fafafa;
border-color: #DDD;
}
.select2-results {
max-height: 350px;
.select2-highlighted {
......@@ -115,8 +139,34 @@
}
}
.select2-container {
width: 100% !important;
.select2-search {
padding: 15px 15px 5px;
.select2-drop-auto-width & {
padding: 15px 15px 5px;
}
}
.select2-search input {
padding: 2px 25px 2px 5px;
background: #fff image-url('select2.png');
background-repeat: no-repeat;
background-position: right 0px bottom 6px;
border: 1px solid $input-border;
@include border-radius($border-radius-default);
@include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
&:focus {
border-color: $input-border-focus;
}
}
.select2-search input.select2-active {
background-color: #fff;
background-image: image-url('select2-spinner.gif') !important;
background-repeat: no-repeat;
background-position: right 5px center !important;
background-size: 16px 16px !important;
}
/** Branch/tag selector **/
......@@ -124,10 +174,19 @@
width: 160px !important;
}
.ajax-users-dropdown, .ajax-project-users-dropdown {
.select2-search {
padding-top: 2px;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results {
margin: 0;
padding: 10px 0;
}
.ajax-users-select {
......
......@@ -31,6 +31,7 @@ $gl-padding-top:10px;
$gl-avatar-size: 40px;
$secondary-text: #7f8fa4;
$error-exclamation-point: #E62958;
$border-radius-default: 3px;
/*
* Color schema
......@@ -100,6 +101,8 @@ $gl-success: $green-normal;
$gl-info: $blue-normal;
$gl-warning: $orange-normal;
$gl-danger: $red-normal;
$gl-btn-active-background: rgba(0, 0, 0, 0.12);
$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background;
/*
* Commit Diff Colors
......
......@@ -12,6 +12,14 @@
.identifier {
color: #5c5d5e;
}
.issue_created_ago, .author_link {
white-space: nowrap;
}
.issue-meta {
margin-left: 65px
}
}
.detail-page-description {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -64,7 +64,6 @@
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter_inner_width;
overflow-x: hidden;
// --
&:first-child {
......@@ -90,7 +89,6 @@
.gutter-toggle {
margin-left: 20px;
border-left: 1px solid $border-gray-light;
padding-left: 10px;
&:hover {
......@@ -157,11 +155,10 @@
.right-sidebar {
position: fixed;
top: 58px;
bottom: 0;
right: 0;
height: 100%;
transition-duration: .3s;
transition: width .3s;
background: $gray-light;
overflow: scroll;
padding: 10px 20px;
&.right-sidebar-expanded {
......@@ -170,6 +167,14 @@
hr {
display: none;
}
.sidebar-collapsed-icon {
display: none;
}
.gutter-toggle {
border-left: 1px solid $border-gray-light;
}
}
.subscribe-button {
......@@ -181,7 +186,6 @@
&.right-sidebar-collapsed {
width: $sidebar_collapsed_width;
padding-top: 0;
overflow-x: hidden;
hr {
margin: 0;
......@@ -192,21 +196,13 @@
}
.block {
border-bottom: none;
width: $sidebar_collapsed_width - 1px;
margin-left: -19px;
padding: 15px 0 0 0;
}
}
.btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
}
border-bottom: none;
overflow: hidden;
}
&.right-sidebar-collapsed {
.issuable-count,
.issuable-nav,
.assignee > *,
......@@ -219,15 +215,13 @@
}
.gutter-toggle {
margin-left: -$gutter_inner_width + 4;
margin-left: -36px;
}
.sidebar-collapsed-icon {
display: block;
float: left;
width: 62px;
width: 100%;
text-align: center;
margin-left: -19px;
padding-bottom: 10px;
color: #999999;
......@@ -247,14 +241,15 @@
color: #999999;
}
}
}
}
&.right-sidebar-expanded {
.sidebar-collapsed-icon {
display: none;
.btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
}
}
}
......
......@@ -9,7 +9,7 @@
}
}
.manage-labels-list {
.label-row {
.label {
padding: 9px;
font-size: 14px;
......
......@@ -11,3 +11,60 @@ li.milestone {
height: 6px;
}
}
.milestone-content {
.issues-count {
margin-right: 17px;
float: right;
width: 105px;
}
.issue-row {
.color-label {
border-radius: 2px;
padding: 3px !important;
}
// Issue title
span a {
color: rgba(0,0,0,0.64);
}
}
}
.milestone-summary {
margin-bottom: 25px;
.milestone-stat {
margin-right: 10px;
}
.time-elapsed {
color: $orange-light;
}
}
.issues-sortable-list {
.issue-detail {
display: block;
.issue-number{
color: rgba(0,0,0,0.44);
margin-right: 5px;
}
.color-label {
padding: 6px 10px;
margin-right: 7px;
margin-top: 10px;
}
.avatar {
float: none;
}
}
}
.milestone-detail {
border-bottom: 1px solid $border-color;
padding: 20px 0;
}
......@@ -32,6 +32,7 @@
.cover-controls {
.project-settings-dropdown {
margin-left: 10px;
display: inline-block;
}
}
......@@ -73,24 +74,19 @@
font-weight: normal;
}
.visibility-icon {
display: inline-block;
margin-left: 5px;
font-size: 18px;
color: $gray;
}
p {
padding: 0 $gl-padding;
color: #5c5d5e;
}
}
.visibility-level-label {
@extend .btn;
@extend .btn-gray;
color: $gray;
cursor: default;
i {
color: inherit;
}
}
.project-repo-buttons {
margin-top: 20px;
margin-bottom: 0px;
......@@ -191,10 +187,10 @@
.dropdown-menu {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px);
@include border-radius ($border-radius-default);
border: none;
padding: 16px 0;
padding: 10px 0;
font-size: 14px;
font-weight: 100;
......
/**
* Dashboard Todos
*
*/
.navbar-nav {
li {
.badge.todos-pending-count {
background-color: #7f8fa4;
margin-top: -5px;
}
}
}
.todos {
.panel {
border-top: none;
margin-bottom: 0;
}
}
.todo-item {
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
&.todo-inline {
.avatar {
position: relative;
top: -2px;
}
.todo-title {
line-height: 40px;
}
}
a {
color: #4c4e54;
}
.avatar {
margin-left: -($gl-avatar-size + $gl-padding-top);
}
.todo-title {
@include str-truncated(calc(100% - 174px));
font-weight: 600;
.author_name {
color: #333;
}
}
.todo-body {
margin-right: 174px;
.todo-note {
word-wrap: break-word;
.md {
color: #7f8fa4;
font-size: $gl-font-size;
p {
color: #5c5d5e;
}
}
pre {
border: none;
background: #f9f9f9;
border-radius: 0;
color: #777;
margin: 0 20px;
overflow: hidden;
}
.note-image-attach {
margin-top: 4px;
margin-left: 0px;
max-width: 200px;
float: none;
}
p:last-child {
margin-bottom: 0;
}
}
.todo-note-icon {
color: #777;
float: left;
font-size: $gl-font-size;
line-height: 16px;
margin-right: 5px;
}
}
&:last-child { border:none }
}
@media (max-width: $screen-xs-max) {
.todo-item {
padding-left: $gl-padding;
.todo-title {
white-space: normal;
overflow: visible;
max-width: 100%;
}
.avatar {
display: none;
}
.todo-body {
margin: 0;
border-left: 2px solid #DDD;
padding-left: 10px;
}
}
}
......@@ -4,8 +4,3 @@
margin-right: auto;
padding-right: 7px;
}
.wiki-last-edit-by {
font-size: 80%;
font-weight: normal;
}
......@@ -53,6 +53,6 @@ class Admin::LabelsController < Admin::ApplicationController
end
def label_params
params[:label].permit(:title, :color)
params[:label].permit(:title, :description, :color)
end
end
......@@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base
helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
helper_method :repository
helper_method :repository, :can_collaborate_with_project?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
......@@ -410,6 +410,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path
end
def can_collaborate_with_project?(project = nil)
project ||= @project
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
end
private
def set_default_sort
......
......@@ -13,17 +13,11 @@ module CreatesCommit
result = service.new(@tree_edit_project, current_user, commit_params).execute
if result[:status] == :success
flash[:notice] = success_notice || "Your changes have been successfully committed."
if create_merge_request?
success_path = new_merge_request_path
target = different_project? ? "project" : "branch"
flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
end
update_flash_notice(success_notice)
respond_to do |format|
format.html { redirect_to success_path }
format.json { render json: { message: "success", filePath: success_path } }
format.html { redirect_to final_success_path(success_path) }
format.json { render json: { message: "success", filePath: final_success_path(success_path) } }
end
else
flash[:alert] = result[:message]
......@@ -41,14 +35,32 @@ module CreatesCommit
end
def authorize_edit_tree!
return if can?(current_user, :push_code, project)
return if current_user && current_user.already_forked?(project)
return if can_collaborate_with_project?
access_denied!
end
private
def update_flash_notice(success_notice)
flash[:notice] = success_notice || "Your changes have been successfully committed."
if create_merge_request?
if merge_request_exists?
flash[:notice] = nil
else
target = different_project? ? "project" : "branch"
flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
end
end
end
def final_success_path(success_path)
return success_path unless create_merge_request?
merge_request_exists? ? existing_merge_request_path : new_merge_request_path
end
def new_merge_request_path
new_namespace_project_merge_request_path(
@mr_source_project.namespace,
......@@ -62,6 +74,19 @@ module CreatesCommit
)
end
def existing_merge_request_path
namespace_project_merge_request_path(@mr_target_project.namespace, @mr_target_project, @merge_request)
end
def merge_request_exists?
return @merge_request if defined?(@merge_request)
@merge_request = @mr_target_project.merge_requests.opened.find_by(
source_branch: @mr_source_branch,
target_branch: @mr_target_branch
)
end
def different_project?
@mr_source_project != @mr_target_project
end
......@@ -75,7 +100,7 @@ module CreatesCommit
end
def set_commit_variables
@mr_source_branch = @target_branch
@mr_source_branch ||= @target_branch
if can?(current_user, :push_code, @project)
# Edit file in this project
......@@ -89,7 +114,7 @@ module CreatesCommit
else
# Merge request to this project
@mr_target_project = @project
@mr_target_branch = @ref
@mr_target_branch ||= @ref
end
else
# Edit file in fork
......@@ -97,7 +122,7 @@ module CreatesCommit
# Merge request from fork to this project
@mr_source_project = @tree_edit_project
@mr_target_project = @project
@mr_target_branch = @ref
@mr_target_branch ||= @ref
end
end
end
......@@ -6,6 +6,8 @@ module IssuesAction
@issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
@issues = @issues.preload(:author, :project)
@label = @issuable_finder.labels.first
respond_to do |format|
format.html
format.atom { render layout: false }
......
......@@ -5,5 +5,7 @@ module MergeRequestsAction
@merge_requests = get_merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
@label = @issuable_finder.labels.first
end
end
class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
def index
@todos = @todos.page(params[:page]).per(PER_PAGE)
end
def destroy
todo.done!
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { render nothing: true }
end
end
def destroy_all
@todos.each(&:done!)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { render nothing: true }
end
end
private
def todo
@todo ||= current_user.todos.find(params[:id])
end
def find_todos
@todos = TodosFinder.new(current_user, params).execute
end
end
......@@ -42,6 +42,26 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
def saml
if current_user
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
else
redirect_to after_sign_in_path_for(current_user)
end
else
saml_user = Gitlab::Saml::User.new(oauth)
saml_user.save
@user = saml_user.gl_user
continue_login_process
end
end
def omniauth_error
@provider = params[:provider]
@error = params[:error]
......@@ -65,25 +85,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
@user = Gitlab::OAuth::User.new(oauth)
@user.save
# Only allow properly saved users to login.
if @user.persisted? && @user.valid?
log_audit_event(@user.gl_user, with: oauth['provider'])
sign_in_and_redirect(@user.gl_user)
else
error_message =
if @user.gl_user.errors.any?
@user.gl_user.errors.map do |attribute, message|
"#{attribute} #{message}"
end.join(", ")
else
''
end
oauth_user = Gitlab::OAuth::User.new(oauth)
oauth_user.save
@user = oauth_user.gl_user
redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
end
continue_login_process
end
rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
......@@ -104,6 +110,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
def continue_login_process
# Only allow properly saved users to login.
if @user.persisted? && @user.valid?
log_audit_event(@user, with: oauth['provider'])
sign_in_and_redirect(@user)
else
error_message = @user.errors.full_messages.to_sentence
redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
end
end
def oauth
@oauth ||= request.env['omniauth.auth']
end
......
......@@ -87,7 +87,7 @@ class Projects::BlobController < Projects::ApplicationController
private
def blob
@blob ||= @repository.blob_at(@commit.id, @path)
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
if @blob
@blob
......
......@@ -56,6 +56,12 @@ class Projects::BuildsController < Projects::ApplicationController
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
def erase
@build.erase(erased_by: current_user)
redirect_to namespace_project_build_path(project.namespace, project, @build),
notice: "Build has been sucessfully erased!"
end
private
def build
......
......@@ -2,6 +2,8 @@
#
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
include CreatesCommit
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
......@@ -9,6 +11,7 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
before_action :define_show_vars, only: [:show, :builds]
before_action :authorize_edit_tree!, only: [:revert]
def show
apply_diff_view_cookie!
......@@ -55,8 +58,37 @@ class Projects::CommitController < Projects::ApplicationController
render layout: false
end
def revert
assign_revert_commit_vars
return render_404 if @target_branch.blank?
create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.",
success_path: successful_revert_path, failure_path: failed_revert_path)
end
private
def revert_type_title
@commit.merged_merge_request ? 'merge request' : 'commit'
end
def successful_revert_path
return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
def failed_revert_path
return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commit_url(@project.namespace, @project, params[:id])
end
def referenced_merge_request_url
namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request)
end
def commit
@commit ||= @project.commit(params[:id])
end
......@@ -79,4 +111,16 @@ class Projects::CommitController < Projects::ApplicationController
@statuses = ci_commit.statuses if ci_commit
end
def assign_revert_commit_vars
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
@mr_source_branch = @commit.revert_branch_name
@mr_target_branch = @target_branch
@commit_params = {
commit: @commit,
revert_type_title: revert_type_title,
create_merge_request: params[:create_merge_request].present? || different_project?
}
end
end
......@@ -32,7 +32,7 @@ class Projects::ForksController < Projects::ApplicationController
if continue_params
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked."
redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
end
end
else
......
......@@ -3,6 +3,7 @@ class Projects::ImportsController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :require_no_repo, only: [:new, :create]
before_action :redirect_if_progress, only: [:new, :create]
before_action :redirect_if_no_import, only: :show
def new
end
......@@ -63,14 +64,19 @@ class Projects::ImportsController < Projects::ApplicationController
def require_no_repo
if @project.repository_exists?
redirect_to(namespace_project_path(@project.namespace, @project))
redirect_to namespace_project_path(@project.namespace, @project)
end
end
def redirect_if_progress
if @project.import_in_progress?
redirect_to namespace_project_import_path(@project.namespace, @project) &&
return
redirect_to namespace_project_import_path(@project.namespace, @project)
end
end
def redirect_if_no_import
if @project.repository_exists? && @project.no_import?
redirect_to namespace_project_path(@project.namespace, @project)
end
end
end
......@@ -32,6 +32,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
@issues = @issues.page(params[:page]).per(PER_PAGE)
@label = @project.labels.find_by(title: params[:label_name])
respond_to do |format|
format.html
......
......@@ -69,7 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
def label_params
params.require(:label).permit(:title, :color)
params.require(:label).permit(:title, :description, :color)
end
def label
......
......@@ -34,6 +34,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:target_project)
@label = @project.labels.find_by(title: params[:label_name])
respond_to do |format|
format.html
format.json do
......@@ -179,6 +181,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
TodoService.new.merge_merge_request(merge_request, current_user)
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
......
......@@ -35,6 +35,7 @@ class Projects::MilestonesController < Projects::ApplicationController
@issues = @milestone.issues
@users = @milestone.participants.uniq
@merge_requests = @milestone.merge_requests
@labels = @milestone.labels
end
def create
......
......@@ -64,9 +64,9 @@ class Projects::RefsController < Projects::ApplicationController
}
end
if @logs.present?
@log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))
@more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))
offset = (@offset + @limit)
if contents.size > offset
@more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
respond_to do |format|
......
......@@ -11,7 +11,9 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
render json: ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute
RepositoryArchiveCacheWorker.perform_async
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
head :ok
rescue => ex
logger.error("#{self.class.name}: #{ex}")
return git_not_found!
......
......@@ -236,7 +236,7 @@ class ProjectsController < ApplicationController
Emoji.emojis.map do |name, emoji|
{
name: name,
path: view_context.image_url("emoji/#{emoji["unicode"]}.png")
path: view_context.image_url("#{emoji["unicode"]}.png")
}
end
end
......
......@@ -119,6 +119,20 @@ class IssuableFinder
labels? && params[:label_name] == Label::None.title
end
def labels
return @labels if defined?(@labels)
if labels? && !filter_by_no_label?
@labels = Label.where(title: label_names)
if projects
@labels = @labels.where(project: projects)
end
else
@labels = Label.none
end
end
def assignee?
params[:assignee_id].present?
end
......@@ -253,8 +267,6 @@ class IssuableFinder
joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id").
where(label_links: { id: nil })
else
label_names = params[:label_name].split(",")
items = items.joins(:labels).where(labels: { title: label_names })
if projects
......@@ -266,6 +278,10 @@ class IssuableFinder
items
end
def label_names
params[:label_name].split(',')
end
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
......
# TodosFinder
#
# Used to filter Todos by set of params
#
# Arguments:
# current_user - which user use
# params:
# action_id: integer
# author_id: integer
# project_id; integer
# state: 'pending' or 'done'
# type: 'Issue' or 'MergeRequest'
#
class TodosFinder
NONE = '0'
attr_accessor :current_user, :params
def initialize(current_user, params)
@current_user = current_user
@params = params
end
def execute
items = current_user.todos
items = by_action_id(items)
items = by_author(items)
items = by_project(items)
items = by_state(items)
items = by_type(items)
items
end
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
end
def action_id
params[:action_id]
end
def author?
params[:author_id].present?
end
def author
return @author if defined?(@author)
@author =
if author? && params[:author_id] != NONE
User.find(params[:author_id])
else
nil
end
end
def project?
params[:project_id].present?
end
def project
return @project if defined?(@project)
if project?
@project = Project.find(params[:project_id])
unless Ability.abilities.allowed?(current_user, :read_project, @project)
@project = nil
end
else
@project = nil
end
@project
end
def type?
type.present? && ['Issue', 'MergeRequest'].include?(type)
end
def type
params[:type]
end
def by_action_id(items)
if action_id?
items = items.where(action: action_id)
end
items
end
def by_author(items)
if author?
items = items.where(author_id: author.try(:id))
end
items
end
def by_project(items)
if project?
items = items.where(project: project)
end
items
end
def by_state(items)
case params[:state]
when 'done'
items.done
else
items.pending
end
end
def by_type(items)
if type?
items = items.where(target_type: type)
end
items
end
end
......@@ -118,12 +118,6 @@ module ApplicationHelper
grouped_options_for_select(options, @ref || @project.default_branch)
end
def emoji_autocomplete_source
# should be an array of strings
# so to_s can be called, because it is sufficient and to_json is too slow
Emoji.names.to_s
end
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
......
......@@ -127,10 +127,6 @@ module BlobHelper
end
end
def blob_svg?(blob)
blob.language && blob.language.name == 'SVG'
end
# SVGs can contain malicious JavaScript; only include whitelisted
# elements and attributes. Note that this whitelist is by no means complete
# and may omit some elements.
......
......@@ -123,6 +123,37 @@ module CommitsHelper
)
end
def revert_commit_link(commit, continue_to_path, btn_class: nil)
return unless current_user
tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request"
if can_collaborate_with_project?
content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
end
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
notice: edit_in_new_fork_notice + ' Try to revert this commit again.',
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(@project.namespace, @project,
namespace_key: current_user.namespace.id,
continue: continue_params)
link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
end
end
def revert_commit_type(commit)
if commit.merged_merge_request
'merge request'
else
'commit'
end
end
protected
# Private: Returns a link to a person. If the person has a matching user and
......
......@@ -69,7 +69,7 @@ module DiffHelper
end
def line_comments
@line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
@line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
end
def organize_comments(type_left, type_right, line_code_left, line_code_right)
......@@ -137,7 +137,7 @@ module DiffHelper
# Always use HTML to handle case where JSON diff rendered this button
params_copy.delete(:format)
link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do
title
end
end
......
......@@ -23,6 +23,7 @@ module NavHelper
if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') ||
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
......
......@@ -11,6 +11,8 @@ module SortingHelper
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
sort_value_upvotes => sort_title_upvotes
}
end
......@@ -54,6 +56,14 @@ module SortingHelper
'Oldest sign in'
end
def sort_title_downvotes
'Least popular'
end
def sort_title_upvotes
'Most popular'
end
def sort_value_oldest_updated
'updated_asc'
end
......@@ -93,4 +103,12 @@ module SortingHelper
def sort_value_oldest_signin
'oldest_sign_in'
end
def sort_value_downvotes
'downvotes_desc'
end
def sort_value_upvotes
'upvotes_desc'
end
end
module TodosHelper
def todos_pending_count
current_user.todos.pending.count
end
def todos_done_count
current_user.todos.done.count
end
def todo_action_name(todo)
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
end
end
def todo_target_link(todo)
target = todo.target_type.titleize.downcase
link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo)
end
def todo_target_path(todo)
anchor = dom_id(todo.note) if todo.note.present?
polymorphic_path([todo.project.namespace.becomes(Namespace),
todo.project, todo.target], anchor: anchor)
end
def todos_filter_params
{
state: params[:state],
project_id: params[:project_id],
author_id: params[:author_id],
type: params[:type],
action_id: params[:action_id],
}
end
def todos_filter_path(options = {})
without = options.delete(:without)
options = todos_filter_params.merge(options)
if without.present?
without.each do |key|
options.delete(key)
end
end
path = request.path
path << "?#{options.to_param}"
path
end
def todo_actions_options
actions = [
OpenStruct.new(id: '', title: 'Any Action'),
OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
]
options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
end
def todo_projects_options
projects = current_user.authorized_projects.sorted_by_activity.non_archived
projects = projects.includes(:namespace)
projects = projects.map do |project|
OpenStruct.new(id: project.id, title: project.name_with_namespace)
end
projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
end
def todo_types_options
types = [
OpenStruct.new(title: 'Any Type', name: ''),
OpenStruct.new(title: 'Issue', name: 'Issue'),
OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
]
options_from_collection_for_select(types, 'name', 'title', params[:type])
end
end
......@@ -56,8 +56,7 @@ module TreeHelper
return false unless on_top_of_branch?(project, ref)
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
can_collaborate_with_project?(project)
end
def tree_edit_branch(project = @project, ref = @ref)
......
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
# This method prevents the decorated object from evaluating to "truthy" when
# given a nil value. For example:
#
# blob = Blob.new(nil)
# puts "truthy" if blob # => "truthy"
#
# blob = Blob.decorate(nil)
# puts "truthy" if blob # No output
def self.decorate(blob)
return if blob.nil?
new(blob)
end
def svg?
text? && language && language.name == 'SVG'
end
def to_partial_path
if lfs_pointer?
'download'
elsif image? || svg?
'image'
elsif text?
'text'
else
'download'
end
end
end
......@@ -31,15 +31,19 @@
# artifacts_file :text
# gl_project_id :integer
# artifacts_metadata :text
# erased_by_id :integer
# erased_at :datetime
#
module Ci
class Build < CommitStatus
include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User'
serialize :options
......@@ -103,23 +107,22 @@ module Ci
end
state_machine :status, initial: :pending do
after_transition pending: :running do |build, transition|
after_transition pending: :running do |build|
build.execute_hooks
end
after_transition any => [:success, :failed, :canceled] do |build, transition|
return unless build.project
# We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
around_transition any => [:success, :failed, :canceled] do |build, block|
block.call
build.commit.create_next_builds(build) if build.commit
end
after_transition any => [:success, :failed, :canceled] do |build|
build.update_coverage
build.commit.create_next_builds(build)
build.execute_hooks
end
end
def ignored?
failed? && allow_failure?
end
def retryable?
project.builds_enabled? && commands.present?
end
......@@ -179,6 +182,7 @@ module Ci
end
def update_coverage
return unless project
coverage_regex = project.build_coverage_regex
return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
......@@ -203,6 +207,10 @@ module Ci
end
end
def has_trace?
raw_trace.present?
end
def raw_trace
if File.file?(path_to_trace)
File.read(path_to_trace)
......@@ -330,6 +338,7 @@ module Ci
end
def execute_hooks
return unless project
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
......@@ -359,6 +368,33 @@ module Ci
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
end
def erase(opts = {})
return false unless erasable?
remove_artifacts_file!
remove_artifacts_metadata!
erase_trace!
update_erased!(opts[:erased_by])
end
def erasable?
complete? && (artifacts? || has_trace?)
end
def erased?
!self.erased_at.nil?
end
private
def erase_trace!
self.trace = nil
end
def update_erased!(user = nil)
self.update(erased_by: user, erased_at: Time.now)
end
private
def yaml_variables
......
......@@ -22,6 +22,7 @@ module Ci
extend Ci::Model
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online']
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
......@@ -38,6 +39,11 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
acts_as_taggable
def self.search(query)
......
......@@ -215,6 +215,44 @@ class Commit
ci_commit.try(:status) || :not_found
end
def revert_branch_name
"revert-#{short_id}"
end
def revert_description
if merged_merge_request
"This reverts merge request #{merged_merge_request.to_reference}"
else
"This reverts commit #{sha}"
end
end
def revert_message
%Q{Revert "#{title}"\n\n#{revert_description}}
end
def reverts_commit?(commit)
description? && description.include?(commit.revert_description)
end
def merge_commit?
parents.size > 1
end
def merged_merge_request
return @merged_merge_request if defined?(@merged_merge_request)
@merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit?
end
def has_been_reverted?(current_user = nil, noteable = self)
Gitlab::ReferenceExtractor.lazily do
noteable.notes.system.flat_map do |note|
note.all_references(current_user).commits
end
end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
end
private
def repo_changes
......
......@@ -75,16 +75,16 @@ class CommitStatus < ActiveRecord::Base
transition [:pending, :running] => :canceled
end
after_transition pending: :running do |build, transition|
build.update_attributes started_at: Time.now
after_transition pending: :running do |commit_status|
commit_status.update_attributes started_at: Time.now
end
after_transition any => [:success, :failed, :canceled] do |build, transition|
build.update_attributes finished_at: Time.now
after_transition any => [:success, :failed, :canceled] do |commit_status|
commit_status.update_attributes finished_at: Time.now
end
after_transition [:pending, :running] => :success do |build, transition|
MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build)
after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
state :pending, value: 'pending'
......@@ -113,6 +113,10 @@ class CommitStatus < ActiveRecord::Base
canceled? || success? || failed?
end
def ignored?
failed? && allow_failure?
end
def duration
if started_at && finished_at
finished_at - started_at
......
......@@ -69,10 +69,35 @@ module Issuable
case method.to_s
when 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
else
order_by(method)
end
end
def order_downvotes_desc
order_votes_desc('thumbsdown')
end
def order_upvotes_desc
order_votes_desc('thumbsup')
end
def order_votes_desc(award_emoji_name)
issuable_table = self.arel_table
note_table = Note.arel_table
join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
note_table[:noteable_id].eq(issuable_table[:id]).and(
note_table[:noteable_type].eq(self.name).and(
note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
)
)
).join_sources
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
end
end
def today?
......
......@@ -9,6 +9,7 @@
# created_at :datetime
# updated_at :datetime
# template :boolean default(FALSE)
# description :string(255)
#
class Label < ActiveRecord::Base
......@@ -85,6 +86,10 @@ class Label < ActiveRecord::Base
issues.opened.count
end
def closed_issues_count
issues.closed.count
end
def template?
template
end
......
......@@ -24,6 +24,7 @@
# merge_params :text
# merge_when_build_succeeds :boolean default(FALSE), not null
# merge_user_id :integer
# merge_commit_sha :string
#
require Rails.root.join("app/models/commit")
......@@ -137,7 +138,7 @@ class MergeRequest < ActiveRecord::Base
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :opened, -> { with_state(:opened) }
scope :opened, -> { with_states(:opened, :reopened) }
scope :merged, -> { with_state(:merged) }
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
......@@ -532,4 +533,12 @@ class MergeRequest < ActiveRecord::Base
[diff_base_commit, last_commit]
end
def merge_commit
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
def can_be_reverted?(current_user = nil)
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
end
......@@ -27,6 +27,7 @@ class Milestone < ActiveRecord::Base
belongs_to :project
has_many :issues
has_many :labels, through: :issues
has_many :merge_requests
has_many :participants, through: :issues, source: :assignee
......@@ -109,6 +110,19 @@ class Milestone < ActiveRecord::Base
0
end
# Returns the elapsed time (in percent) since the Milestone creation date until today.
# If the Milestone doesn't have a due_date then returns 0 since we can't calculate the elapsed time.
# If the Milestone is overdue then it returns 100%.
def percent_time_used
return 0 unless due_date
return 100 if expired?
duration = ((created_at - due_date.to_datetime) / 1.day)
days_elapsed = ((created_at - Time.now) / 1.day)
((days_elapsed.to_f / duration) * 100).floor
end
def expires_at
if due_date
if due_date.past?
......
......@@ -37,6 +37,8 @@ class Note < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
has_many :todos, dependent: :destroy
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
......@@ -375,6 +377,7 @@ class Note < ActiveRecord::Base
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
end
......@@ -382,7 +385,7 @@ class Note < ActiveRecord::Base
private
def awards_supported?
noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
(noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)) && !for_diff_line?
end
def contains_emoji_only?
......
......@@ -382,6 +382,10 @@ class Project < ActiveRecord::Base
external_import? || forked?
end
def no_import?
import_status == 'none'
end
def external_import?
import_url.present?
end
......
......@@ -136,7 +136,7 @@ class ProjectTeam
end
def human_max_access(user_id)
Gitlab::Access.options.key max_member_access(user_id)
Gitlab::Access.options_with_owner.key(max_member_access(user_id))
end
# This method assumes project and group members are eager loaded for optimal
......
......@@ -23,13 +23,11 @@ class Repository
def raw_repository
return nil unless path_with_namespace
@raw_repository ||= begin
repo = Gitlab::Git::Repository.new(path_to_repo)
repo.autocrlf = :input
repo
rescue Gitlab::Git::Repository::NoRepository
nil
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
end
def update_autocrlf_option
raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
end
# Return absolute path to repository
......@@ -40,7 +38,12 @@ class Repository
end
def exists?
raw_repository
return false unless raw_repository
raw_repository.rugged
true
rescue Gitlab::Git::Repository::NoRepository
false
end
def empty?
......@@ -67,7 +70,7 @@ class Repository
end
def commit(id = 'HEAD')
return nil unless raw_repository
return nil unless exists?
commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit, @project) if commit
commit
......@@ -236,6 +239,19 @@ class Repository
end
expire_branch_cache(branch_name)
# This ensures this particular cache is flushed after the first commit to a
# new repository.
expire_emptiness_caches if empty?
end
# Expires _all_ caches, including those that would normally only be expired
# under specific conditions.
def expire_all_caches!
expire_cache
expire_root_ref_cache
expire_emptiness_caches
expire_has_visible_content_cache
end
def expire_branch_cache(branch_name = nil)
......@@ -258,6 +274,14 @@ class Repository
@root_ref = nil
end
# Expires the cache(s) used to determine if a repository is empty or not.
def expire_emptiness_caches
cache.expire(:empty?)
@empty = nil
expire_has_visible_content_cache
end
def expire_has_visible_content_cache
cache.expire(:has_visible_content?)
@has_visible_content = nil
......@@ -599,6 +623,34 @@ class Repository
end
end
def revert(user, commit, base_branch, target_branch = nil)
source_sha = find_branch(base_branch).target
target_branch ||= base_branch
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
tree_id = revert_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
commit_with_hooks(user, target_branch) do |ref|
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message,
author: committer,
committer: committer,
tree: tree_id,
parents: [rugged.lookup(source_sha)],
update_ref: ref)
end
end
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
......@@ -611,6 +663,8 @@ class Repository
end
def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
rugged.merge_base(first_commit_id, second_commit_id)
rescue Rugged::ReferenceError
nil
......@@ -674,12 +728,15 @@ class Repository
end
def commit_with_hooks(current_user, branch)
update_autocrlf_option
oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
target_branch = find_branch(branch)
was_empty = empty?
unless was_empty
oldrev = find_branch(branch).target
if !was_empty && target_branch
oldrev = target_branch.target
end
with_tmp_ref(oldrev) do |tmp_ref|
......@@ -691,7 +748,7 @@ class Repository
end
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
if was_empty
if was_empty || !target_branch
# Create branch
rugged.references.create(ref, newrev)
else
......@@ -706,6 +763,8 @@ class Repository
end
end
end
newrev
end
end
......
# == Schema Information
#
# Table name: todos
#
# id :integer not null, primary key
# user_id :integer not null
# project_id :integer not null
# target_id :integer not null
# target_type :string not null
# author_id :integer
# note_id :integer
# action :integer not null
# state :string not null
# created_at :datetime
# updated_at :datetime
#
class Todo < ActiveRecord::Base
ASSIGNED = 1
MENTIONED = 2
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
belongs_to :target, polymorphic: true, touch: true
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target, :user, presence: true
default_scope { reorder(id: :desc) }
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
state_machine :state, initial: :pending do
event :done do
transition [:pending, :done] => :done
end
state :pending
state :done
end
def body
if note.present?
note.note
else
target.title
end
end
end
......@@ -143,7 +143,7 @@ class User < ActiveRecord::Base
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy
#
# Validations
......
class ArchiveRepositoryService
attr_reader :project, :ref, :format
def initialize(project, ref, format)
format ||= 'tar.gz'
@project, @ref, @format = project, ref, format.downcase
end
def execute(options = {})
RepositoryArchiveCacheWorker.perform_async
metadata = project.repository.archive_metadata(ref, storage_path, format)
raise "Repository or ref not found" if metadata.empty?
metadata
end
private
def storage_path
Gitlab.config.gitlab.repository_downloads_path
end
end
......@@ -23,6 +23,10 @@ class BaseService
EventCreateService.new
end
def todo_service
TodoService.new
end
def log_info(message)
Gitlab::AppLogger.info message
end
......
......@@ -34,6 +34,7 @@ module Ci
build = commit.builds.create!(build_attrs)
build.execute_hooks
build
end
end
end
......
module Commits
class RevertService < ::BaseService
class ValidationError < StandardError; end
class ReversionError < StandardError; end
def execute
@source_project = params[:source_project] || @project
@target_branch = params[:target_branch]
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
validate and commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ReversionError => ex
error(ex.message)
end
def commit
revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
if @create_merge_request
# Temporary branch exists and contains the revert commit
return success if repository.find_branch(revert_into)
create_target_branch
end
unless repository.revert(current_user, @commit, revert_into)
error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
It may have already been reverted, or a more recent commit may have updated some of its content."
raise ReversionError, error_msg
end
success
end
private
def create_target_branch
result = CreateBranchService.new(@project, current_user)
.execute(@commit.revert_branch_name, @target_branch, source_project: @source_project)
if result[:status] == :error
raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
end
end
def validate
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed
raise_error('You are not allowed to push into this branch')
end
true
end
end
end
class GitPushService
attr_accessor :project, :user, :push_data, :push_commits
class GitPushService < BaseService
attr_accessor :push_data, :push_commits
include Gitlab::CurrentSettings
include Gitlab::Access
# This method will be called after each git update
# and only if the provided user and project is present in GitLab.
# and only if the provided user and project are present in GitLab.
#
# All callbacks for post receive action should be placed here.
#
......@@ -15,67 +15,67 @@ class GitPushService
# 4. Executes the project's web hooks
# 5. Executes the project's services
#
def execute(project, user, oldrev, newrev, ref)
@project, @user = project, user
branch_name = Gitlab::Git.ref_name(ref)
project.repository.expire_cache(branch_name)
if push_remove_branch?(ref, newrev)
project.repository.expire_has_visible_content_cache
def execute
@project.repository.expire_cache(branch_name)
if push_remove_branch?
@project.repository.expire_has_visible_content_cache
@push_commits = []
elsif push_to_new_branch?(ref, oldrev)
project.repository.expire_has_visible_content_cache
elsif push_to_new_branch?
@project.repository.expire_has_visible_content_cache
# Re-find the pushed commits.
if is_default_branch?(ref)
if is_default_branch?
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
@push_commits = project.repository.commits(newrev)
# Ensure HEAD points to the default branch in case it is not master
project.change_head(branch_name)
# Set protection on the default branch if configured
if (current_application_settings.default_branch_protection != PROTECTION_NONE)
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push })
end
process_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)
@push_commits = @project.repository.commits_between(@project.default_branch, params[:newrev])
# don't process commits for the initial push to the default branch
process_commit_messages(ref)
process_commit_messages
end
elsif push_to_existing_branch?(ref, oldrev)
elsif push_to_existing_branch?
# Collect data for this git push
@push_commits = project.repository.commits_between(oldrev, newrev)
process_commit_messages(ref)
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages
end
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
project.update_merge_requests(oldrev, newrev, ref, @user)
update_merge_requests
end
@push_data = build_push_data(oldrev, newrev, ref)
protected
EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup, :push_hooks)
CreateCommitBuildsService.new.execute(project, @user, @push_data)
ProjectCacheWorker.perform_async(project.id)
def update_merge_requests
@project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
ProjectCacheWorker.perform_async(@project.id)
end
protected
def process_default_branch
@push_commits = project.repository.commits(params[:newrev])
# Ensure HEAD points to the default branch in case it is not master
project.change_head(branch_name)
# Set protection on the default branch if configured
if (current_application_settings.default_branch_protection != PROTECTION_NONE)
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
end
end
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
def process_commit_messages(ref)
is_default_branch = is_default_branch?(ref)
def process_commit_messages
is_default_branch = is_default_branch?
authors = Hash.new do |hash, commit|
email = commit.author_email
......@@ -94,7 +94,7 @@ class GitPushService
# Close issues if these commits were pushed to the project's default branch and the commit message matches the
# closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
# a different branch.
closed_issues = commit.closes_issues(user)
closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue|
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit)
end
......@@ -104,34 +104,38 @@ class GitPushService
end
end
def build_push_data(oldrev, newrev, ref)
Gitlab::PushDataBuilder.
build(project, user, oldrev, newrev, ref, push_commits)
def build_push_data
@push_data ||= Gitlab::PushDataBuilder.
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
end
def push_to_existing_branch?(ref, oldrev)
def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev)
Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
end
def push_to_new_branch?(ref, oldrev)
Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev)
def push_to_new_branch?
Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:oldrev])
end
def push_remove_branch?(ref, newrev)
Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev)
def push_remove_branch?
Gitlab::Git.branch_ref?(params[:ref]) && Gitlab::Git.blank_ref?(params[:newrev])
end
def push_to_branch?(ref)
Gitlab::Git.branch_ref?(ref)
def push_to_branch?
Gitlab::Git.branch_ref?(params[:ref])
end
def is_default_branch?(ref)
Gitlab::Git.branch_ref?(ref) &&
(Gitlab::Git.ref_name(ref) == project.default_branch || project.default_branch.nil?)
def is_default_branch?
Gitlab::Git.branch_ref?(params[:ref]) &&
(Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?)
end
def commit_user(commit)
commit.author || user
commit.author || current_user
end
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
end
......@@ -54,7 +54,7 @@ class IssuableBaseService < BaseService
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
handle_changes(issuable)
handle_changes(issuable, old_labels: old_labels)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
......@@ -71,6 +71,19 @@ class IssuableBaseService < BaseService
end
end
def has_changes?(issuable, options = {})
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
end
old_labels = options[:old_labels]
labels_changed = old_labels && issuable.labels != old_labels
attrs_changed || labels_changed
end
def handle_common_system_notes(issuable, options = {})
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
......
......@@ -3,6 +3,7 @@ module Issues
def execute(issue, commit = nil)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
return issue
end
......@@ -10,6 +11,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, commit)
notification_service.close_issue(issue, current_user)
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
end
......
......@@ -9,6 +9,7 @@ module Issues
if issue.save
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
todo_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
issue.create_cross_references!(current_user)
execute_hooks(issue, 'open')
......
......@@ -4,7 +4,16 @@ module Issues
update(issue)
end
def handle_changes(issue)
def handle_changes(issue, options = {})
if has_changes?(issue, options)
todo_service.mark_pending_todos_as_done(issue, current_user)
end
if issue.previous_changes.include?('title') ||
issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user)
end
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
......@@ -12,6 +21,7 @@ module Issues
if issue.previous_changes.include?('assignee_id')
create_assignee_note(issue)
notification_service.reassigned_issue(issue, current_user)
todo_service.reassigned_issue(issue, current_user)
end
end
......
......@@ -56,7 +56,7 @@ module MergeRequests
if commits && commits.count == 1
commit = commits.first
merge_request.title = commit.title
merge_request.description = commit.description.try(:strip)
merge_request.description ||= commit.description.try(:strip)
else
merge_request.title = merge_request.source_branch.titleize.humanize
end
......
......@@ -9,6 +9,7 @@ module MergeRequests
event_service.close_mr(merge_request, current_user)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
end
......
......@@ -18,6 +18,7 @@ module MergeRequests
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)
todo_service.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(current_user)
execute_hooks(merge_request)
end
......
......@@ -34,7 +34,8 @@ module MergeRequests
committer: committer
}
repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
merge_request.update(merge_commit_sha: commit_id)
rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
......
......@@ -19,17 +19,21 @@ module MergeRequests
end
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(build)
merge_requests = merge_request_from(build)
def trigger(commit_status)
merge_requests = merge_request_from(commit_status)
merge_requests.each do |merge_request|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
next unless ci_commit.success?
if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end
end
end
# Cancels the automatic merge
def cancel(merge_request)
......@@ -45,9 +49,16 @@ module MergeRequests
private
def merge_request_from(build)
merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
......
......@@ -14,7 +14,16 @@ module MergeRequests
update(merge_request)
end
def handle_changes(merge_request)
def handle_changes(merge_request, options = {})
if has_changes?(merge_request, options)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
if merge_request.previous_changes.include?('title') ||
merge_request.previous_changes.include?('description')
todo_service.update_merge_request(merge_request, current_user)
end
if merge_request.previous_changes.include?('target_branch')
create_branch_change_note(merge_request, 'target',
merge_request.previous_changes['target_branch'].first,
......@@ -28,6 +37,7 @@ module MergeRequests
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
todo_service.reassigned_merge_request(merge_request, current_user)
end
if merge_request.previous_changes.include?('target_branch') ||
......
......@@ -8,6 +8,7 @@ module Notes
if note.save
# Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params)
TodoService.new.new_note(note, current_user)
end
note
......
module Notes
class PostProcessService
attr_accessor :note
def initialize(note)
......@@ -25,6 +24,5 @@ module Notes
@note.project.execute_hooks(note_data, :note_hooks)
@note.project.execute_services(note_data, :note_hooks)
end
end
end
......@@ -7,6 +7,10 @@ module Notes
note.create_new_cross_references!(current_user)
note.reset_events_cache
if note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user)
end
note
end
end
......
......@@ -16,11 +16,15 @@ module Projects
return false unless can?(current_user, :remove_project, project)
project.team.truncate
project.repository.expire_cache unless project.empty_repo?
repo_path = project.path_with_namespace
wiki_path = repo_path + '.wiki'
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
flush_caches(project, wiki_path)
Project.transaction do
project.destroy!
......@@ -70,5 +74,13 @@ module Projects
def removal_path(path)
"#{path}+#{project.id}#{DELETED_FLAG}"
end
def flush_caches(project, wiki_path)
project.repository.expire_all_caches! if project.repository.exists?
wiki_repo = Repository.new(wiki_path, project)
wiki_repo.expire_all_caches! if wiki_repo.exists?
end
end
end
# TodoService class
#
# Used for creating todos after certain user actions
#
# Ex.
# TodoService.new.new_issue(issue, current_user)
#
class TodoService
# When create an issue we should:
#
# * create a todo for assignee if issue is assigned
# * create a todo for each mentioned user on issue
#
def new_issue(issue, current_user)
new_issuable(issue, current_user)
end
# When update an issue we should:
#
# * mark all pending todos related to the issue for the current user as done
#
def update_issue(issue, current_user)
create_mention_todos(issue.project, issue, current_user)
end
# When close an issue we should:
#
# * mark all pending todos related to the target for the current user as done
#
def close_issue(issue, current_user)
mark_pending_todos_as_done(issue, current_user)
end
# When we reassign an issue we should:
#
# * create a pending todo for new assignee if issue is assigned
#
def reassigned_issue(issue, current_user)
create_assignment_todo(issue, current_user)
end
# When create a merge request we should:
#
# * creates a pending todo for assignee if merge request is assigned
# * create a todo for each mentioned user on merge request
#
def new_merge_request(merge_request, current_user)
new_issuable(merge_request, current_user)
end
# When update a merge request we should:
#
# * create a todo for each mentioned user on merge request
#
def update_merge_request(merge_request, current_user)
create_mention_todos(merge_request.project, merge_request, current_user)
end
# When close a merge request we should:
#
# * mark all pending todos related to the target for the current user as done
#
def close_merge_request(merge_request, current_user)
mark_pending_todos_as_done(merge_request, current_user)
end
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
#
def reassigned_merge_request(merge_request, current_user)
create_assignment_todo(merge_request, current_user)
end
# When merge a merge request we should:
#
# * mark all pending todos related to the target for the current user as done
#
def merge_merge_request(merge_request, current_user)
mark_pending_todos_as_done(merge_request, current_user)
end
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
# * create a todo for each mentioned user on note
#
def new_note(note, current_user)
handle_note(note, current_user)
end
# When update a note we should:
#
# * mark all pending todos related to the noteable for the current user as done
# * create a todo for each new user mentioned on note
#
def update_note(note, current_user)
handle_note(note, current_user)
end
# When marking pending todos as done we should:
#
# * mark all pending todos related to the target for the current user as done
#
def mark_pending_todos_as_done(target, user)
pending_todos(user, target.project, target).update_all(state: :done)
end
private
def create_todos(project, target, author, users, action, note = nil)
Array(users).each do |user|
next if pending_todos(user, project, target).exists?
Todo.create(
project: project,
user_id: user.id,
author_id: author.id,
target_id: target.id,
target_type: target.class.name,
action: action,
note: note
)
end
end
def new_issuable(issuable, author)
create_assignment_todo(issuable, author)
create_mention_todos(issuable.project, issuable, author)
end
def handle_note(note, author)
# Skip system notes, like status changes and cross-references
return if note.system
project = note.project
target = note.noteable
mark_pending_todos_as_done(target, author)
create_mention_todos(project, target, author, note)
end
def create_assignment_todo(issuable, author)
if issuable.assignee && issuable.assignee != author
create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
end
end
def create_mention_todos(project, issuable, author, note = nil)
mentioned_users = filter_mentioned_users(project, note || issuable, author)
create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note)
end
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users.select do |user|
user.can?(:read_project, project)
end
mentioned_users.delete(author)
mentioned_users.uniq
end
def pending_todos(user, project, target)
user.todos.pending.where(
project_id: project.id,
target_id: target.id,
target_type: target.class.name
)
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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