Commit f4d43ed3 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' into ce-upstream

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

Conflicts:
	app/controllers/projects/services_controller.rb
	app/models/concerns/mentionable.rb
	app/models/project.rb
	app/models/user.rb
	app/views/admin/users/index.html.haml
	app/views/projects/services/_form.html.haml
	config/gitlab.yml.example
	config/initializers/1_settings.rb
	lib/gitlab/git_access.rb
	lib/gitlab/ldap/access.rb
	lib/gitlab/ldap/person.rb
	lib/gitlab/reference_extractor.rb
	spec/lib/gitlab/ldap/access_spec.rb
parents 9d2840fb e3bd17a7
...@@ -12,9 +12,21 @@ v 7.4.0 ...@@ -12,9 +12,21 @@ v 7.4.0
- API: Add support for forking a project via the API (Bernhard Kaindl) - API: Add support for forking a project via the API (Bernhard Kaindl)
- API: filter project issues by milestone (Julien Bianchi) - API: filter project issues by milestone (Julien Bianchi)
- Fail harder in the backup script - Fail harder in the backup script
- Changes to Slack service structure, only webhook url needed
- Zen mode for wiki and milestones (Robert Schilling) - Zen mode for wiki and milestones (Robert Schilling)
- Move Emoji parsing to html-pipeline-gitlab (Robert Schilling) - Move Emoji parsing to html-pipeline-gitlab (Robert Schilling)
- Font Awesome 4.2 integration (Sullivan Senechal) - Font Awesome 4.2 integration (Sullivan Senechal)
- Add Pushover service integration (Sullivan Senechal)
- Add select field type for services options (Sullivan Senechal)
- Add cross-project references to the Markdown parser (Vinnie Okada)
- Add task lists to issue and merge request descriptions (Vinnie Okada)
- Snippets can be public, internal or private
- Improve danger zone: ask project path to confirm data-loss action
- Raise exception on forgery
- Show build coverage in Merge Requests (requires GitLab CI v5.1)
- New milestone and label links on issue edit form
- Improved repository graphs
- Improve event note display in dashboard and project activity views (Vinnie Okada)
v 7.3.2 v 7.3.2
- Fix creating new file via web editor - Fix creating new file via web editor
......
...@@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic ...@@ -10,7 +10,7 @@ By submitting code as an individual you agree to the [individual contributor lic
## Security vulnerability disclosure ## Security vulnerability disclosure
Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://www.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities. Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Closing policy for issues and merge requests ## Closing policy for issues and merge requests
...@@ -22,7 +22,7 @@ Issues and merge requests should be in English and contain appropriate language ...@@ -22,7 +22,7 @@ Issues and merge requests should be in English and contain appropriate language
## Issue tracker ## Issue tracker
To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://www.gitlab.com/subscription/) and [consulting services](http://www.gitlab.com/consultancy/) are available from [GitLab.com](http://www.gitlab.com/). To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/).
The [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue. The [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue.
......
...@@ -31,7 +31,7 @@ gem 'omniauth-shibboleth' ...@@ -31,7 +31,7 @@ gem 'omniauth-shibboleth'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem "gitlab_git", '7.0.0.rc8' gem "gitlab_git", '7.0.0.rc9'
# Ruby/Rack Git Smart-HTTP Server Handler # Ruby/Rack Git Smart-HTTP Server Handler
gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack' gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack'
......
...@@ -168,7 +168,7 @@ GEM ...@@ -168,7 +168,7 @@ GEM
multi_json multi_json
gitlab-grack (2.0.0.pre) gitlab-grack (2.0.0.pre)
rack (~> 1.5.1) rack (~> 1.5.1)
gitlab-grit (2.6.11) gitlab-grit (2.6.12)
charlock_holmes (~> 0.6) charlock_holmes (~> 0.6)
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (~> 1.15) mime-types (~> 1.15)
...@@ -179,7 +179,7 @@ GEM ...@@ -179,7 +179,7 @@ GEM
mime-types (~> 1.19) mime-types (~> 1.19)
gitlab_emoji (0.0.1.1) gitlab_emoji (0.0.1.1)
emoji (~> 1.0.1) emoji (~> 1.0.1)
gitlab_git (7.0.0.rc8) gitlab_git (7.0.0.rc9)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.6) charlock_holmes (~> 0.6)
gitlab-linguist (~> 3.0) gitlab-linguist (~> 3.0)
...@@ -241,9 +241,11 @@ GEM ...@@ -241,9 +241,11 @@ GEM
html-pipeline (1.11.0) html-pipeline (1.11.0)
activesupport (>= 2) activesupport (>= 2)
nokogiri (~> 1.4) nokogiri (~> 1.4)
html-pipeline-gitlab (0.1.0) html-pipeline-gitlab (0.1.5)
gitlab_emoji (~> 0.0.1.1) actionpack (~> 4)
gitlab_emoji (~> 0.0.1)
html-pipeline (~> 1.11.0) html-pipeline (~> 1.11.0)
sanitize (~> 2.1)
http_parser.rb (0.5.3) http_parser.rb (0.5.3)
httparty (0.13.0) httparty (0.13.0)
json (~> 1.8) json (~> 1.8)
...@@ -622,7 +624,7 @@ DEPENDENCIES ...@@ -622,7 +624,7 @@ DEPENDENCIES
gitlab-grack (~> 2.0.0.pre) gitlab-grack (~> 2.0.0.pre)
gitlab-linguist (~> 3.0.0) gitlab-linguist (~> 3.0.0)
gitlab_emoji (~> 0.0.1.1) gitlab_emoji (~> 0.0.1.1)
gitlab_git (= 7.0.0.rc8) gitlab_git (= 7.0.0.rc9)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (= 1.1.0) gitlab_omniauth-ldap (= 1.1.0)
gollum-lib (~> 3.0.0) gollum-lib (~> 3.0.0)
......
...@@ -18,7 +18,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co ...@@ -18,7 +18,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co
- Responds to merge requests the issue team mentions them in and monitors for new merge requests - Responds to merge requests the issue team mentions them in and monitors for new merge requests
- Provides feedback to the merge request submitter to improve the merge request (style, tests, etc.) - Provides feedback to the merge request submitter to improve the merge request (style, tests, etc.)
- Mark merge requests 'ready-for-merge' when they meet the contribution guidelines - Mark merge requests 'ready-for-merge' when they meet the contribution guidelines
- Mention developer(s) based on the [list of members and their specialities](https://www.gitlab.com/core-team/) - Mention developer(s) based on the [list of members and their specialities](https://about.gitlab.com/core-team/)
- Closes merge requests with no feedback from the reporter for two weeks - Closes merge requests with no feedback from the reporter for two weeks
## Priorities of the issue team ## Priorities of the issue team
...@@ -30,7 +30,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co ...@@ -30,7 +30,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co
## Mentioning people ## Mentioning people
The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issue. Please select someone with relevant experience from [GitLab core team](https://www.gitlab.com/core-team/). If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person. The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issue. Please select someone with relevant experience from [GitLab core team](https://about.gitlab.com/core-team/). If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person.
## Workflow labels ## Workflow labels
...@@ -79,7 +79,7 @@ Thanks for the issue report but we only support issues for the latest stable ver ...@@ -79,7 +79,7 @@ Thanks for the issue report but we only support issues for the latest stable ver
### Support requests and configuration questions ### Support requests and configuration questions
Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://www.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please use the \[support forum\]\(https://groups.google.com/forum/#!forum/gitlabhq), \[Stack Overflow\]\(http://stackoverflow.com/questions/tagged/gitlab), the #gitlab IRC channel on Freenode or the http://about.gitlab.com paid services for this purpose. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information.
### Code format ### Code format
......
...@@ -177,6 +177,13 @@ $ -> ...@@ -177,6 +177,13 @@ $ ->
$(@).closest(".diff-file").find(".notes_holder").toggle() $(@).closest(".diff-file").find(".notes_holder").toggle()
e.preventDefault() e.preventDefault()
$(document).on "click", '.js-confirm-danger', (e) ->
e.preventDefault()
btn = $(e.target)
text = btn.data("confirm-danger-message")
form = btn.closest("form")
new ConfirmDangerModal(form, text)
(($) -> (($) ->
# Disable an element and add the 'disabled' Bootstrap class # Disable an element and add the 'disabled' Bootstrap class
$.fn.extend disable: -> $.fn.extend disable: ->
......
window.updateTaskState = (taskableType) ->
objType = taskableType.data
isChecked = $(this).prop("checked")
if $(this).is(":checked")
stateEvent = "task_check"
else
stateEvent = "task_uncheck"
taskableUrl = $("form.edit-" + objType).first().attr("action")
taskableNum = taskableUrl.match(/\d+$/)
taskNum = 0
$("li.task-list-item input:checkbox").each( (index, e) =>
if e == this
taskNum = index + 1
)
$.ajax
type: "PATCH"
url: taskableUrl
data: objType + "[state_event]=" + stateEvent +
"&" + objType + "[task_num]=" + taskNum
class ConfirmDangerModal
constructor: (form, text) ->
@form = form
$('.js-confirm-text').text(text || '')
$('.js-confirm-danger-input').val('')
$('#modal-confirm-danger').modal('show')
project_path = $('.js-confirm-danger-match').text()
submit = $('.js-confirm-danger-submit')
submit.disable()
$('.js-confirm-danger-input').on 'input', ->
if rstrip($(@).val()) is project_path
submit.enable()
else
submit.disable()
$('.js-confirm-danger-submit').on 'click', =>
@form.submit()
@ConfirmDangerModal = ConfirmDangerModal
...@@ -6,4 +6,14 @@ class Issue ...@@ -6,4 +6,14 @@ class Issue
$(".issue-box .inline-update").on "change", "#issue_assignee_id", -> $(".issue-box .inline-update").on "change", "#issue_assignee_id", ->
$(this).submit() $(this).submit()
if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false)
$(".task-list-item input:checkbox").on(
"click"
null
"issue"
updateTaskState
)
@Issue = Issue @Issue = Issue
...@@ -17,6 +17,8 @@ class MergeRequest ...@@ -17,6 +17,8 @@ class MergeRequest
disableButtonIfEmptyField '#commit_message', '.accept_merge_request' disableButtonIfEmptyField '#commit_message', '.accept_merge_request'
if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false)
# Local jQuery finder # Local jQuery finder
$: (selector) -> $: (selector) ->
...@@ -72,6 +74,13 @@ class MergeRequest ...@@ -72,6 +74,13 @@ class MergeRequest
this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_in_progress').hide()
this.$('.remove_source_branch_widget.failed').show() this.$('.remove_source_branch_widget.failed').show()
$(".task-list-item input:checkbox").on(
"click"
null
"merge_request"
updateTaskState
)
activateTab: (action) -> activateTab: (action) ->
this.$('.merge-request-tabs li').removeClass 'active' this.$('.merge-request-tabs li').removeClass 'active'
this.$('.tab-content').hide() this.$('.tab-content').hide()
...@@ -96,14 +105,6 @@ class MergeRequest ...@@ -96,14 +105,6 @@ class MergeRequest
else else
$('.ci_widget.ci-error').show() $('.ci_widget.ci-error').show()
switch state
when "success"
$('.mr-state-widget').addClass("panel-success")
when "failed"
$('.mr-state-widget').addClass("panel-danger")
when "running", "pending"
$('.mr-state-widget').addClass("panel-warning")
showCiCoverage: (coverage) -> showCiCoverage: (coverage) ->
cov_html = $('<span>') cov_html = $('<span>')
cov_html.addClass('ci-coverage') cov_html.addClass('ci-coverage')
......
...@@ -365,3 +365,6 @@ table { ...@@ -365,3 +365,6 @@ table {
font-size: 42px; font-size: 42px;
} }
.task-status {
margin-left: 10px;
}
...@@ -10,8 +10,7 @@ ...@@ -10,8 +10,7 @@
.issue-box { .issue-box {
color: #555; color: #555;
margin:20px 0; margin:20px 0;
background: #f9f9f9; background: $box_bg;
border-top-left-radius: 5px;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09));
&.issue-box-closed { &.issue-box-closed {
...@@ -112,7 +111,6 @@ ...@@ -112,7 +111,6 @@
float: left; float: left;
font-weight: bold; font-weight: bold;
padding: 10px 15px; padding: 10px 15px;
border-top-left-radius: 5px;
} }
.creator { .creator {
......
...@@ -122,3 +122,7 @@ ul.bordered-list { ...@@ -122,3 +122,7 @@ ul.bordered-list {
} }
} }
} }
li.task-list-item {
list-style-type: none;
}
...@@ -233,8 +233,8 @@ $list-group-active-bg: $bg_primary; ...@@ -233,8 +233,8 @@ $list-group-active-bg: $bg_primary;
} }
.form-actions { .form-actions {
margin-bottom: 0; margin: -15px;
background: #FFF; margin-top: 18px;
} }
} }
...@@ -262,53 +262,33 @@ $list-group-active-bg: $bg_primary; ...@@ -262,53 +262,33 @@ $list-group-active-bg: $bg_primary;
} }
.panel-danger { .panel-danger {
border-color: $border_danger; @include panel-colored;
.panel-heading { .panel-heading {
color: #ffffff; color: $border_danger;
background-color: $bg_danger;
border-color: $border_danger; border-color: $border_danger;
a {
color: #FFF;
text-decoration: underline;
}
} }
} }
.panel-success { .panel-success {
border-color: $border_success; @include panel-colored;
.panel-heading { .panel-heading {
color: #ffffff; color: $border_success;
background-color: $bg_success;
border-color: $border_success; border-color: $border_success;
a {
color: #FFF;
text-decoration: underline;
}
} }
} }
.panel-primary { .panel-primary {
border-color: $border_primary; @include panel-colored;
.panel-heading { .panel-heading {
color: #ffffff; color: $border_primary;
background-color: $bg_primary;
border-color: $border_primary; border-color: $border_primary;
a {
color: #FFF;
text-decoration: underline;
}
} }
} }
.panel-warning { .panel-warning {
border-color: $border_warning; @include panel-colored;
.panel-heading { .panel-heading {
color: #ffffff; color: $border_warning;
background-color: $bg_warning;
border-color: $border_warning; border-color: $border_warning;
a {
color: #FFF;
text-decoration: underline;
}
} }
} }
...@@ -132,3 +132,14 @@ ...@@ -132,3 +132,14 @@
white-space: nowrap; white-space: nowrap;
max-width: $max_width; max-width: $max_width;
} }
@mixin panel-colored {
border: none;
background: $box_bg;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09));
.panel-heading {
font-weight: bold;
background-color: $box_bg;
}
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
*/ */
$style_color: #474D57; $style_color: #474D57;
$hover: #FFECDB; $hover: #FFECDB;
$box_bg: #F9F9F9;
/* /*
* Link colors * Link colors
......
...@@ -104,7 +104,44 @@ ...@@ -104,7 +104,44 @@
} }
.mr-state-widget { .mr-state-widget {
.panel-body { background: $box_bg;
margin-bottom: 20px;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09));
.ci_widget {
padding: 10px 15px;
font-size: 15px;
border-bottom: 1px dashed #AAA;
&.ci-success {
color: $bg_success;
border-color: $border_success;
}
&.ci-pending {
color: #548;
border-color: #548;
}
&.ci-running {
color: $bg_warning;
border-color: $border_warning;
}
&.ci-failed {
color: $bg_danger;
border-color: $border_danger;
}
&.ci-error {
color: $bg_danger;
border-color: $border_danger;
}
}
.mr-widget-body {
padding: 10px 15px;
h4 { h4 {
margin-top: 0px; margin-top: 0px;
} }
...@@ -114,6 +151,11 @@ ...@@ -114,6 +151,11 @@
} }
} }
.mr-widget-footer {
padding: 10px 15px;
border-top: 1px solid #EEE;
}
.ci-coverage { .ci-coverage {
float: right; float: right;
} }
......
...@@ -8,8 +8,6 @@ ...@@ -8,8 +8,6 @@
ul { ul {
padding: 0; padding: 0;
margin: auto; margin: auto;
height: 40px;
overflow: hidden;
.count { .count {
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
...@@ -37,53 +35,28 @@ ...@@ -37,53 +35,28 @@
a { a {
color: $link_color; color: $link_color;
font-weight: bold; font-weight: bold;
&:after { border-bottom: 3px solid $link_color;
content: '';
display: block;
position: relative;
bottom: -1px;
border-color: $link_color;
border-style: solid;
border-width: 2px;
}
} }
} }
&:hover { &:hover {
a { a {
color: $link_hover_color; color: $link_hover_color;
&:after { border-bottom: 3px solid $link_hover_color;
content: '';
display: block;
position: relative;
bottom: -1px;
border-color: $link_hover_color;
border-style: solid;
border-width: 2px;
}
}
}
&.home {
a {
i {
font-size: 20px;
position: relative;
top: 4px;
}
} }
} }
} }
a { a {
display: block; display: block;
text-align: center; text-align: center;
font-weight: 500; font-weight: bold;
height: 38px; height: 42px;
line-height: 34px; line-height: 39px;
color: #777; color: #777;
text-shadow: 0 1px 1px white; text-shadow: 0 1px 1px white;
text-decoration: none; text-decoration: none;
padding-top: 2px; overflow: hidden;
margin-bottom: -1px;
} }
} }
......
...@@ -4,6 +4,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -4,6 +4,7 @@ class Admin::UsersController < Admin::ApplicationController
def index def index
@users = User.filter(params[:filter]) @users = User.filter(params[:filter])
@users = @users.search(params[:name]) if params[:name].present? @users = @users.search(params[:name]) if params[:name].present?
@users = @users.sort(@sort = params[:sort])
@users = @users.alphabetically.page(params[:page]) @users = @users.alphabetically.page(params[:page])
end end
......
...@@ -13,7 +13,7 @@ class ApplicationController < ActionController::Base ...@@ -13,7 +13,7 @@ class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller? before_filter :configure_permitted_parameters, if: :devise_controller?
before_filter :require_email, unless: :devise_controller? before_filter :require_email, unless: :devise_controller?
protect_from_forgery protect_from_forgery with: :exception
helper_method :abilities, :can? helper_method :abilities, :can?
......
...@@ -15,15 +15,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -15,15 +15,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
error.to_s.humanize if error error.to_s.humanize if error
end end
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap def ldap
# We only find ourselves here @user = Gitlab::LDAP::User.new(oauth)
# if the authentication to LDAP was successful. @user.save if @user.changed? # will also save new users
@user = Gitlab::LDAP::User.find_or_create(oauth) gl_user = @user.gl_user
@user.remember_me = true if @user.persisted? gl_user.remember_me = true if @user.persisted?
# Do additional LDAP checks for the user filter and EE features # Do additional LDAP checks for the user filter and EE features
if Gitlab::LDAP::Access.allowed?(@user) if @user.allowed?
sign_in_and_redirect(@user) sign_in_and_redirect(gl_user)
else else
flash[:alert] = "Access denied for your LDAP account." flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path redirect_to new_user_session_path
...@@ -46,24 +48,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -46,24 +48,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
current_user.save current_user.save
redirect_to profile_path redirect_to profile_path
else else
@user = Gitlab::OAuth::User.find(oauth) @user = Gitlab::OAuth::User.new(oauth)
# Create user if does not exist if Gitlab.config.omniauth['allow_single_sign_on'] && @user.new?
# and allow_single_sign_on is true @user.save
if Gitlab.config.omniauth['allow_single_sign_on'] && !@user
@user, errors = Gitlab::OAuth::User.create(oauth)
end end
if @user && !errors if @user.valid?
sign_in_and_redirect(@user) sign_in_and_redirect(@user.gl_user)
else else
if errors error_message = @user.gl_user.errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ")
error_message = errors.map{ |attribute, message| "#{attribute} #{message}" }.join(", ") redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
else
flash[:notice] = "There's no such user!"
end
redirect_to new_user_session_path
end end
end end
end end
......
...@@ -17,7 +17,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -17,7 +17,7 @@ class Projects::CommitsController < Projects::ApplicationController
group(:commit_id).count group(:commit_id).count
respond_to do |format| respond_to do |format|
format.html # index.html.erb format.html
format.json { pager_json("projects/commits/_commits", @commits.size) } format.json { pager_json("projects/commits/_commits", @commits.size) }
format.atom { render layout: false } format.atom { render layout: false }
end end
......
...@@ -152,7 +152,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -152,7 +152,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(
:title, :assignee_id, :position, :description, :title, :assignee_id, :position, :description,
:milestone_id, :state_event, label_ids: [] :milestone_id, :state_event, :task_num, label_ids: []
) )
end end
end end
...@@ -250,7 +250,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -250,7 +250,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit( params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch, :title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id, :target_project_id, :target_branch, :milestone_id,
:state_event, :description, label_ids: [] :state_event, :description, :task_num, label_ids: []
) )
end end
end end
...@@ -40,7 +40,8 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -40,7 +40,8 @@ class Projects::ServicesController < Projects::ApplicationController
def service_params def service_params
params.require(:service).permit( params.require(:service).permit(
:title, :token, :type, :active, :api_key, :subdomain, :title, :token, :type, :active, :api_key, :subdomain,
:room, :recipients, :project_url, :username, :password, :api_version :room, :recipients, :project_url, :webhook, :username, :password, :api_version,
:user_key, :device, :priority, :sound
) )
end end
end end
...@@ -17,7 +17,10 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -17,7 +17,10 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html respond_to :html
def index def index
@snippets = @project.snippets.fresh.non_expired @snippets = SnippetsFinder.new.execute(current_user, {
filter: :by_project,
project: @project
})
end end
def new def new
...@@ -88,6 +91,6 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -88,6 +91,6 @@ class Projects::SnippetsController < Projects::ApplicationController
end end
def snippet_params def snippet_params
params.require(:project_snippet).permit(:title, :content, :file_name, :private) params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level)
end end
end end
...@@ -18,6 +18,10 @@ class SessionsController < Devise::SessionsController ...@@ -18,6 +18,10 @@ class SessionsController < Devise::SessionsController
store_location_for(:redirect, redirect_path) store_location_for(:redirect, redirect_path)
end end
if Gitlab.config.ldap.enabled
@ldap_servers = Gitlab::LDAP::Config.servers
end
super super
end end
......
...@@ -9,12 +9,14 @@ class SnippetsController < ApplicationController ...@@ -9,12 +9,14 @@ class SnippetsController < ApplicationController
before_filter :set_title before_filter :set_title
skip_before_filter :authenticate_user!, only: [:index, :user_index]
respond_to :html respond_to :html
layout 'navless' layout :determine_layout
def index def index
@snippets = Snippet.are_public.fresh.non_expired.page(params[:page]).per(20) @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(20)
end end
def user_index def user_index
...@@ -22,22 +24,11 @@ class SnippetsController < ApplicationController ...@@ -22,22 +24,11 @@ class SnippetsController < ApplicationController
render_404 and return unless @user render_404 and return unless @user
@snippets = @user.snippets.fresh.non_expired @snippets = SnippetsFinder.new.execute(current_user, {
filter: :by_user,
if @user == current_user user: @user,
@snippets = case params[:scope] scope: params[:scope]}).
when 'are_public' then page(params[:page]).per(20)
@snippets.are_public
when 'are_private' then
@snippets.are_private
else
@snippets
end
else
@snippets = @snippets.are_public
end
@snippets = @snippets.page(params[:page]).per(20)
if @user == current_user if @user == current_user
render 'current_user_index' render 'current_user_index'
...@@ -95,7 +86,14 @@ class SnippetsController < ApplicationController ...@@ -95,7 +86,14 @@ class SnippetsController < ApplicationController
protected protected
def snippet def snippet
@snippet ||= PersonalSnippet.where('author_id = :user_id or private is false', user_id: current_user.id).find(params[:id]) @snippet ||= if current_user
PersonalSnippet.where("author_id = ? OR visibility_level IN (?)",
current_user.id,
[Snippet::PUBLIC, Snippet::INTERNAL]).
find(params[:id])
else
PersonalSnippet.are_public.find(params[:id])
end
end end
def authorize_modify_snippet! def authorize_modify_snippet!
...@@ -111,6 +109,10 @@ class SnippetsController < ApplicationController ...@@ -111,6 +109,10 @@ class SnippetsController < ApplicationController
end end
def snippet_params def snippet_params
params.require(:personal_snippet).permit(:title, :content, :file_name, :private) params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level)
end
def determine_layout
current_user ? 'navless' : 'public_users'
end end
end end
# Finders # Finders
This type of classes responsible for collectiong items based on different conditions. This type of classes responsible for collection items based on different conditions.
To prevent lookup methods in models like this: To prevent lookup methods in models like this:
```ruby ```ruby
class Project class Project
...@@ -13,10 +13,10 @@ end ...@@ -13,10 +13,10 @@ end
issues = project.issues_for_user_filtered_by(user, params) issues = project.issues_for_user_filtered_by(user, params)
``` ```
Better use this: Better use this:
```ruby ```ruby
issues = IssuesFinder.new.execute(project, user, filter) issues = IssuesFinder.new.execute(project, user, filter)
``` ```
It will help keep models thiner It will help keep models thiner.
class SnippetsFinder
def execute(current_user, params = {})
filter = params[:filter]
case filter
when :all then
snippets(current_user).fresh.non_expired
when :by_user then
by_user(current_user, params[:user], params[:scope])
when :by_project
by_project(current_user, params[:project])
end
end
private
def snippets(current_user)
if current_user
Snippet.public_and_internal
else
# Not authenticated
#
# Return only:
# public snippets
Snippet.are_public
end
end
def by_user(current_user, user, scope)
snippets = user.snippets.fresh.non_expired
if user == current_user
case scope
when 'are_internal' then
snippets.are_internal
when 'are_private' then
snippets.are_private
when 'are_public' then
snippets.are_public
else
snippets
end
else
snippets.public_and_internal
end
end
def by_project(current_user, project)
snippets = project.snippets.fresh.non_expired
if current_user
if project.team.member?(current_user.id)
snippets
else
snippets.public_and_internal
end
else
snippets.are_public
end
end
end
...@@ -263,4 +263,16 @@ module ApplicationHelper ...@@ -263,4 +263,16 @@ module ApplicationHelper
super super
end end
def escaped_autolink(text)
auto_link ERB::Util.html_escape(text), link: :urls
end
def promo_host
'about.gitlab.com'
end
def promo_url
'https://' + promo_host
end
end end
...@@ -120,4 +120,8 @@ module CommitsHelper ...@@ -120,4 +120,8 @@ module CommitsHelper
class: 'commit-short-id') class: 'commit-short-id')
end end
end end
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
end end
...@@ -37,40 +37,31 @@ module DashboardHelper ...@@ -37,40 +37,31 @@ module DashboardHelper
end end
def assigned_entities_count(current_user, entity, scope = nil) def assigned_entities_count(current_user, entity, scope = nil)
items = current_user.send("assigned_" + entity.pluralize).opened items = current_user.send('assigned_' + entity.pluralize)
get_count(items, scope)
if scope.kind_of?(Group)
items = items.of_group(scope)
elsif scope.kind_of?(Project)
items = items.of_projects(scope)
end
items.count
end end
def authored_entities_count(current_user, entity, scope = nil) def authored_entities_count(current_user, entity, scope = nil)
items = current_user.send(entity.pluralize).opened items = current_user.send(entity.pluralize)
get_count(items, scope)
if scope.kind_of?(Group)
items = items.of_group(scope)
elsif scope.kind_of?(Project)
items = items.of_projects(scope)
end
items.count
end end
def authorized_entities_count(current_user, entity, scope = nil) def authorized_entities_count(current_user, entity, scope = nil)
items = entity.classify.constantize.opened items = entity.classify.constantize
get_count(items, scope, true, current_user)
end
protected
def get_count(items, scope, get_authorized = false, current_user = nil)
items = items.opened
if scope.kind_of?(Group) if scope.kind_of?(Group)
items = items.of_group(scope) items = items.of_group(scope)
elsif scope.kind_of?(Project) elsif scope.kind_of?(Project)
items = items.of_projects(scope) items = items.of_projects(scope)
else elsif get_authorized
items = items.of_projects(current_user.authorized_projects) items = items.of_projects(current_user.authorized_projects)
end end
items.count items.count
end end
end end
...@@ -136,9 +136,8 @@ module EventsHelper ...@@ -136,9 +136,8 @@ module EventsHelper
end end
def event_note(text) def event_note(text)
text = first_line_in_markdown(text) text = first_line_in_markdown(text, 150)
text = truncate(text, length: 150) sanitize(text, tags: %w(a img b pre code p))
sanitize(markdown(text), tags: %w(a img b pre p))
end end
def event_commit_title(message) def event_commit_title(message)
......
...@@ -51,12 +51,14 @@ module GitlabMarkdownHelper ...@@ -51,12 +51,14 @@ module GitlabMarkdownHelper
@markdown.render(text).html_safe @markdown.render(text).html_safe
end end
def first_line_in_markdown(text) # Return the first line of +text+, up to +max_chars+, after parsing the line
line = text.split("\n").detect do |i| # as Markdown. HTML tags in the parsed output are not counted toward the
i.present? && markdown(i).present? # +max_chars+ limit. If the length limit falls within a tag's contents, then
end # the tag contents are truncated without removing the closing tag.
line += '...' unless line.nil? def first_line_in_markdown(text, max_chars = nil)
line md = markdown(text).strip
truncate_visible(md, max_chars || md.length) if md.present?
end end
def render_wiki_content(wiki_page) def render_wiki_content(wiki_page)
...@@ -204,4 +206,52 @@ module GitlabMarkdownHelper ...@@ -204,4 +206,52 @@ module GitlabMarkdownHelper
def correct_ref def correct_ref
@ref ? @ref : "master" @ref ? @ref : "master"
end end
private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML
# tags.
def truncate_visible(text, max_chars)
doc = Nokogiri::HTML.fragment(text)
content_length = 0
truncated = false
doc.traverse do |node|
if node.text? || node.content.empty?
if truncated
node.remove
next
end
# Handle line breaks within a node
if node.content.strip.lines.length > 1
node.content = "#{node.content.lines.first.chomp}..."
truncated = true
end
num_remaining = max_chars - content_length
if node.content.length > num_remaining
node.content = node.content.truncate(num_remaining)
truncated = true
end
content_length += node.content.length
end
truncated = truncate_if_block(node, truncated)
end
doc.to_html
end
# Used by #truncate_visible. If +node+ is the first block element, and the
# text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false.
def truncate_if_block(node, truncated)
if node.element? && node.description.block? && !truncated
node.content = "#{node.content}..." if node.next_sibling
true
else
truncated
end
end
end end
...@@ -19,8 +19,7 @@ module MergeRequestsHelper ...@@ -19,8 +19,7 @@ module MergeRequestsHelper
source_project_id: event.project.id, source_project_id: event.project.id,
target_project_id: target_project.id, target_project_id: target_project.id,
source_branch: event.branch_name, source_branch: event.branch_name,
target_branch: target_project.repository.root_ref, target_branch: target_project.repository.root_ref
title: event.branch_name.titleize.humanize
} }
end end
......
module OauthHelper module OauthHelper
def ldap_enabled? def ldap_enabled?
Devise.omniauth_providers.include?(:ldap) Gitlab.config.ldap.enabled
end end
def default_providers def default_providers
......
...@@ -56,6 +56,10 @@ module ProjectsHelper ...@@ -56,6 +56,10 @@ module ProjectsHelper
"You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?"
end end
def transfer_project_message(project)
"You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
end
def project_nav_tabs def project_nav_tabs
@nav_tabs ||= get_project_nav_tabs(@project, current_user) @nav_tabs ||= get_project_nav_tabs(@project, current_user)
end end
...@@ -128,9 +132,9 @@ module ProjectsHelper ...@@ -128,9 +132,9 @@ module ProjectsHelper
toggle_html = content_tag('span', class: 'toggle') do toggle_html = content_tag('span', class: 'toggle') do
toggle_text = if starred toggle_text = if starred
'Unstar' ' Unstar'
else else
'Star' ' Star'
end end
content_tag('i', ' ', class: 'fa fa-star') + toggle_text content_tag('i', ' ', class: 'fa fa-star') + toggle_text
......
...@@ -90,7 +90,7 @@ module TreeHelper ...@@ -90,7 +90,7 @@ module TreeHelper
end end
def editing_preview_title(filename) def editing_preview_title(filename)
if gitlab_markdown?(filename) || markup?(filename) if Gitlab::MarkdownHelper.previewable?(filename)
'Preview' 'Preview'
else else
'Diff' 'Diff'
......
...@@ -28,6 +28,23 @@ module VisibilityLevelHelper ...@@ -28,6 +28,23 @@ module VisibilityLevelHelper
end end
end end
def snippet_visibility_level_description(level)
capture_haml do
haml_tag :span do
case level
when Gitlab::VisibilityLevel::PRIVATE
haml_concat "The snippet is visible only for me"
when Gitlab::VisibilityLevel::INTERNAL
haml_concat "The snippet is visible for any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
haml_concat "The snippet can be accessed"
haml_concat "without any"
haml_concat "authentication."
end
end
end
end
def visibility_level_icon(level) def visibility_level_icon(level)
case level case level
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
......
...@@ -19,13 +19,24 @@ class Commit ...@@ -19,13 +19,24 @@ class Commit
class << self class << self
def decorate(commits) def decorate(commits)
commits.map { |c| self.new(c) } commits.map do |commit|
if commit.kind_of?(Commit)
commit
else
self.new(commit)
end
end
end end
# Calculate number of lines to render for diffs # Calculate number of lines to render for diffs
def diff_line_count(diffs) def diff_line_count(diffs)
diffs.reduce(0) { |sum, d| sum + d.diff.lines.count } diffs.reduce(0) { |sum, d| sum + d.diff.lines.count }
end end
# Truncate sha to 8 characters
def truncate_sha(sha)
sha[0..7]
end
end end
attr_accessor :raw attr_accessor :raw
...@@ -111,7 +122,7 @@ class Commit ...@@ -111,7 +122,7 @@ class Commit
# Mentionable override. # Mentionable override.
def gfm_reference def gfm_reference
"commit #{sha[0..5]}" "commit #{id}"
end end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
...@@ -124,6 +135,11 @@ class Commit ...@@ -124,6 +135,11 @@ class Commit
super super
end end
# Truncate sha to 8 characters
def short_id
@raw.short_id(7)
end
def parents def parents
@parents ||= Commit.decorate(super) @parents ||= Commit.decorate(super)
end end
......
...@@ -67,9 +67,10 @@ module Mentionable ...@@ -67,9 +67,10 @@ module Mentionable
def references(p = project, text = mentionable_text) def references(p = project, text = mentionable_text)
return [] if text.blank? return [] if text.blank?
ext = Gitlab::ReferenceExtractor.new ext = Gitlab::ReferenceExtractor.new
ext.analyze(text) ext.analyze(text, p)
issues = ext.issues_for(p).select { |issue| issue.is_a?(Issue) } (ext.issues_for +
(issues + ext.merge_requests_for(p) + ext.commits_for(p)).uniq - [local_reference] ext.merge_requests_for +
ext.commits_for).uniq - [local_reference]
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
......
# Contains functionality for objects that can have task lists in their
# descriptions. Task list items can be added with Markdown like "* [x] Fix
# bugs".
#
# Used by MergeRequest and Issue
module Taskable
TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze
TASK_PATTERN_HTML = /^<li>\[(?<checked>[ xX])\]/.freeze
# Change the state of a task list item for this Taskable. Edit the object's
# description by finding the nth task item and changing its checkbox
# placeholder to "[x]" if +checked+ is true, or "[ ]" if it's false.
# Note: task numbering starts with 1
def update_nth_task(n, checked)
index = 0
check_char = checked ? 'x' : ' '
# Do this instead of using #gsub! so that ActiveRecord detects that a field
# has changed.
self.description = self.description.gsub(TASK_PATTERN_MD) do |match|
index += 1
case index
when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]"
else match
end
end
save
end
# Return true if this object's description has any task list items.
def tasks?
description && description.match(TASK_PATTERN_MD)
end
# Return a string that describes the current state of this Taskable's task
# list items, e.g. "20 tasks (12 done, 8 unfinished)"
def task_status
return nil unless description
num_tasks = 0
num_done = 0
description.scan(TASK_PATTERN_MD) do
num_tasks += 1
num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' '
end
"#{num_tasks} tasks (#{num_done} done, #{num_tasks - num_done} unfinished)"
end
end
...@@ -266,7 +266,7 @@ class Event < ActiveRecord::Base ...@@ -266,7 +266,7 @@ class Event < ActiveRecord::Base
end end
def note_short_commit_id def note_short_commit_id
note_commit_id[0..8] Commit.truncate_sha(note_commit_id)
end end
def note_commit? def note_commit?
......
...@@ -23,6 +23,7 @@ require 'file_size_validator' ...@@ -23,6 +23,7 @@ require 'file_size_validator'
class Issue < ActiveRecord::Base class Issue < ActiveRecord::Base
include Issuable include Issuable
include InternalId include InternalId
include Taskable
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
......
# == Schema Information
#
# Table name: members
#
# id :integer not null, primary key
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
# user_id :integer not null
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
#
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
include Notifiable include Notifiable
include Gitlab::Access include Gitlab::Access
......
# == Schema Information
#
# Table name: members
#
# id :integer not null, primary key
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
# user_id :integer not null
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
#
class GroupMember < Member class GroupMember < Member
SOURCE_TYPE = 'Namespace' SOURCE_TYPE = 'Namespace'
......
# == Schema Information
#
# Table name: members
#
# id :integer not null, primary key
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
# user_id :integer not null
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
#
class ProjectMember < Member class ProjectMember < Member
SOURCE_TYPE = 'Project' SOURCE_TYPE = 'Project'
......
...@@ -25,6 +25,7 @@ require Rails.root.join("lib/static_model") ...@@ -25,6 +25,7 @@ require Rails.root.join("lib/static_model")
class MergeRequest < ActiveRecord::Base class MergeRequest < ActiveRecord::Base
include Issuable include Issuable
include Taskable
include InternalId include InternalId
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
......
...@@ -55,7 +55,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -55,7 +55,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def last_commit_short_sha def last_commit_short_sha
@last_commit_short_sha ||= last_commit.sha[0..10] @last_commit_short_sha ||= last_commit.short_id
end end
private private
......
...@@ -47,7 +47,7 @@ class Note < ActiveRecord::Base ...@@ -47,7 +47,7 @@ class Note < ActiveRecord::Base
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") } scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :system, ->{ where(system: true) }
scope :common, ->{ where(noteable_type: ["", nil]) } scope :common, ->{ where(noteable_type: ["", nil]) }
scope :fresh, ->{ order("created_at ASC, id ASC") } scope :fresh, ->{ order("created_at ASC, id ASC") }
scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author_project, ->{ includes(:project, :author) }
...@@ -70,13 +70,17 @@ class Note < ActiveRecord::Base ...@@ -70,13 +70,17 @@ class Note < ActiveRecord::Base
) )
end end
# +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note. # +noteable+ was referenced from +mentioner+, by including GFM in either
# Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+. # +mentioner+'s description or an associated Note.
# Create a system Note associated with +noteable+ with a GFM back-reference
# to +mentioner+.
def create_cross_reference_note(noteable, mentioner, author, project) def create_cross_reference_note(noteable, mentioner, author, project)
gfm_reference = mentioner_gfm_ref(noteable, mentioner, project)
note_options = { note_options = {
project: project, project: project,
author: author, author: author,
note: "_mentioned in #{mentioner.gfm_reference}_", note: "_mentioned in #{gfm_reference}_",
system: true system: true
} }
...@@ -163,12 +167,78 @@ class Note < ActiveRecord::Base ...@@ -163,12 +167,78 @@ class Note < ActiveRecord::Base
# Determine whether or not a cross-reference note already exists. # Determine whether or not a cross-reference note already exists.
def cross_reference_exists?(noteable, mentioner) def cross_reference_exists?(noteable, mentioner)
where(noteable_id: noteable.id, system: true, note: "_mentioned in #{mentioner.gfm_reference}_").any? gfm_reference = mentioner_gfm_ref(noteable, mentioner)
notes = if noteable.is_a?(Commit)
where(commit_id: noteable.id)
else
where(noteable_id: noteable.id)
end
notes.where('note like ?', "_mentioned in #{gfm_reference}_").
system.any?
end end
def search(query) def search(query)
where("note like :query", query: "%#{query}%") where("note like :query", query: "%#{query}%")
end end
private
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def mentioner_gfm_ref(noteable, mentioner, project = nil)
if mentioner.is_a?(Commit)
if project.nil?
return mentioner.gfm_reference.sub('commit ', 'commit %')
else
mentioning_project = project
end
else
mentioning_project = mentioner.project
end
noteable_project_id = noteable_project_id(noteable, mentioning_project)
full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
end
# Return the ID of the project that +noteable+ belongs to, or nil if
# +noteable+ is a commit and is not part of the project that owns
# +mentioner+.
def noteable_project_id(noteable, mentioning_project)
if noteable.is_a?(Commit)
if mentioning_project.repository.commit(noteable.id)
# The noteable commit belongs to the mentioner's project
mentioning_project.id
else
nil
end
else
noteable.project.id
end
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
def full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
if mentioning_project.id == noteable_project_id
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
end end
def commit_author def commit_author
......
...@@ -2,17 +2,17 @@ ...@@ -2,17 +2,17 @@
# #
# Table name: snippets # Table name: snippets
# #
# id :integer not null, primary key # id :integer not null, primary key
# title :string(255) # title :string(255)
# content :text # content :text
# author_id :integer not null # author_id :integer not null
# project_id :integer # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime # expires_at :datetime
# private :boolean default(TRUE), not null # type :string(255)
# type :string(255) # visibility_level :integer default(0), not null
# #
class PersonalSnippet < Snippet class PersonalSnippet < Snippet
......
...@@ -67,6 +67,8 @@ class Project < ActiveRecord::Base ...@@ -67,6 +67,8 @@ class Project < ActiveRecord::Base
has_one :slack_service, dependent: :destroy has_one :slack_service, dependent: :destroy
has_one :jira_service, dependent: :destroy has_one :jira_service, dependent: :destroy
has_one :jenkins_service, dependent: :destroy has_one :jenkins_service, dependent: :destroy
has_one :buildbox_service, dependent: :destroy
has_one :pushover_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
...@@ -318,7 +320,7 @@ class Project < ActiveRecord::Base ...@@ -318,7 +320,7 @@ class Project < ActiveRecord::Base
end end
def available_services_names def available_services_names
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack jira jenkins) %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack jira jenkins pushover buildbox)
end end
def gitlab_ci? def gitlab_ci?
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
class AssemblaService < Service class AssemblaService < Service
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
class BuildboxService < CiService
prop_accessor :project_url, :token
validates :project_url, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def webhook_url
"#{buildbox_endpoint('webhook')}/deliver/#{webhook_token}"
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = webhook_url
hook.save
end
def execute(data)
service_hook.execute(data)
end
def commit_status(sha)
response = HTTParty.get(commit_status_path(sha), verify: false)
if response.code == 200 && response['status']
response['status']
else
:error
end
end
def commit_status_path(sha)
"#{buildbox_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
end
def build_page(sha)
"#{project_url}/builds?commit=#{sha}"
end
def builds_path
"#{project_url}/builds?branch=#{project.default_branch}"
end
def status_img_path
"#{buildbox_endpoint('badge')}/#{status_token}.svg"
end
def title
'Buildbox'
end
def description
'Continuous integration and deployments'
end
def to_param
'buildbox'
end
def fields
[
{ type: 'text',
name: 'token',
placeholder: 'Buildbox project GitLab token' },
{ type: 'text',
name: 'project_url',
placeholder: 'https://buildbox.io/example/project' }
]
end
private
def webhook_token
token_parts.first
end
def status_token
token_parts.second
end
def token_parts
if token.present?
token.split(':')
else
[]
end
end
def buildbox_endpoint(subdomain = nil)
endpoint = 'https://buildbox.io'
if subdomain.present?
uri = Addressable::URI.parse(endpoint)
new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}"
if uri.port.present?
"#{new_endpoint}:#{uri.port}"
else
new_endpoint
end
else
endpoint
end
end
end
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
class CampfireService < Service class CampfireService < Service
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
# Base class for CI services # Base class for CI services
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
class EmailsOnPushService < Service class EmailsOnPushService < Service
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
require "flowdock-git-hook" require "flowdock-git-hook"
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
require "gemnasium/gitlab_service" require "gemnasium/gitlab_service"
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# property :text # properties :text
# #
class GitlabCiService < CiService class GitlabCiService < CiService
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
class HipchatService < Service class HipchatService < Service
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
class PivotaltrackerService < Service class PivotaltrackerService < Service
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
class PushoverService < Service
include HTTParty
base_uri 'https://api.pushover.net/1'
prop_accessor :api_key, :user_key, :device, :priority, :sound
validates :api_key, :user_key, :priority, presence: true, if: :activated?
def title
'Pushover'
end
def description
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end
def to_param
'pushover'
end
def fields
[
{ type: 'text', name: 'api_key', placeholder: 'Your application key' },
{ type: 'text', name: 'user_key', placeholder: 'Your user key' },
{ type: 'text', name: 'device', placeholder: 'Leave blank for all active devices' },
{ type: 'select', name: 'priority', choices:
[
['Lowest Priority', -2],
['Low Priority', -1],
['Normal Priority', 0],
['High Priority', 1]
],
default_choice: 0
},
{ type: 'select', name: 'sound', choices:
[
['Device default sound', nil],
['Pushover (default)', 'pushover'],
['Bike', 'bike'],
['Bugle', 'bugle'],
['Cash Register', 'cashregister'],
['Classical', 'classical'],
['Cosmic', 'cosmic'],
['Falling', 'falling'],
['Gamelan', 'gamelan'],
['Incoming', 'incoming'],
['Intermission', 'intermission'],
['Magic', 'magic'],
['Mechanical', 'mechanical'],
['Piano Bar', 'pianobar'],
['Siren', 'siren'],
['Space Alarm', 'spacealarm'],
['Tug Boat', 'tugboat'],
['Alien Alarm (long)', 'alien'],
['Climb (long)', 'climb'],
['Persistent (long)', 'persistent'],
['Pushover Echo (long)', 'echo'],
['Up Down (long)', 'updown'],
['None (silent)', 'none']
]
},
]
end
def execute(push_data)
ref = push_data[:ref].gsub('refs/heads/', '')
before = push_data[:before]
after = push_data[:after]
if before =~ /000000/
message = "#{push_data[:user_name]} pushed new branch \"#{ref}\"."
elsif after =~ /000000/
message = "#{push_data[:user_name]} deleted branch \"#{ref}\"."
else
message = "#{push_data[:user_name]} push to branch \"#{ref}\"."
end
if push_data[:total_commits_count] > 0
message << "\nTotal commits count: #{push_data[:total_commits_count]}"
end
pushover_data = {
token: api_key,
user: user_key,
device: device,
priority: priority,
title: "#{project.name_with_namespace}",
message: message,
url: push_data[:repository][:homepage],
url_title: "See project #{project.name_with_namespace}"
}
# Sound parameter MUST NOT be sent to API if not selected
if sound
pushover_data.merge!(sound: sound)
end
PushoverService.post('/messages.json', body: pushover_data)
end
end
...@@ -2,21 +2,19 @@ ...@@ -2,21 +2,19 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# #
class SlackService < Service class SlackService < Service
prop_accessor :room, :subdomain, :token prop_accessor :webhook
validates :room, presence: true, if: :activated? validates :webhook, presence: true, if: :activated?
validates :subdomain, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
def title def title
'Slack' 'Slack'
...@@ -32,9 +30,7 @@ class SlackService < Service ...@@ -32,9 +30,7 @@ class SlackService < Service
def fields def fields
[ [
{ type: 'text', name: 'subdomain', placeholder: '' }, { type: 'text', name: 'webhook', placeholder: '' }
{ type: 'text', name: 'token', placeholder: '' },
{ type: 'text', name: 'room', placeholder: 'Ex. #general' },
] ]
end end
...@@ -44,10 +40,13 @@ class SlackService < Service ...@@ -44,10 +40,13 @@ class SlackService < Service
project_name: project_name project_name: project_name
)) ))
notifier = Slack::Notifier.new(subdomain, token) credentials = webhook.match(/(\w*).slack.com.*services\/(.*)/)
notifier.channel = room if credentials.present?
notifier.username = 'GitLab' subdomain = credentials[1]
notifier.ping(message.pretext, attachments: message.attachments) token = credentials[2].split("token=").last
notifier = Slack::Notifier.new(subdomain, token)
notifier.ping(message.pretext, attachments: message.attachments)
end
end end
private private
......
...@@ -2,17 +2,17 @@ ...@@ -2,17 +2,17 @@
# #
# Table name: snippets # Table name: snippets
# #
# id :integer not null, primary key # id :integer not null, primary key
# title :string(255) # title :string(255)
# content :text # content :text
# author_id :integer not null # author_id :integer not null
# project_id :integer # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime # expires_at :datetime
# private :boolean default(TRUE), not null # type :string(255)
# type :string(255) # visibility_level :integer default(0), not null
# #
class ProjectSnippet < Snippet class ProjectSnippet < Snippet
......
...@@ -133,6 +133,10 @@ class ProjectTeam ...@@ -133,6 +133,10 @@ class ProjectTeam
max_tm_access(user.id) == Gitlab::Access::MASTER max_tm_access(user.id) == Gitlab::Access::MASTER
end end
def member?(user_id)
!!find_tm(user_id)
end
def max_tm_access(user_id) def max_tm_access(user_id)
access = [] access = []
access << project.project_members.find_by(user_id: user_id).try(:access_field) access << project.project_members.find_by(user_id: user_id).try(:access_field)
......
...@@ -30,6 +30,8 @@ class Repository ...@@ -30,6 +30,8 @@ class Repository
commit = Gitlab::Git::Commit.find(raw_repository, id) commit = Gitlab::Git::Commit.find(raw_repository, id)
commit = Commit.new(commit) if commit commit = Commit.new(commit) if commit
commit commit
rescue Rugged::OdbError => ex
nil
end end
def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false) def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
......
...@@ -2,14 +2,15 @@ ...@@ -2,14 +2,15 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
#
# To add new service you should build a class inherited from Service # To add new service you should build a class inherited from Service
# and implement a set of methods # and implement a set of methods
......
...@@ -2,23 +2,24 @@ ...@@ -2,23 +2,24 @@
# #
# Table name: snippets # Table name: snippets
# #
# id :integer not null, primary key # id :integer not null, primary key
# title :string(255) # title :string(255)
# content :text # content :text
# author_id :integer not null # author_id :integer not null
# project_id :integer # project_id :integer
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# file_name :string(255) # file_name :string(255)
# expires_at :datetime # expires_at :datetime
# private :boolean default(TRUE), not null # type :string(255)
# type :string(255) # visibility_level :integer default(0), not null
# #
class Snippet < ActiveRecord::Base class Snippet < ActiveRecord::Base
include Linguist::BlobHelper include Linguist::BlobHelper
include Gitlab::VisibilityLevel
default_value_for :private, true default_value_for :visibility_level, Snippet::PRIVATE
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
...@@ -30,10 +31,13 @@ class Snippet < ActiveRecord::Base ...@@ -30,10 +31,13 @@ class Snippet < ActiveRecord::Base
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
validates :file_name, presence: true, length: { within: 0..255 } validates :file_name, presence: true, length: { within: 0..255 }
validates :content, presence: true validates :content, presence: true
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
# Scopes # Scopes
scope :are_public, -> { where(private: false) } scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) }
scope :are_private, -> { where(private: true) } scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) }
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") } scope :fresh, -> { order("created_at DESC") }
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
...@@ -66,6 +70,10 @@ class Snippet < ActiveRecord::Base ...@@ -66,6 +70,10 @@ class Snippet < ActiveRecord::Base
expires_at && expires_at < Time.current expires_at && expires_at < Time.current
end end
def visibility_level_field
visibility_level
end
class << self class << self
def search(query) def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
...@@ -76,7 +84,7 @@ class Snippet < ActiveRecord::Base ...@@ -76,7 +84,7 @@ class Snippet < ActiveRecord::Base
end end
def accessible_to(user) def accessible_to(user)
where('private = ? OR author_id = ?', false, user) where('visibility_level IN (?) OR author_id = ?', [Snippet::INTERNAL, Snippet::PUBLIC], user)
end end
end end
end end
...@@ -15,7 +15,7 @@ class Tree ...@@ -15,7 +15,7 @@ class Tree
# by markup renderer. # by markup renderer.
if available_readmes.length > 1 if available_readmes.length > 1
supported_readmes = available_readmes.select do |readme| supported_readmes = available_readmes.select do |readme|
gitlab_markdown?(readme.name) || markup?(readme.name) previewable?(readme.name)
end end
# Take the first supported readme, or the first available readme, if we # Take the first supported readme, or the first available readme, if we
......
...@@ -178,9 +178,8 @@ class User < ActiveRecord::Base ...@@ -178,9 +178,8 @@ class User < ActiveRecord::Base
scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
scope :ldap, -> { where(provider: 'ldap') }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) } scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
scope :ldap, -> { where('provider LIKE ?', 'ldap%') }
scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active }
# #
...@@ -197,6 +196,16 @@ class User < ActiveRecord::Base ...@@ -197,6 +196,16 @@ class User < ActiveRecord::Base
end end
end end
def sort(method)
case method.to_s
when 'recent_sign_in' then reorder('users.last_sign_in_at DESC')
when 'oldest_sign_in' then reorder('users.last_sign_in_at ASC')
when 'recently_created' then reorder('users.created_at DESC')
when 'late_created' then reorder('users.created_at ASC')
else reorder("users.name ASC")
end
end
def find_for_commit(email, name) def find_for_commit(email, name)
# Prefer email match over name match # Prefer email match over name match
User.where(email: email).first || User.where(email: email).first ||
...@@ -399,7 +408,7 @@ class User < ActiveRecord::Base ...@@ -399,7 +408,7 @@ class User < ActiveRecord::Base
end end
def ldap_user? def ldap_user?
extern_uid && provider == 'ldap' extern_uid && provider.start_with?('ldap')
end end
def accessible_deploy_keys def accessible_deploy_keys
......
class IssuableBaseService < BaseService
private
def create_assignee_note(issuable)
Note.create_assignee_change_note(
issuable, issuable.project, current_user, issuable.assignee)
end
def create_milestone_note(issuable)
Note.create_milestone_change_note(
issuable, issuable.project, current_user, issuable.milestone)
end
end
module Issues module Issues
class BaseService < ::BaseService class BaseService < ::IssuableBaseService
private private
def create_assignee_note(issue)
Note.create_assignee_change_note(issue, issue.project, current_user, issue.assignee)
end
def execute_hooks(issue, action = 'open') def execute_hooks(issue, action = 'open')
issue_data = issue.to_hook_data issue_data = issue.to_hook_data
issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
issue_data[:object_attributes].merge!(url: issue_url, action: action) issue_data[:object_attributes].merge!(url: issue_url, action: action)
issue.project.execute_hooks(issue_data, :issue_hooks) issue.project.execute_hooks(issue_data, :issue_hooks)
end end
def create_milestone_note(issue)
Note.create_milestone_change_note(issue, issue.project, current_user, issue.milestone)
end
end end
end end
...@@ -8,9 +8,14 @@ module Issues ...@@ -8,9 +8,14 @@ module Issues
Issues::ReopenService.new(project, current_user, {}).execute(issue) Issues::ReopenService.new(project, current_user, {}).execute(issue)
when 'close' when 'close'
Issues::CloseService.new(project, current_user, {}).execute(issue) Issues::CloseService.new(project, current_user, {}).execute(issue)
when 'task_check'
issue.update_nth_task(params[:task_num].to_i, true)
when 'task_uncheck'
issue.update_nth_task(params[:task_num].to_i, false)
end end
if params.present? && issue.update_attributes(params.except(:state_event)) if params.present? && issue.update_attributes(params.except(:state_event,
:task_num))
issue.reset_events_cache issue.reset_events_cache
if issue.previous_changes.include?('milestone_id') if issue.previous_changes.include?('milestone_id')
......
module MergeRequests module MergeRequests
class BaseService < ::BaseService class BaseService < ::IssuableBaseService
private
def create_assignee_note(merge_request)
Note.create_assignee_change_note(merge_request, merge_request.project, current_user, merge_request.assignee)
end
def create_note(merge_request) def create_note(merge_request)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
...@@ -16,9 +10,5 @@ module MergeRequests ...@@ -16,9 +10,5 @@ module MergeRequests
merge_request.project.execute_hooks(merge_request.to_hook_data, :merge_request_hooks) merge_request.project.execute_hooks(merge_request.to_hook_data, :merge_request_hooks)
end end
end end
def create_milestone_note(merge_request)
Note.create_milestone_change_note(merge_request, merge_request.project, current_user, merge_request.milestone)
end
end end
end end
...@@ -17,9 +17,15 @@ module MergeRequests ...@@ -17,9 +17,15 @@ module MergeRequests
MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request) MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request)
when 'close' when 'close'
MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request) MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request)
when 'task_check'
merge_request.update_nth_task(params[:task_num].to_i, true)
when 'task_uncheck'
merge_request.update_nth_task(params[:task_num].to_i, false)
end end
if params.present? && merge_request.update_attributes(params.except(:state_event)) if params.present? && merge_request.update_attributes(
params.except(:state_event, :task_num)
)
merge_request.reset_events_cache merge_request.reset_events_cache
if merge_request.previous_changes.include?('milestone_id') if merge_request.previous_changes.include?('milestone_id')
......
...@@ -33,6 +33,26 @@ ...@@ -33,6 +33,26 @@
Users (#{@users.total_count}) Users (#{@users.total_count})
.panel-head-actions .panel-head-actions
= link_to 'Send email to users', admin_email_path, class: 'btn btn-info' = link_to 'Send email to users', admin_email_path, class: 'btn btn-info'
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light sort:
- if @sort.present?
= @sort.humanize
- else
Name
%b.caret
%ul.dropdown-menu
%li
= link_to admin_users_path(sort: nil) do
Name
= link_to admin_users_path(sort: 'recent_sign_in') do
Recent sign in
= link_to admin_users_path(sort: 'oldest_sign_in') do
Oldest sign in
= link_to admin_users_path(sort: 'recently_created') do
Recently created
= link_to admin_users_path(sort: 'late_created') do
Late created
= link_to 'New User', new_admin_user_path, class: "btn btn-new" = link_to 'New User', new_admin_user_path, class: "btn btn-new"
%ul.well-list %ul.well-list
- @users.each do |user| - @users.each do |user|
......
= form_tag(user_omniauth_callback_path(:ldap), id: 'new_ldap_user' ) do = form_tag(user_omniauth_callback_path(provider), id: 'new_ldap_user' ) do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"} = text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
%br/ %br/
......
...@@ -4,20 +4,22 @@ ...@@ -4,20 +4,22 @@
.login-body .login-body
- if ldap_enabled? && gitlab_config.signin_enabled - if ldap_enabled? && gitlab_config.signin_enabled
%ul.nav.nav-tabs %ul.nav.nav-tabs
%li.active - @ldap_servers.each_with_index do |server, i|
= link_to 'LDAP', '#tab-ldap', 'data-toggle' => 'tab' %li{class: (:active if i==0)}
= link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
%li %li
= link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab' = link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
.tab-content .tab-content
%div#tab-ldap.tab-pane.active - @ldap_servers.each_with_index do |server,i|
= render partial: 'devise/sessions/new_ldap' %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i==0)}
= render 'devise/sessions/new_ldap', provider: server['provider_name']
%div#tab-signin.tab-pane %div#tab-signin.tab-pane
= render partial: 'devise/sessions/new_base' = render 'devise/sessions/new_base'
- elsif ldap_enabled? - elsif ldap_enabled?
= render partial: 'devise/sessions/new_ldap' = render 'devise/sessions/new_ldap', ldap_servers: @ldap_servers
- elsif gitlab_config.signin_enabled - elsif gitlab_config.signin_enabled
= render partial: 'devise/sessions/new_base' = render 'devise/sessions/new_base'
- else - else
%div %div
No authentication methods configured. No authentication methods configured.
...@@ -36,7 +38,6 @@ ...@@ -36,7 +38,6 @@
%span.light Did not receive confirmation email? %span.light Did not receive confirmation email?
= link_to "Send again", new_confirmation_path(resource_name) = link_to "Send again", new_confirmation_path(resource_name)
- if extra_config.has_key?('sign_in_text') - if extra_config.has_key?('sign_in_text')
%hr %hr
= markdown(extra_config.sign_in_text) = markdown(extra_config.sign_in_text)
%li.commit %li.commit
.commit-row-title .commit-row-title
= link_to commit[:id][0..8], project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit_short_id", alt: ''
&nbsp; &nbsp;
= gfm event_commit_title(commit[:message]), project = gfm event_commit_title(commit[:message]), project
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- event.commits.first(15).each do |commit| - event.commits.first(15).each do |commit|
%p %p
%strong= commit[:author][:name] %strong= commit[:author][:name]
= link_to "(##{commit[:id][0...8]})", project_commit_path(event.project, id: commit[:id]) = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id])
%i %i
at at
= commit[:timestamp].to_time.to_s(:short) = commit[:timestamp].to_time.to_s(:short)
......
...@@ -22,4 +22,4 @@ ...@@ -22,4 +22,4 @@
- if event.commits_count > 2 - if event.commits_count > 2
%span ... and #{event.commits_count - 2} more commits. %span ... and #{event.commits_count - 2} more commits.
= link_to project_compare_path(event.project, from: event.commit_from, to: event.commit_to) do = link_to project_compare_path(event.project, from: event.commit_from, to: event.commit_to) do
%strong Compare &rarr; #{event.commit_from[0..7]}...#{event.commit_to[0..7]} %strong Compare &rarr; #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)}
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
= @group.name = @group.name
- if @group.description.present? - if @group.description.present?
%p %p
= auto_link @group.description, link: :urls = escaped_autolink(@group.description)
= render "projects", projects: @projects = render "projects", projects: @projects
%br %br
= render "shared_projects", projects: @shared_projects = render "shared_projects", projects: @shared_projects
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
%br %br
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br %br
Read more about GitLab at #{link_to "www.gitlab.com", "https://www.gitlab.com/", target: "_blank"}. Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
%hr %hr
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
%ul.well-list %ul.well-list
%li %li
See our website for See our website for
= link_to "getting help", "https://www.gitlab.com/getting-help/" = link_to 'getting help', promo_url + '/getting-help/'
%li %li
Use the Use the
= link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)' = link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)'
......
...@@ -31,12 +31,12 @@ ...@@ -31,12 +31,12 @@
.clearfix .clearfix
%hr %hr
%p
You can also specify notification level per group or per project
%br
By default all projects and groups uses notification level set above
.row.all-notifications .row.all-notifications
.col-md-6 .col-md-6
%p
You can also specify notification level per group or per project.
%br
By default all projects and groups uses notification level set above.
%h4 Groups: %h4 Groups:
%ul.bordered-list %ul.bordered-list
- @group_members.each do |users_group| - @group_members.each do |users_group|
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
= render 'settings', type: 'group', membership: users_group, notification: notification = render 'settings', type: 'group', membership: users_group, notification: notification
.col-md-6 .col-md-6
%p
To specify notification level per project of a group you belong to,
%br
you need to be a member of the project itself, not only its group.
%h4 Projects: %h4 Projects:
%ul.bordered-list %ul.bordered-list
- @project_members.each do |project_member| - @project_members.each do |project_member|
......
.form-actions
.commit-button-annotation
= button_tag 'Commit Changes',
class: 'btn commit-btn js-commit-button btn-create'
.message
to branch
%strong= ref
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.project-home-row .project-home-row
.project-home-desc .project-home-desc
- if @project.description.present? - if @project.description.present?
= auto_link ERB::Util.html_escape(@project.description), link: :urls = escaped_autolink(@project.description)
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
&ndash; &ndash;
= link_to 'Edit', edit_project_path = link_to 'Edit', edit_project_path
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
%tr %tr
%td.blame-commit %td.blame-commit
%span.commit %span.commit
= link_to commit.short_id(8), project_commit_path(@project, commit), class: "commit_short_id" = link_to commit.short_id, project_commit_path(@project, commit), class: "commit_short_id"
&nbsp; &nbsp;
= commit_author_link(commit, avatar: true, size: 16) = commit_author_link(commit, avatar: true, size: 16)
&nbsp; &nbsp;
......
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
.center .center
= link_to project_raw_path(@project, @id) do = link_to project_raw_path(@project, @id) do
%h1.light %h1.light
%i.fa.fa-arrow-circle-o-down-alt %i.fa.fa-download
%h4 %h4
Download (#{number_to_human_size blob.size}) Download (#{number_to_human_size blob.size})
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= @notes_count = @notes_count
.pull-left.btn-group .pull-left.btn-group
%a.btn.btn-grouped.dropdown-toggle{ data: {toggle: :dropdown} } %a.btn.btn-grouped.dropdown-toggle{ data: {toggle: :dropdown} }
%i.fa.fa-arrow-circle-o-down-alt %i.fa.fa-download
Download as Download as
%span.caret %span.caret
%ul.dropdown-menu %ul.dropdown-menu
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
.commit-info-row .commit-info-row
%span.cgray= pluralize(@commit.parents.count, "parent") %span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent| - @commit.parents.each do |parent|
= link_to parent.id[0...10], project_commit_path(@project, parent) = link_to parent.short_id, project_commit_path(@project, parent)
- if @branches.any? - if @branches.any?
.commit-info-row .commit-info-row
......
%li.commit.js-toggle-container %li.commit.js-toggle-container
.commit-row-title .commit-row-title
= link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id"
&nbsp; &nbsp;
%span.str-truncated %span.str-truncated
= link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message"
......
%li.commit.inline-commit %li.commit.inline-commit
.commit-row-title .commit-row-title
= link_to commit.short_id(8), project_commit_path(project, commit), class: "commit_short_id" = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id"
&nbsp; &nbsp;
%span.str-truncated %span.str-truncated
= link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message"
......
...@@ -93,101 +93,92 @@ ...@@ -93,101 +93,92 @@
.danger-settings.js-toggle-container .danger-settings
.centered-light-block - if can? current_user, :archive_project, @project
%h3 - if @project.archived?
%i.fa.fa-exclamation-triangle .panel.panel-success
Dangerous settings
%p Project settings below may result in data loss!
= link_to '#', class: 'btn js-toggle-button' do
Show them to me
%i.fa.fa-chevron-down
.js-toggle-content.hide
- if can? current_user, :archive_project, @project
.panel.panel-default.panel.panel-warning
.panel-heading .panel-heading
- if @project.archived? Unarchive project
Unarchive project
- else
Archive project
.panel-body .panel-body
- if @project.archived? %p
%p Unarchiving the project will mark its repository as active.
Unarchiving the project will mark its repository as active. %br
%br The project can be committed to.
The project can be committed to. %br
%br %strong Once active this project shows up in the search and on the dashboard.
%strong Once active this project shows up in the search and on the dashboard. = link_to 'Unarchive', unarchive_project_path(@project),
= link_to 'Unarchive', unarchive_project_path(@project), data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." },
data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, method: :post, class: "btn btn-success"
method: :post, class: "btn btn-remove"
- else
%p
Archiving the project will mark its repository as read-only.
%br
It is hidden from the dashboard and doesn't show up in searches.
%br
%strong Archived projects cannot be committed to!
= link_to 'Archive', archive_project_path(@project),
data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
method: :post, class: "btn btn-warning"
- else - else
.nothing-here-block Only the project owner can archive a project .panel.panel-warning
.panel-heading
.panel.panel-default.panel.panel-warning Archive project
.panel-heading Rename repository .panel-body
%p
Archiving the project will mark its repository as read-only.
%br
It is hidden from the dashboard and doesn't show up in searches.
%br
%strong Archived projects cannot be committed to!
= link_to 'Archive', archive_project_path(@project),
data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." },
method: :post, class: "btn btn-warning"
- else
.nothing-here-block Only the project owner can archive a project
.panel.panel-default.panel.panel-warning
.panel-heading Rename repository
.errors-holder
.panel-body
= form_for(@project, html: { class: 'form-horizontal' }) do |f|
.form-group
= f.label :path, class: 'control-label' do
%span Path
.col-sm-9
.form-group
.input-group
= f.text_field :path, class: 'form-control'
%span.input-group-addon .git
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
.form-actions
= f.submit 'Rename', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
.panel.panel-default.panel.panel-danger
.panel-heading Transfer project
.errors-holder .errors-holder
.panel-body .panel-body
= form_for(@project, html: { class: 'form-horizontal' }) do |f| = form_for(@project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f|
.form-group .form-group
= f.label :path, class: 'control-label' do = f.label :namespace_id, class: 'control-label' do
%span Path %span Namespace
.col-sm-9 .col-sm-10
.form-group .form-group
.input-group = f.select :namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace' }, { class: 'select2' }
= f.text_field :path, class: 'form-control'
%span.input-group-addon .git
%ul %ul
%li Be careful. Renaming a project's repository can have unintended side effects. %li Be careful. Changing the project's namespace can have unintended side effects.
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location. %li You will need to update your local repositories to point to the new location.
.form-actions .form-actions
= f.submit 'Rename', class: "btn btn-warning" = f.submit 'Transfer', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- else
- if can?(current_user, :change_namespace, @project) .nothing-here-block Only the project owner can transfer a project
.panel.panel-default.panel.panel-danger
.panel-heading Transfer project
.errors-holder
.panel-body
= form_for(@project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f|
.form-group
= f.label :namespace_id, class: 'control-label' do
%span Namespace
.col-sm-10
.form-group
= f.select :namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace' }, { class: 'select2' }
%ul
%li Be careful. Changing the project's namespace can have unintended side effects.
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location.
.form-actions
= f.submit 'Transfer', class: "btn btn-remove"
- else
.nothing-here-block Only the project owner can transfer a project
- if can?(current_user, :remove_project, @project) - if can?(current_user, :remove_project, @project)
.panel.panel-default.panel.panel-danger .panel.panel-default.panel.panel-danger
.panel-heading Remove project .panel-heading Remove project
.panel-body .panel-body
= form_tag(project_path(@project), method: :delete, html: { class: 'form-horizontal'}) do
%p %p
Removing the project will delete its repository and all related resources including issues, merge requests etc. Removing the project will delete its repository and all related resources including issues, merge requests etc.
%br %br
%strong Removed projects cannot be restored! %strong Removed projects cannot be restored!
= link_to 'Remove project', @project, data: { confirm: remove_project_message(@project) }, method: :delete, class: "btn btn-remove" = link_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
- else - else
.nothing-here-block Only project owner can remove a project .nothing-here-block Only project owner can remove a project
.save-project-loader.hide .save-project-loader.hide
.center .center
...@@ -195,3 +186,6 @@ ...@@ -195,3 +186,6 @@
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
Saving project. Saving project.
%p Please wait a moment, this page will automatically refresh when ready. %p Please wait a moment, this page will automatically refresh when ready.
= render 'shared/confirm_modal', phrase: @project.path
...@@ -23,16 +23,11 @@ ...@@ -23,16 +23,11 @@
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
= render 'shared/commit_message_container', params: params, = render 'shared/commit_message_container', params: params,
placeholder: "Update #{@blob.name}" placeholder: "Update #{@blob.name}"
.form-actions = hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] = render 'projects/commit_button', ref: @ref,
.commit-button-annotation cancel_path: @after_edit_path
= button_tag "Commit changes", class: 'btn commit-btn js-commit-button btn-primary'
.message
to branch
%strong= @ref
= link_to "Cancel", @after_edit_path, class: "btn btn-cancel", data: { confirm: leave_edit_message}
:javascript :javascript
ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace") ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace")
......
...@@ -26,6 +26,10 @@ ...@@ -26,6 +26,10 @@
%span %span
%i.fa.fa-clock-o %i.fa.fa-clock-o
= issue.milestone.title = issue.milestone.title
- if issue.tasks?
%span.task-status
= issue.task_status
.pull-right .pull-right
%small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')} %small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')}
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
.description .description
.wiki .wiki
= preserve do = preserve do
= markdown @issue.description = markdown(@issue.description, parse_tasks: true)
.context .context
%cite.cgray %cite.cgray
= render partial: 'issue_context', locals: { issue: @issue } = render partial: 'issue_context', locals: { issue: @issue }
...@@ -62,7 +62,8 @@ ...@@ -62,7 +62,8 @@
= link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" = link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue"
.participants .participants
%cite.cgray #{@issue.participants.count} participants %cite.cgray
= pluralize(@issue.participants.count, 'participant')
- @issue.participants.each do |participant| - @issue.participants.each do |participant|
= link_to_member(@project, participant, name: false, size: 24) = link_to_member(@project, participant, name: false, size: 24)
......
...@@ -27,7 +27,9 @@ ...@@ -27,7 +27,9 @@
%span %span
%i.fa.fa-clock-o %i.fa.fa-clock-o
= merge_request.milestone.title = merge_request.milestone.title
- if merge_request.tasks?
%span.task-status
= merge_request.task_status
.pull-right .pull-right
%small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')}
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
.description .description
.wiki .wiki
= preserve do = preserve do
= markdown @merge_request.description = markdown(@merge_request.description, parse_tasks: true)
.context .context
%cite.cgray %cite.cgray
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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