Commit fb0d20c5 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into issue_3400_port

parents b4593a1b 2bcbc7c6
This diff is collapsed.
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.6.0 (unreleased) v 8.6.0 (unreleased)
- Add ability to move issue to another project
- Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Add confidential issues - Add confidential issues
- Bump gitlab_git to 9.0.3 (Stan Hu) - Bump gitlab_git to 9.0.3 (Stan Hu)
- Fix diff image view modes (2-up, swipe, onion skin) not working (Stan Hu)
- Support Golang subpackage fetching (Stan Hu) - Support Golang subpackage fetching (Stan Hu)
- Bump Capybara gem to 2.6.2 (Stan Hu) - Bump Capybara gem to 2.6.2 (Stan Hu)
- New branch button appears on issues where applicable - New branch button appears on issues where applicable
- Contributions to forked projects are included in calendar - Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea) - Improve the formatting for the user page bio (Connor Shea)
- Easily (un)mark merge request as WIP using link
- Use specialized system notes when MR is (un)marked as WIP
- Removed the default password from the initial admin account created during - Removed the default password from the initial admin account created during
setup. A password can be provided during setup (see installation docs), or setup. A password can be provided during setup (see installation docs), or
GitLab will ask the user to create a new one upon first visit. GitLab will ask the user to create a new one upon first visit.
...@@ -20,6 +26,7 @@ v 8.6.0 (unreleased) ...@@ -20,6 +26,7 @@ v 8.6.0 (unreleased)
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
- Added omniauth-auth0 Gem (Daniel Carraro) - Added omniauth-auth0 Gem (Daniel Carraro)
- Add label description in tooltip to labels in issue index and sidebar
- Strip leading and trailing spaces in URL validator (evuez) - Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API - Return empty array instead of 404 when commit has no statuses in commit status API
......
...@@ -222,6 +222,8 @@ gem 'net-ssh', '~> 3.0.1' ...@@ -222,6 +222,8 @@ gem 'net-ssh', '~> 3.0.1'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 0.15' gem 'sentry-raven', '~> 0.15'
gem 'premailer-rails', '~> 1.9.0'
# Metrics # Metrics
group :metrics do group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'allocations', '~> 1.0', require: false, platform: :mri
...@@ -285,7 +287,7 @@ group :development, :test do ...@@ -285,7 +287,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.35.0', require: false gem 'rubocop', '~> 0.38.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false gem 'simplecov', '~> 0.10.0', require: false
......
...@@ -61,9 +61,7 @@ GEM ...@@ -61,9 +61,7 @@ GEM
faraday_middleware-multi_json (~> 0.0) faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0) oauth2 (~> 1.0)
asciidoctor (1.5.3) asciidoctor (1.5.3)
ast (2.1.0) ast (2.2.0)
astrolabe (1.3.1)
parser (~> 2.2)
attr_encrypted (1.3.4) attr_encrypted (1.3.4)
encryptor (>= 1.3.0) encryptor (>= 1.3.0)
attr_required (1.0.0) attr_required (1.0.0)
...@@ -150,6 +148,8 @@ GEM ...@@ -150,6 +148,8 @@ GEM
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
creole (0.5.0) creole (0.5.0)
css_parser (1.3.7)
addressable
d3_rails (3.5.11) d3_rails (3.5.11)
railties (>= 3.1.0) railties (>= 3.1.0)
daemons (1.2.3) daemons (1.2.3)
...@@ -423,6 +423,7 @@ GEM ...@@ -423,6 +423,7 @@ GEM
haml (~> 4.0.0) haml (~> 4.0.0)
nokogiri (~> 1.6.0) nokogiri (~> 1.6.0)
ruby_parser (~> 3.5) ruby_parser (~> 3.5)
htmlentities (4.3.4)
http-cookie (1.0.2) http-cookie (1.0.2)
domain_name (~> 0.5) domain_name (~> 0.5)
http_parser.rb (0.5.3) http_parser.rb (0.5.3)
...@@ -554,8 +555,8 @@ GEM ...@@ -554,8 +555,8 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
paranoia (2.1.4) paranoia (2.1.4)
activerecord (~> 4.0) activerecord (~> 4.0)
parser (2.2.3.0) parser (2.3.0.6)
ast (>= 1.1, < 3.0) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
poltergeist (1.9.0) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
...@@ -564,6 +565,12 @@ GEM ...@@ -564,6 +565,12 @@ GEM
websocket-driver (>= 0.2.0) websocket-driver (>= 0.2.0)
posix-spawn (0.3.11) posix-spawn (0.3.11)
powerpack (0.1.1) powerpack (0.1.1)
premailer (1.8.6)
css_parser (>= 1.3.6)
htmlentities (>= 4.0.0)
premailer-rails (1.9.0)
actionmailer (>= 3, < 5)
premailer (~> 1.7, >= 1.7.9)
pry (0.10.3) pry (0.10.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -615,7 +622,7 @@ GEM ...@@ -615,7 +622,7 @@ GEM
activesupport (= 4.2.5.2) activesupport (= 4.2.5.2)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.0.0) rainbow (2.1.0)
raindrops (0.15.0) raindrops (0.15.0)
rake (10.5.0) rake (10.5.0)
raphael-rails (2.1.2) raphael-rails (2.1.2)
...@@ -687,13 +694,12 @@ GEM ...@@ -687,13 +694,12 @@ GEM
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-support (3.3.0) rspec-support (3.3.0)
rubocop (0.35.1) rubocop (0.38.0)
astrolabe (~> 1.3) parser (>= 2.3.0.6, < 3.0)
parser (>= 2.2.3.0, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
tins (<= 1.6.0) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-progressbar (1.7.5) ruby-progressbar (1.7.5)
...@@ -843,6 +849,7 @@ GEM ...@@ -843,6 +849,7 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.1) unf_ext (0.0.7.1)
unicode-display_width (1.0.2)
unicorn (4.9.0) unicorn (4.9.0)
kgio (~> 2.6) kgio (~> 2.6)
rack rack
...@@ -992,6 +999,7 @@ DEPENDENCIES ...@@ -992,6 +999,7 @@ DEPENDENCIES
paranoia (~> 2.0) paranoia (~> 2.0)
pg (~> 0.18.2) pg (~> 0.18.2)
poltergeist (~> 1.9.0) poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
pry-rails pry-rails
quiet_assets (~> 1.0.2) quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.1) rack-attack (~> 4.3.1)
...@@ -1013,7 +1021,7 @@ DEPENDENCIES ...@@ -1013,7 +1021,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0) rspec-rails (~> 3.3.0)
rspec-retry rspec-retry
rubocop (~> 0.35.0) rubocop (~> 0.38.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.0) sass-rails (~> 5.0.0)
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#= require jquery #= require jquery
#= require jquery-ui/autocomplete #= require jquery-ui/autocomplete
#= require jquery-ui/datepicker #= require jquery-ui/datepicker
#= require jquery-ui/draggable
#= require jquery-ui/effect-highlight #= require jquery-ui/effect-highlight
#= require jquery-ui/sortable #= require jquery-ui/sortable
#= require jquery_ujs #= require jquery_ujs
......
...@@ -5,7 +5,6 @@ class @Aside ...@@ -5,7 +5,6 @@ class @Aside
e.preventDefault() e.preventDefault()
btn = $(e.currentTarget) btn = $(e.currentTarget)
icon = btn.find('i') icon = btn.find('i')
console.log('1')
if icon.hasClass('fa-angle-left') if icon.hasClass('fa-angle-left')
btn.parent().find('section').hide() btn.parent().find('section').hide()
......
...@@ -197,7 +197,11 @@ class GitLabDropdown ...@@ -197,7 +197,11 @@ class GitLabDropdown
hidden: => hidden: =>
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").blur().val("") @dropdown
.find(".dropdown-input-field")
.blur()
.val("")
.trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
......
class @IssuableForm class @IssuableForm
issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?'
wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
constructor: (@form) -> constructor: (@form) ->
GitLab.GfmAutoComplete.setup() GitLab.GfmAutoComplete.setup()
new UsersSelect() new UsersSelect()
...@@ -6,14 +9,17 @@ class @IssuableForm ...@@ -6,14 +9,17 @@ class @IssuableForm
@titleField = @form.find("input[name*='[title]']") @titleField = @form.find("input[name*='[title]']")
@descriptionField = @form.find("textarea[name*='[description]']") @descriptionField = @form.find("textarea[name*='[description]']")
@issueMoveField = @form.find("#move_to_project_id")
return unless @titleField.length && @descriptionField.length return unless @titleField.length && @descriptionField.length
@initAutosave() @initAutosave()
@form.on "submit", @resetAutosave @form.on "submit", @handleSubmit
@form.on "click", ".btn-cancel", @resetAutosave @form.on "click", ".btn-cancel", @resetAutosave
@initWip()
initAutosave: -> initAutosave: ->
new Autosave @titleField, [ new Autosave @titleField, [
document.location.pathname, document.location.pathname,
...@@ -27,6 +33,50 @@ class @IssuableForm ...@@ -27,6 +33,50 @@ class @IssuableForm
"description" "description"
] ]
handleSubmit: =>
if (parseInt(@issueMoveField?.val()) ? 0) > 0
return false unless confirm(@issueMoveConfirmMsg)
@resetAutosave()
resetAutosave: => resetAutosave: =>
@titleField.data("autosave").reset() @titleField.data("autosave").reset()
@descriptionField.data("autosave").reset() @descriptionField.data("autosave").reset()
initWip: ->
@$wipExplanation = @form.find(".js-wip-explanation")
@$noWipExplanation = @form.find(".js-no-wip-explanation")
return unless @$wipExplanation.length and @$noWipExplanation.length
@form.on "click", ".js-toggle-wip", @toggleWip
@titleField.on "keyup blur", @renderWipExplanation
@renderWipExplanation()
workInProgress: ->
@wipRegex.test @titleField.val()
renderWipExplanation: =>
if @workInProgress()
@$wipExplanation.show()
@$noWipExplanation.hide()
else
@$wipExplanation.hide()
@$noWipExplanation.show()
toggleWip: (event) =>
event.preventDefault()
if @workInProgress()
@removeWip()
else
@addWip()
@renderWipExplanation()
removeWip: ->
@titleField.val @titleField.val().replace(@wipRegex, "")
addWip: ->
@titleField.val "WIP: #{@titleField.val()}"
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
# Handles persisting and restoring the current tab selection and lazily-loading # Handles persisting and restoring the current tab selection and lazily-loading
# content on the MergeRequests#show page. # content on the MergeRequests#show page.
# #
#= require jquery.cookie
#
# ### Example Markup # ### Example Markup
# #
# <ul class="nav-links merge-request-tabs"> # <ul class="nav-links merge-request-tabs">
...@@ -68,11 +70,15 @@ class @MergeRequestTabs ...@@ -68,11 +70,15 @@ class @MergeRequestTabs
if action == 'commits' if action == 'commits'
@loadCommits($target.attr('href')) @loadCommits($target.attr('href'))
@expandView()
else if action == 'diffs' else if action == 'diffs'
@loadDiff($target.attr('href')) @loadDiff($target.attr('href'))
@shrinkView() @shrinkView()
else if action == 'builds' else if action == 'builds'
@loadBuilds($target.attr('href')) @loadBuilds($target.attr('href'))
@expandView()
else
@expandView()
@setCurrentAction(action) @setCurrentAction(action)
...@@ -189,11 +195,24 @@ class @MergeRequestTabs ...@@ -189,11 +195,24 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited') $('.container-fluid').removeClass('container-limited')
shrinkView: -> shrinkView: ->
$gutterIcon = $('.js-sidebar-toggle i') $gutterIcon = $('.js-sidebar-toggle i:visible')
# Wait until listeners are set # Wait until listeners are set
setTimeout( -> setTimeout( ->
# Only when sidebar is collapsed # Only when sidebar is expanded
if $gutterIcon.is('.fa-angle-double-right') if $gutterIcon.is('.fa-angle-double-right')
$gutterIcon.closest('a').trigger('click',[true]) $gutterIcon.closest('a').trigger('click', [true])
, 0)
# Expand the issuable sidebar unless the user explicitly collapsed it
expandView: ->
return if $.cookie('collapsed_gutter') == 'true'
$gutterIcon = $('.js-sidebar-toggle i:visible')
# Wait until listeners are set
setTimeout( ->
# Only when sidebar is collapsed
if $gutterIcon.is('.fa-angle-double-left')
$gutterIcon.closest('a').trigger('click', [true])
, 0) , 0)
...@@ -361,14 +361,12 @@ class @Notes ...@@ -361,14 +361,12 @@ class @Notes
showEditForm: (e) -> showEditForm: (e) ->
e.preventDefault() e.preventDefault()
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").hide() note.addClass "is-editting"
note.find(".note-header").hide()
form = note.find(".note-edit-form") form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)') isNewForm = form.is(':not(.gfm-form)')
if isNewForm if isNewForm
form.addClass('gfm-form') form.addClass('gfm-form')
form.addClass('current-note-edit-form') form.addClass('current-note-edit-form')
form.show()
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
...@@ -402,11 +400,9 @@ class @Notes ...@@ -402,11 +400,9 @@ class @Notes
cancelEdit: (e) -> cancelEdit: (e) ->
e.preventDefault() e.preventDefault()
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").show() note.removeClass "is-editting"
note.find(".note-header").show()
note.find(".current-note-edit-form") note.find(".current-note-edit-form")
.removeClass("current-note-edit-form") .removeClass("current-note-edit-form")
.hide()
### ###
Called in response to deleting a note of any kind. Called in response to deleting a note of any kind.
......
...@@ -11,7 +11,6 @@ class @Project ...@@ -11,7 +11,6 @@ class @Project
$(@).toggleClass('active') $(@).toggleClass('active')
url = $("#project_clone").val() url = $("#project_clone").val()
console.log("url",url)
# Update the input field # Update the input field
$('#project_clone').val(url) $('#project_clone').val(url)
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
} }
&.group-avatar, &.project-avatar, &.avatar-tile { &.group-avatar, &.project-avatar, &.avatar-tile {
@include border-radius(0px); @include border-radius(0);
} }
&.s16 { width: 16px; height: 16px; margin-right: 6px; } &.s16 { width: 16px; height: 16px; margin-right: 6px; }
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
} }
.select2-drop { .select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
@include border-radius ($border-radius-default); @include border-radius ($border-radius-default);
border: none; border: none;
} }
......
img {
max-width: 100%;
height: auto;
}
p.details {
font-style:italic;
color:#777
}
.footer p {
font-size:small;
color:#777
}
pre.commit-message {
white-space: pre-wrap;
}
.file-stats a {
text-decoration: none;
}
.file-stats .new-file {
color: #090;
}
.file-stats .deleted-file {
color: #B00;
}
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
display: none; display: none;
} }
.new_note, .edit_note { .new_note, .note-edit-form {
.note-form-actions { .note-form-actions {
margin-top: $gl-padding; margin-top: $gl-padding;
} }
......
...@@ -100,6 +100,18 @@ ul.notes { ...@@ -100,6 +100,18 @@ ul.notes {
display: block; display: block;
position: relative; position: relative;
&.is-editting {
.note-header,
.note-text,
.edited-text {
display: none;
}
.note-edit-form {
display: block;
}
}
.note-body { .note-body {
overflow: auto; overflow: auto;
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
.dropdown-menu { .dropdown-menu {
left: auto; left: auto;
width: auto; width: auto;
right: 0px; right: 0;
max-width: 240px; max-width: 240px;
} }
} }
......
...@@ -5,12 +5,12 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -5,12 +5,12 @@ class Admin::GroupsController < Admin::ApplicationController
@groups = Group.all @groups = Group.all
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]).per(PER_PAGE) @groups = @groups.page(params[:page])
end end
def show def show
@members = @group.members.order("access_level DESC").page(params[:members_page]).per(PER_PAGE) @members = @group.members.order("access_level DESC").page(params[:members_page])
@projects = @group.projects.page(params[:projects_page]).per(PER_PAGE) @projects = @group.projects.page(params[:projects_page])
end end
def new def new
......
...@@ -2,7 +2,7 @@ class Admin::LabelsController < Admin::ApplicationController ...@@ -2,7 +2,7 @@ class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy] before_action :set_label, only: [:show, :edit, :update, :destroy]
def index def index
@labels = Label.templates.page(params[:page]).per(PER_PAGE) @labels = Label.templates.page(params[:page])
end end
def show def show
......
...@@ -12,15 +12,15 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -12,15 +12,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.non_archived unless params[:with_archived].present?
@projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
end end
def show def show
if @group if @group
@group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(PER_PAGE) @group_members = @group.members.order("access_level DESC").page(params[:group_members_page])
end end
@project_members = @project.project_members.page(params[:project_members_page]).per(PER_PAGE) @project_members = @project.project_members.page(params[:project_members_page])
end end
def transfer def transfer
......
...@@ -6,8 +6,6 @@ class ApplicationController < ActionController::Base ...@@ -6,8 +6,6 @@ class ApplicationController < ActionController::Base
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
PER_PAGE = 20
before_action :authenticate_user_from_token! before_action :authenticate_user_from_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
......
...@@ -7,7 +7,7 @@ class AutocompleteController < ApplicationController ...@@ -7,7 +7,7 @@ class AutocompleteController < ApplicationController
@users = @users.search(params[:search]) if params[:search].present? @users = @users.search(params[:search]) if params[:search].present?
@users = @users.active @users = @users.active
@users = @users.reorder(:name) @users = @users.reorder(:name)
@users = @users.page(params[:page]).per(PER_PAGE) @users = @users.page(params[:page])
if params[:search].blank? if params[:search].blank?
# Include current user if available to filter by "Me" # Include current user if available to filter by "Me"
......
...@@ -6,7 +6,7 @@ module GlobalMilestones ...@@ -6,7 +6,7 @@ module GlobalMilestones
@milestones = MilestonesFinder.new.execute(@projects, params) @milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones) @milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
@milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE) @milestones = Kaminari.paginate_array(@milestones).page(params[:page])
end end
def milestone def milestone
......
...@@ -3,7 +3,7 @@ module IssuesAction ...@@ -3,7 +3,7 @@ module IssuesAction
def issues def issues
@issues = get_issues_collection.non_archived @issues = get_issues_collection.non_archived
@issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE) @issues = @issues.page(params[:page])
@issues = @issues.preload(:author, :project) @issues = @issues.preload(:author, :project)
@label = @issuable_finder.labels.first @label = @issuable_finder.labels.first
......
...@@ -3,7 +3,7 @@ module MergeRequestsAction ...@@ -3,7 +3,7 @@ module MergeRequestsAction
def merge_requests def merge_requests
@merge_requests = get_merge_requests_collection.non_archived @merge_requests = get_merge_requests_collection.non_archived
@merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:author, :target_project) @merge_requests = @merge_requests.preload(:author, :target_project)
@label = @issuable_finder.labels.first @label = @issuable_finder.labels.first
......
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
def index def index
@group_members = current_user.group_members.page(params[:page]).per(PER_PAGE) @group_members = current_user.group_members.page(params[:page])
end end
end end
...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) @projects = @projects.page(params[:page])
@last_push = current_user.recent_push @last_push = current_user.recent_push
...@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) @projects = @projects.page(params[:page])
@last_push = current_user.recent_push @last_push = current_user.recent_push
@groups = [] @groups = []
......
...@@ -6,6 +6,6 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController ...@@ -6,6 +6,6 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController
user: current_user, user: current_user,
scope: params[:scope] scope: params[:scope]
) )
@snippets = @snippets.page(params[:page]).per(PER_PAGE) @snippets = @snippets.page(params[:page])
end end
end end
...@@ -2,7 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -2,7 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy, :destroy_all] before_action :find_todos, only: [:index, :destroy, :destroy_all]
def index def index
@todos = @todos.page(params[:page]).per(PER_PAGE) @todos = @todos.page(params[:page])
end end
def destroy def destroy
......
...@@ -3,6 +3,6 @@ class Explore::GroupsController < Explore::ApplicationController ...@@ -3,6 +3,6 @@ class Explore::GroupsController < Explore::ApplicationController
@groups = Group.order_id_desc @groups = Group.order_id_desc
@groups = @groups.search(params[:search]) if params[:search].present? @groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(PER_PAGE) @groups = @groups.page(params[:page])
end end
end end
...@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) @projects = @projects.includes(:namespace).page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.page(params[:page]).per(PER_PAGE) @projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC') @projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) @projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
......
class Explore::SnippetsController < Explore::ApplicationController class Explore::SnippetsController < Explore::ApplicationController
def index def index
@snippets = SnippetsFinder.new.execute(current_user, filter: :all) @snippets = SnippetsFinder.new.execute(current_user, filter: :all)
@snippets = @snippets.page(params[:page]).per(PER_PAGE) @snippets = @snippets.page(params[:page])
end end
end end
...@@ -44,7 +44,7 @@ class GroupsController < Groups::ApplicationController ...@@ -44,7 +44,7 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]) if params[:filter_projects].blank?
@shared_projects = @group.shared_projects @shared_projects = @group.shared_projects
......
...@@ -7,6 +7,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController ...@@ -7,6 +7,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
if pre_auth.authorizable? if pre_auth.authorizable?
if skip_authorization? || matching_token? if skip_authorization? || matching_token?
auth = authorization.authorize auth = authorization.authorize
session.delete(:user_return_to)
redirect_to auth.redirect_uri redirect_to auth.redirect_uri
else else
render "doorkeeper/authorizations/new" render "doorkeeper/authorizations/new"
......
...@@ -34,8 +34,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -34,8 +34,7 @@ class ProfilesController < Profiles::ApplicationController
def audit_log def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id). @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC"). order("created_at DESC").
page(params[:page]). page(params[:page])
per(PER_PAGE)
end end
def update_username def update_username
......
...@@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController
def index def index
@sort = params[:sort] || 'name' @sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort) @branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) @branches = Kaminari.paginate_array(@branches).page(params[:page])
@max_commits = @branches.reduce(0) do |memo, branch| @max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch) diverging_commit_counts = repository.diverging_commit_counts(branch)
......
...@@ -15,7 +15,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class Projects::ForksController < Projects::ApplicationController
@sort = params[:sort] || 'id_desc' @sort = params[:sort] || 'id_desc'
@forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present? @forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
@forks = @forks.order_by(@sort).page(params[:page]).per(PER_PAGE) @forks = @forks.order_by(@sort).page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -33,7 +33,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -33,7 +33,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
@issues = @issues.page(params[:page]).per(PER_PAGE) @issues = @issues.page(params[:page])
@label = @project.labels.find_by(title: params[:label_name]) @label = @project.labels.find_by(title: params[:label_name])
respond_to do |format| respond_to do |format|
...@@ -90,6 +90,12 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -90,6 +90,12 @@ class Projects::IssuesController < Projects::ApplicationController
def update def update
@issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue) @issue = Issues::UpdateService.new(project, current_user, issue_params).execute(issue)
if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id])
move_service = Issues::MoveService.new(project, current_user)
@issue = move_service.execute(@issue, new_project)
end
respond_to do |format| respond_to do |format|
format.js format.js
format.html do format.html do
......
...@@ -11,7 +11,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -11,7 +11,7 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to :js, :html respond_to :js, :html
def index def index
@labels = @project.labels.page(params[:page]).per(PER_PAGE) @labels = @project.labels.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -5,7 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -5,7 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
:ci_status, :cancel_merge_when_build_succeeds :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
] ]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
...@@ -20,7 +20,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -20,7 +20,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_create_merge_request!, only: [:new, :create] before_action :authorize_create_merge_request!, only: [:new, :create]
# Allow modify merge_request # Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort] before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
def index def index
terms = params['issue_search'] terms = params['issue_search']
...@@ -34,7 +34,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -34,7 +34,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project) @merge_requests = @merge_requests.preload(:target_project)
@label = @project.labels.find_by(title: params[:label_name]) @label = @project.labels.find_by(title: params[:label_name])
...@@ -164,6 +164,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -164,6 +164,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
def remove_wip
MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged."
end
def merge_check def merge_check
@merge_request.check_if_can_be_merged @merge_request.check_if_can_be_merged
......
...@@ -22,7 +22,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -22,7 +22,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestones = @milestones.page(params[:page]).per(PER_PAGE) @milestones = @milestones.page(params[:page])
end end
format.json do format.json do
render json: @milestones render json: @milestones
......
...@@ -21,7 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -21,7 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController
filter: :by_project, filter: :by_project,
project: @project project: @project
}) })
@snippets = @snippets.page(params[:page]).per(PER_PAGE) @snippets = @snippets.page(params[:page])
end end
def new def new
......
...@@ -7,7 +7,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class Projects::TagsController < Projects::ApplicationController
def index def index
sorted = VersionSorter.rsort(@repository.tag_names) sorted = VersionSorter.rsort(@repository.tag_names)
@tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) @tags = Kaminari.paginate_array(sorted).page(params[:page])
@releases = project.releases.where(tag: @tags) @releases = project.releases.where(tag: @tags)
end end
......
...@@ -7,7 +7,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -7,7 +7,7 @@ class Projects::WikisController < Projects::ApplicationController
before_action :load_project_wiki before_action :load_project_wiki
def pages def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE) @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
end end
def show def show
......
...@@ -25,7 +25,7 @@ class SnippetsController < ApplicationController ...@@ -25,7 +25,7 @@ class SnippetsController < ApplicationController
filter: :by_user, filter: :by_user,
user: @user, user: @user,
scope: params[:scope] }). scope: params[:scope] }).
page(params[:page]).per(PER_PAGE) page(params[:page])
render 'index' render 'index'
else else
......
...@@ -100,7 +100,7 @@ class UsersController < ApplicationController ...@@ -100,7 +100,7 @@ class UsersController < ApplicationController
def load_projects def load_projects
@projects = @projects =
PersonalProjectsFinder.new(@user).execute(current_user) PersonalProjectsFinder.new(@user).execute(current_user)
.page(params[:page]).per(PER_PAGE) .page(params[:page])
end end
def load_contributed_projects def load_contributed_projects
......
...@@ -23,36 +23,34 @@ module ButtonHelper ...@@ -23,36 +23,34 @@ module ButtonHelper
end end
def http_clone_button(project) def http_clone_button(project)
klass = 'btn js-protocol-switch' klass = 'http-selector'
klass << ' active' if default_clone_protocol == 'http'
klass << ' has_tooltip' if current_user.try(:require_password?) klass << ' has_tooltip' if current_user.try(:require_password?)
protocol = gitlab_config.protocol.upcase protocol = gitlab_config.protocol.upcase
content_tag :button, protocol, content_tag :a, protocol,
class: klass, class: klass,
href: @project.http_url_to_repo,
data: { data: {
clone: project.http_url_to_repo, html: true,
placement: 'right',
container: 'body', container: 'body',
html: 'true',
title: "Set a password on your account<br>to pull or push via #{protocol}" title: "Set a password on your account<br>to pull or push via #{protocol}"
}, }
type: :button
end end
def ssh_clone_button(project) def ssh_clone_button(project)
klass = 'btn js-protocol-switch' klass = 'ssh-selector'
klass << ' active' if default_clone_protocol == 'ssh'
klass << ' has_tooltip' if current_user.try(:require_ssh_key?) klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
content_tag :button, 'SSH', content_tag :a, 'SSH',
class: klass, class: klass,
href: project.ssh_url_to_repo,
data: { data: {
clone: project.ssh_url_to_repo, html: true,
placement: 'right',
container: 'body', container: 'body',
html: 'true',
title: 'Add an SSH key to your profile<br>to pull or push via SSH.' title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
}, }
type: :button
end end
end end
...@@ -57,6 +57,19 @@ module IssuesHelper ...@@ -57,6 +57,19 @@ module IssuesHelper
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end end
def project_options(issuable, current_user, ability: :read_project)
projects = current_user.authorized_projects
projects = projects.select do |project|
current_user.can?(ability, project)
end
no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project')
projects.unshift(no_project)
projects.delete(issuable.project)
options_from_collection_for_select(projects, :id, :name_with_namespace)
end
def status_box_class(item) def status_box_class(item)
if item.respond_to?(:expired?) && item.expired? if item.respond_to?(:expired?) && item.expired?
'status-box-expired' 'status-box-expired'
......
...@@ -32,7 +32,7 @@ module LabelsHelper ...@@ -32,7 +32,7 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" } # link_to_label(label) { "My Custom Label Text" }
# #
# Returns a String # Returns a String
def link_to_label(label, project: nil, type: :issue, &block) def link_to_label(label, project: nil, type: :issue, tooltip: true, &block)
project ||= @project || label.project project ||= @project || label.project
link = send("namespace_project_#{type.to_s.pluralize}_path", link = send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace, project.namespace,
...@@ -42,7 +42,7 @@ module LabelsHelper ...@@ -42,7 +42,7 @@ module LabelsHelper
if block_given? if block_given?
link_to link, &block link_to link, &block
else else
link_to render_colored_label(label), link link_to render_colored_label(label, tooltip: tooltip), link
end end
end end
...@@ -50,23 +50,24 @@ module LabelsHelper ...@@ -50,23 +50,24 @@ module LabelsHelper
@project.labels.pluck(:title) @project.labels.pluck(:title)
end end
def render_colored_label(label, label_suffix = '') def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color) text_color = text_color_for_bg(label_color)
# Intentionally not using content_tag here so that this method can be called # Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter # by LabelReferenceFilter
span = %(<span class="label color-label") + span = %(<span class="label color-label #{"has_tooltip" if tooltip}" ) +
%(style="background-color: #{label_color}; color: #{text_color}">) + %(style="background-color: #{label_color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>) %(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe span.html_safe
end end
def render_colored_cross_project_label(label) def render_colored_cross_project_label(label, tooltip: true)
label_suffix = label.project.name_with_namespace label_suffix = label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>" label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix) render_colored_label(label, label_suffix, tooltip: tooltip)
end end
def suggested_colors def suggested_colors
......
...@@ -209,7 +209,7 @@ module ProjectsHelper ...@@ -209,7 +209,7 @@ module ProjectsHelper
def default_clone_protocol def default_clone_protocol
if !current_user || current_user.require_ssh_key? if !current_user || current_user.require_ssh_key?
"http" gitlab_config.protocol
else else
"ssh" "ssh"
end end
......
...@@ -36,6 +36,14 @@ module Emails ...@@ -36,6 +36,14 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
def issue_moved_email(recipient, issue, new_issue, updated_by_user)
setup_issue_mail(issue.id, recipient.id)
@new_issue = new_issue
@new_project = new_issue.project
mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id))
end
private private
def setup_issue_mail(issue_id, recipient_id) def setup_issue_mail(issue_id, recipient_id)
......
...@@ -209,4 +209,13 @@ module Issuable ...@@ -209,4 +209,13 @@ module Issuable
Taskable.get_updated_tasks(old_content: previous_changes['description'].first, Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
new_content: description) new_content: description)
end end
##
# Method that checks if issuable can be moved to another project.
#
# Should be overridden if issuable can be moved.
#
def can_move?(*)
false
end
end end
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# state :string(255) # state :string(255)
# iid :integer # iid :integer
# updated_by_id :integer # updated_by_id :integer
# moved_to_id :integer
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -31,6 +32,8 @@ class Issue < ActiveRecord::Base ...@@ -31,6 +32,8 @@ class Issue < ActiveRecord::Base
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
belongs_to :project belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
validates :project, presence: true validates :project, presence: true
scope :of_group, scope :of_group,
...@@ -105,9 +108,8 @@ class Issue < ActiveRecord::Base ...@@ -105,9 +108,8 @@ class Issue < ActiveRecord::Base
end end
def related_branches def related_branches
return [] if self.project.empty_repo? project.repository.branch_names.select do |branch|
self.project.repository.branch_names.select do |branch| branch.end_with?("-#{iid}")
branch =~ /\A#{iid}-(?!\d+-stable)/i
end end
end end
...@@ -138,8 +140,20 @@ class Issue < ActiveRecord::Base ...@@ -138,8 +140,20 @@ class Issue < ActiveRecord::Base
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
end end
def moved?
!moved_to.nil?
end
def can_move?(user, to_project = nil)
if to_project
return false unless user.can?(:admin_issue, to_project)
end
!moved? && user.can?(:admin_issue, self.project)
end
def to_branch_name def to_branch_name
"#{iid}-#{title.parameterize}" "#{title.parameterize}-#{iid}"
end end
def can_be_worked_on?(current_user) def can_be_worked_on?(current_user)
......
...@@ -277,8 +277,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -277,8 +277,14 @@ class MergeRequest < ActiveRecord::Base
self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def work_in_progress? def work_in_progress?
!!(title =~ /\A\[?WIP(\]|:| )/i) title =~ WIP_REGEX
end
def wipless_title
self.title.sub(WIP_REGEX, "")
end end
def mergeable? def mergeable?
......
...@@ -876,6 +876,7 @@ class Project < ActiveRecord::Base ...@@ -876,6 +876,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously # Forked import is handled asynchronously
unless forked? unless forked?
if gitlab_shell.add_repository(path_with_namespace) if gitlab_shell.add_repository(path_with_namespace)
repository.after_create
true true
else else
errors.add(:base, 'Failed to create repository via gitlab-shell') errors.add(:base, 'Failed to create repository via gitlab-shell')
......
...@@ -123,23 +123,27 @@ class ProjectWiki ...@@ -123,23 +123,27 @@ class ProjectWiki
end end
def repository def repository
Repository.new(path_with_namespace, @project) @repository ||= Repository.new(path_with_namespace, @project)
end end
def default_branch def default_branch
wiki.class.default_ref wiki.class.default_ref
end end
private
def create_repo! def create_repo!
if init_repo(path_with_namespace) if init_repo(path_with_namespace)
Gollum::Wiki.new(path_to_repo) wiki = Gollum::Wiki.new(path_to_repo)
else else
raise CouldNotCreateWikiError raise CouldNotCreateWikiError
end end
repository.after_create
wiki
end end
private
def init_repo(path_with_namespace) def init_repo(path_with_namespace)
gitlab_shell.add_repository(path_with_namespace) gitlab_shell.add_repository(path_with_namespace)
end end
......
...@@ -42,13 +42,16 @@ class Repository ...@@ -42,13 +42,16 @@ class Repository
end end
def exists? def exists?
return false unless raw_repository return @exists unless @exists.nil?
raw_repository.rugged @exists = cache.fetch(:exists?) do
true begin
raw_repository && raw_repository.rugged ? true : false
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
false false
end end
end
end
def empty? def empty?
return @empty unless @empty.nil? return @empty unless @empty.nil?
...@@ -320,12 +323,23 @@ class Repository ...@@ -320,12 +323,23 @@ class Repository
@avatar = nil @avatar = nil
end end
def expire_exists_cache
cache.expire(:exists?)
@exists = nil
end
# Runs code after a repository has been created.
def after_create
expire_exists_cache
end
# Runs code just before a repository is deleted. # Runs code just before a repository is deleted.
def before_delete def before_delete
expire_cache if exists? expire_cache if exists?
expire_root_ref_cache expire_root_ref_cache
expire_emptiness_caches expire_emptiness_caches
expire_exists_cache
end end
# Runs code just before the HEAD of a repository is changed. # Runs code just before the HEAD of a repository is changed.
...@@ -351,6 +365,7 @@ class Repository ...@@ -351,6 +365,7 @@ class Repository
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_emptiness_caches expire_emptiness_caches
expire_exists_cache
end end
# Runs code after a new commit has been pushed. # Runs code after a new commit has been pushed.
......
...@@ -435,7 +435,7 @@ class User < ActiveRecord::Base ...@@ -435,7 +435,7 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})") Group.where("namespaces.id IN (#{union.to_sql})")
end end
# Returns the groups a user is authorized to access. # Returns projects user is authorized to access.
def authorized_projects def authorized_projects
Project.where("projects.id IN (#{projects_union.to_sql})") Project.where("projects.id IN (#{projects_union.to_sql})")
end end
......
...@@ -120,7 +120,7 @@ class GitPushService < BaseService ...@@ -120,7 +120,7 @@ class GitPushService < BaseService
closed_issues = commit.closes_issues(current_user) closed_issues = commit.closes_issues(current_user)
closed_issues.each do |issue| closed_issues.each do |issue|
if can?(current_user, :update_issue, issue) if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
end end
end end
end end
......
module Issues module Issues
class CloseService < Issues::BaseService class CloseService < Issues::BaseService
def execute(issue, commit = nil) def execute(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue) project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user) todo_service.close_issue(issue, current_user)
...@@ -9,8 +9,8 @@ module Issues ...@@ -9,8 +9,8 @@ module Issues
if project.default_issues_tracker? && issue.close if project.default_issues_tracker? && issue.close
event_service.close_issue(issue, current_user) event_service.close_issue(issue, current_user)
create_note(issue, commit) create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user) todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close') execute_hooks(issue, 'close')
end end
......
...@@ -4,7 +4,7 @@ module Issues ...@@ -4,7 +4,7 @@ module Issues
filter_params filter_params
label_params = params[:label_ids] label_params = params[:label_ids]
issue = project.issues.new(params.except(:label_ids)) issue = project.issues.new(params.except(:label_ids))
issue.author = current_user issue.author = params[:author] || current_user
if issue.save if issue.save
issue.update_attributes(label_ids: label_params) issue.update_attributes(label_ids: label_params)
......
module Issues
class MoveService < Issues::BaseService
class MoveError < StandardError; end
def execute(issue, new_project)
@old_issue = issue
@old_project = @project
@new_project = new_project
unless issue.can_move?(current_user, new_project)
raise MoveError, 'Cannot move issue due to insufficient permissions!'
end
if @project == new_project
raise MoveError, 'Cannot move issue to project it originates from!'
end
# Using transaction because of a high resources footprint
# on rewriting notes (unfolding references)
#
ActiveRecord::Base.transaction do
# New issue tasks
#
@new_issue = create_new_issue
rewrite_notes
add_note_moved_from
# Old issue tasks
#
add_note_moved_to
close_issue
mark_as_moved
end
notify_participants
@new_issue
end
private
def create_new_issue
new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
project: @new_project, author: @old_issue.author,
description: unfold_references(@old_issue.description) }
new_params = @old_issue.serializable_hash.merge(new_params)
CreateService.new(@new_project, @current_user, new_params).execute
end
def rewrite_notes
@old_issue.notes.find_each do |note|
new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue,
note: unfold_references(new_note.note),
created_at: note.created_at }
new_note.update(new_params)
end
end
def close_issue
close_service = CloseService.new(@old_project, @current_user)
close_service.execute(@old_issue, notifications: false, system_note: false)
end
def add_note_moved_from
SystemNoteService.noteable_moved(@new_issue, @new_project,
@old_issue, @current_user,
direction: :from)
end
def add_note_moved_to
SystemNoteService.noteable_moved(@old_issue, @old_project,
@new_issue, @current_user,
direction: :to)
end
def unfold_references(content)
rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
@current_user)
rewriter.rewrite(@new_project)
end
def notify_participants
notification_service.issue_moved(@old_issue, @new_issue, @current_user)
end
def mark_as_moved
@old_issue.update(moved_to: @new_issue)
end
end
end
...@@ -5,6 +5,19 @@ module MergeRequests ...@@ -5,6 +5,19 @@ module MergeRequests
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end end
def create_title_change_note(issuable, old_title)
removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress?
added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress?
if removed_wip
SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
elsif added_wip
SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
else
super
end
end
def hook_data(merge_request, action) def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user) hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
......
...@@ -51,7 +51,7 @@ module MergeRequests ...@@ -51,7 +51,7 @@ module MergeRequests
# be interpreted as the use wants to close that issue on this project # be interpreted as the use wants to close that issue on this project
# Pattern example: 112-fix-mep-mep # Pattern example: 112-fix-mep-mep
# Will lead to appending `Closes #112` to the description # Will lead to appending `Closes #112` to the description
if match = merge_request.source_branch.match(/\A(\d+)-/) if match = merge_request.source_branch.match(/-(\d+)\z/)
iid = match[1] iid = match[1]
closes_issue = "Closes ##{iid}" closes_issue = "Closes ##{iid}"
......
...@@ -22,7 +22,7 @@ module MergeRequests ...@@ -22,7 +22,7 @@ module MergeRequests
closed_issues = merge_request.closes_issues(current_user) closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue| closed_issues.each do |issue|
if can?(current_user, :update_issue, issue) if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
end end
end end
end end
......
...@@ -236,6 +236,16 @@ class NotificationService ...@@ -236,6 +236,16 @@ class NotificationService
end end
end end
def issue_moved(issue, new_issue, current_user)
recipients = build_recipients(issue, issue.project, current_user)
recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
email.deliver_later
email
end
end
protected protected
# Get project users with WATCH notification level # Get project users with WATCH notification level
......
...@@ -144,6 +144,18 @@ class SystemNoteService ...@@ -144,6 +144,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
def self.remove_merge_request_wip(noteable, project, author)
body = 'Unmarked this merge request as a Work In Progress'
create_note(noteable: noteable, project: project, author: author, note: body)
end
def self.add_merge_request_wip(noteable, project, author)
body = 'Marked this merge request as a **Work In Progress**'
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the title of a Noteable is changed # Called when the title of a Noteable is changed
# #
# noteable - Noteable object that responds to `title` # noteable - Noteable object that responds to `title`
...@@ -210,7 +222,7 @@ class SystemNoteService ...@@ -210,7 +222,7 @@ class SystemNoteService
# Called when a branch is created from the 'new branch' button on a issue # Called when a branch is created from the 'new branch' button on a issue
# Example note text: # Example note text:
# #
# "Started branch `201-issue-branch-button`" # "Started branch `issue-branch-button-201`"
def self.new_issue_branch(issue, project, author, branch) def self.new_issue_branch(issue, project, author, branch)
h = Gitlab::Application.routes.url_helpers h = Gitlab::Application.routes.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
...@@ -399,4 +411,26 @@ class SystemNoteService ...@@ -399,4 +411,26 @@ class SystemNoteService
body = "Marked the task **#{new_task.source}** as #{status_label}" body = "Marked the task **#{new_task.source}** as #{status_label}"
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
# Called when noteable has been moved to another project
#
# direction - symbol, :to or :from
# noteable - Noteable object
# noteable_ref - Referenced noteable
# author - User performing the move
#
# Example Note text:
#
# "Moved to some_namespace/project_new#11"
#
# Returns the created Note object
def self.noteable_moved(noteable, project, noteable_ref, author, direction:)
unless [:to, :from].include?(direction)
raise ArgumentError, "Invalid direction `#{direction}`"
end
cross_reference = noteable_ref.to_reference(project)
body = "Moved #{direction} #{cross_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
end end
%li{id: dom_id(label)} %li{id: dom_id(label)}
.label-row .label-row
= render_colored_label(label) = render_colored_label(label, tooltip: false)
= markdown(label.description, pipeline: :single_line) = markdown(label.description, pipeline: :single_line)
.pull-right .pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
......
...@@ -3,31 +3,7 @@ ...@@ -3,31 +3,7 @@
%meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
%title %title
GitLab GitLab
:css = stylesheet_link_tag 'notify'
img {
max-width: 100%;
height: auto;
}
p.details {
font-style:italic;
color:#777
}
.footer p {
font-size:small;
color:#777
}
pre.commit-message {
white-space: pre-wrap;
}
.file-stats a {
text-decoration: none;
}
.file-stats .new-file {
color: #090;
}
.file-stats .deleted-file {
color: #B00;
}
%body %body
%div.content %div.content
= yield = yield
......
%p
Issue was moved to another project.
%p
New issue:
= link_to namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) do
= @new_issue.title
Issue was moved to another project.
New issue location:
<%= namespace_project_issue_url(@new_project.namespace, @new_project, @new_issue) %>
...@@ -20,4 +20,4 @@ ...@@ -20,4 +20,4 @@
- next unless blob - next unless blob
= render 'projects/diffs/file', i: index, project: project, = render 'projects/diffs/file', i: index, project: project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
...@@ -53,6 +53,6 @@ ...@@ -53,6 +53,6 @@
= render "projects/diffs/text_file", diff_file: diff_file, index: i = render "projects/diffs/text_file", diff_file: diff_file, index: i
- elsif blob.image? - elsif blob.image?
- old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs
- else - else
.nothing-here-block No preview for this file type .nothing-here-block No preview for this file type
- diff = diff_file.diff - diff = diff_file.diff
- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path)) - old_commit_id = diff_refs.first.id
- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file - if diff.renamed_file || diff.new_file || diff.deleted_file
.image .image
%span.wrap %span.wrap
...@@ -12,7 +13,7 @@ ...@@ -12,7 +13,7 @@
%div.two-up.view %div.two-up.view
%span.wrap %span.wrap
.frame.deleted .frame.deleted
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))} %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))}
%img{src: old_file_raw_path} %img{src: old_file_raw_path}
%p.image-info.hide %p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}" %span.meta-filesize= "#{number_to_human_size old_file.size}"
......
- type = line.type
%tr.line_holder{id: line_code, class: type}
- case type
- when 'match'
= render "projects/diffs/match_line", {line: line.text,
line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
- when 'nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num{class: type}
- link_text = raw(type == "new" ? "&nbsp;" : line.old_pos)
- if defined?(plain) && plain
= link_text
- else
= link_to link_text, "##{line_code}", id: line_code
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(line_code)
%td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
- link_text = raw(type == "old" ? "&nbsp;" : line.new_pos)
- if defined?(plain) && plain
= link_text
- else
= link_to link_text, "##{line_code}", id: line_code
%td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
...@@ -8,26 +8,9 @@ ...@@ -8,26 +8,9 @@
- last_line = 0 - last_line = 0
- raw_diff_lines = diff_file.diff_lines.to_a - raw_diff_lines = diff_file.diff_lines.to_a
- diff_file.highlighted_diff_lines.each_with_index do |line, index| - diff_file.highlighted_diff_lines.each_with_index do |line, index|
- type = line.type
- last_line = line.new_pos
- line_code = generate_line_code(diff_file.file_path, line) - line_code = generate_line_code(diff_file.file_path, line)
- line_old = line.old_pos - last_line = line.new_pos
%tr.line_holder{ id: line_code, class: "#{type}" } = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code}
- if type == "match"
= render "projects/diffs/match_line", {line: line.text,
line_old: line_old, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
- elsif type == 'nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num{class: type}
= link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(line_code)
%td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
= link_to raw(type == "old" ? "&nbsp;" : line.new_pos), "##{line_code}", id: line_code
%td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
- if @reply_allowed - if @reply_allowed
- comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
......
%h4 %h4
This merge request is currently a Work In Progress This merge request is currently a Work In Progress
%p - if can?(current_user, :update_merge_request, @merge_request)
When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged. %p
When this merge request is ready,
= link_to remove_wip_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), method: :post do
remove the
%code WIP:
prefix from the title
to allow it to be merged.
...@@ -5,6 +5,6 @@ ...@@ -5,6 +5,6 @@
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.note-form-actions .note-form-actions.clearfix
= f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
= link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel' = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
...@@ -8,11 +8,9 @@ ...@@ -8,11 +8,9 @@
= icon('angle-down') = icon('angle-down')
%ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown
%li %li
%a#ssh-selector{href: @project.ssh_url_to_repo} = ssh_clone_button(project)
SSH
%li %li
%a#http-selector{href: @project.http_url_to_repo} = http_clone_button(project)
HTTPS
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn .input-group-btn
......
%span.label-row %span.label-row
= link_to_label(label) = link_to_label(label, tooltip: false)
%span.prepend-left-10 %span.prepend-left-10
= markdown(label.description, pipeline: :single_line) = markdown(label.description, pipeline: :single_line)
...@@ -13,12 +13,21 @@ ...@@ -13,12 +13,21 @@
- if issuable.is_a?(MergeRequest) - if issuable.is_a?(MergeRequest)
%p.help-block %p.help-block
- if issuable.work_in_progress? .js-wip-explanation
Remove the <code>WIP</code> prefix from the title to allow this %a.js-toggle-wip{href: ""}
<strong>Work In Progress</strong> merge request to be merged when it's ready. Remove the
- else %code WIP:
Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a prefix from the title
<strong>Work In Progress</strong> merge request from being merged before it's ready. to allow this
%strong Work In Progress
merge request to be merged when it's ready.
.js-no-wip-explanation
%a.js-toggle-wip{href: ""}
Start the title with
%code WIP:
to prevent a
%strong Work In Progress
merge request from being merged before it's ready.
.form-group.detail-page-description .form-group.detail-page-description
= f.label :description, 'Description', class: 'control-label' = f.label :description, 'Description', class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -76,6 +85,19 @@ ...@@ -76,6 +85,19 @@
- if can? current_user, :admin_label, issuable.project - if can? current_user, :admin_label, issuable.project
= link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
- if issuable.can_move?(current_user)
%hr
.form-group
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
- projects = project_options(issuable, current_user, ability: :admin_issue)
= select_tag(:move_to_project_id, projects, include_blank: true,
class: 'select2', data: { placeholder: 'Select project' })
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
- if issuable.is_a?(MergeRequest) - if issuable.is_a?(MergeRequest)
%hr %hr
- if @merge_request.new_record? - if @merge_request.new_record?
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%li %li
%span.label-row %span.label-row
= link_to milestones_label_path(options) do = link_to milestones_label_path(options) do
- render_colored_label(label) - render_colored_label(label, tooltip: false)
%span.prepend-left-10 %span.prepend-left-10
= markdown(label.description, pipeline: :single_line) = markdown(label.description, pipeline: :single_line)
......
...@@ -20,14 +20,15 @@ class RepositoryForkWorker ...@@ -20,14 +20,15 @@ class RepositoryForkWorker
return return
end end
project.repository.after_import
unless project.valid_repo? unless project.valid_repo?
logger.error("Project #{id} had an invalid repository after fork") logger.error("Project #{project_id} had an invalid repository after fork")
project.update(import_error: "The forked repository is invalid.") project.update(import_error: "The forked repository is invalid.")
project.import_fail project.import_fail
return return
end end
project.repository.after_import
project.import_finish project.import_finish
end end
end end
...@@ -49,6 +49,7 @@ module Gitlab ...@@ -49,6 +49,7 @@ module Gitlab
config.assets.paths << Gemojione.index.images_path config.assets.paths << Gemojione.index.images_path
config.assets.precompile << "*.png" config.assets.precompile << "*.png"
config.assets.precompile << "print.css" config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
# Version of your assets, change this if you want to expire all your assets # Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0' config.assets.version = '1.0'
......
# See https://github.com/fphilipe/premailer-rails#configuration
Premailer::Rails.config.merge!(
generate_text_part: false,
preserve_styles: true,
remove_comments: true,
remove_ids: true
)
...@@ -623,6 +623,7 @@ Rails.application.routes.draw do ...@@ -623,6 +623,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds post :cancel_merge_when_build_succeeds
get :ci_status get :ci_status
post :toggle_subscription post :toggle_subscription
post :remove_wip
end end
collection do collection do
......
class AddMovedToToIssue < ActiveRecord::Migration
def change
add_reference :issues, :moved_to, references: :issues
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160316204731) do ActiveRecord::Schema.define(version: 20160317092222) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -416,6 +416,7 @@ ActiveRecord::Schema.define(version: 20160316204731) do ...@@ -416,6 +416,7 @@ ActiveRecord::Schema.define(version: 20160316204731) do
t.string "state" t.string "state"
t.integer "iid" t.integer "iid"
t.integer "updated_by_id" t.integer "updated_by_id"
t.integer "moved_to_id"
t.boolean "confidential", default: false t.boolean "confidential", default: false
end end
......
...@@ -279,6 +279,8 @@ job_name: ...@@ -279,6 +279,8 @@ job_name:
| Keyword | Required | Description | | Keyword | Required | Description |
|---------------|----------|-------------| |---------------|----------|-------------|
| script | yes | Defines a shell script which is executed by runner | | script | yes | Defines a shell script which is executed by runner |
| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| stage | no | Defines a build stage (default: `test`) | | stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` | | type | no | Alias for `stage` |
| only | no | Defines a list of git refs for which build is created | | only | no | Defines a list of git refs for which build is created |
......
...@@ -348,7 +348,7 @@ GitLab Shell is an SSH access and repository management software developed speci ...@@ -348,7 +348,7 @@ GitLab Shell is an SSH access and repository management software developed speci
cd /home/git cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse cd gitlab-workhorse
sudo -u git -H git checkout 0.6.5 sudo -u git -H git checkout v0.7.1
sudo -u git -H make sudo -u git -H make
### Initialize Database and Activate Advanced Features ### Initialize Database and Activate Advanced Features
......
...@@ -58,7 +58,7 @@ GitLab 8.1. ...@@ -58,7 +58,7 @@ GitLab 8.1.
```bash ```bash
cd /home/git/gitlab-workhorse cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all sudo -u git -H git fetch --all
sudo -u git -H git checkout 0.6.5 sudo -u git -H git checkout v0.7.1
sudo -u git -H make sudo -u git -H make
``` ```
......
...@@ -160,6 +160,7 @@ Feature: Project Issues ...@@ -160,6 +160,7 @@ Feature: Project Issues
Scenario: Issues on empty project Scenario: Issues on empty project
Given empty project "Empty Project" Given empty project "Empty Project"
And I have an ssh key
When I visit empty project page When I visit empty project page
And I see empty project details with ssh clone info And I see empty project details with ssh clone info
When I visit empty project's issues page When I visit empty project's issues page
......
...@@ -27,7 +27,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps ...@@ -27,7 +27,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
step 'I click on HTTP' do step 'I click on HTTP' do
find('#clone-dropdown').click find('#clone-dropdown').click
find('#http-selector').click find('.http-selector').click
end end
step 'Remote url should update to http link' do step 'Remote url should update to http link' do
...@@ -36,7 +36,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps ...@@ -36,7 +36,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
step 'If I click on SSH' do step 'If I click on SSH' do
find('#clone-dropdown').click find('#clone-dropdown').click
find('#ssh-selector').click find('.ssh-selector').click
end end
step 'Remote url should update to ssh link' do step 'Remote url should update to ssh link' do
......
...@@ -5,6 +5,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -5,6 +5,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
include SharedNote include SharedNote
include SharedPaths include SharedPaths
include SharedMarkdown include SharedMarkdown
include SharedUser
step 'I should see "Release 0.4" in issues' do step 'I should see "Release 0.4" in issues' do
expect(page).to have_content "Release 0.4" expect(page).to have_content "Release 0.4"
...@@ -240,7 +241,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -240,7 +241,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'empty project "Empty Project"' do step 'empty project "Empty Project"' do
create :empty_project, name: 'Empty Project', namespace: @user.namespace create :project_empty_repo, name: 'Empty Project', namespace: @user.namespace
end end
When 'I visit empty project page' do When 'I visit empty project page' do
......
...@@ -47,6 +47,7 @@ module Banzai ...@@ -47,6 +47,7 @@ module Banzai
# Returns a String # Returns a String
def data_attribute(attributes = {}) def data_attribute(attributes = {})
attributes[:reference_filter] = self.class.name.demodulize attributes[:reference_filter] = self.class.name.demodulize
attributes.delete(:original) if context[:no_original_data]
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end end
......
module Gitlab
module Gfm
##
# Class that unfolds local references in text.
#
# The initializer takes text in Markdown and project this text is valid
# in context of.
#
# `unfold` method tries to find all local references and unfold each of
# those local references to cross reference format, assuming that the
# argument passed to this method is a project that references will be
# viewed from (see `Referable#to_reference method).
#
# Examples:
#
# 'Hello, this issue is related to #123 and
# other issues labeled with ~"label"', will be converted to:
#
# 'Hello, this issue is related to gitlab-org/gitlab-ce#123 and
# other issue labeled with gitlab-org/gitlab-ce~"label"'.
#
# It does respect markdown lexical rules, so text in code block will not be
# replaced, see another example:
#
# 'Merge request for issue #1234, see also link:
# http://gitlab.com/some/link/#1234, and code `puts #1234`' =>
#
# 'Merge request for issue gitlab-org/gitlab-ce#1234, se also link:
# http://gitlab.com/some/link/#1234, and code `puts #1234`'
#
class ReferenceRewriter
def initialize(text, source_project, current_user)
@text = text
@source_project = source_project
@current_user = current_user
@original_html = markdown(text)
end
def rewrite(target_project)
pattern = Gitlab::ReferenceExtractor.references_pattern
@text.gsub(pattern) do |reference|
unfold_reference(reference, Regexp.last_match, target_project)
end
end
private
def unfold_reference(reference, match, target_project)
before = @text[0...match.begin(0)]
after = @text[match.end(0)..-1]
referable = find_referable(reference)
return reference unless referable
cross_reference = referable.to_reference(target_project)
return reference if reference == cross_reference
new_text = before + cross_reference + after
substitution_valid?(new_text) ? cross_reference : reference
end
def find_referable(reference)
extractor = Gitlab::ReferenceExtractor.new(@source_project,
@current_user)
extractor.analyze(reference)
extractor.all.first
end
def substitution_valid?(substituted)
@original_html == markdown(substituted)
end
def markdown(text)
Banzai.render(text, project: @source_project, no_original_data: true)
end
end
end
end
module Gitlab module Gitlab
# Extract possible GFM references from an arbitrary String for further processing. # Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor class ReferenceExtractor < Banzai::ReferenceExtractor
REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
attr_accessor :project, :current_user, :author attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil, author = nil) def initialize(project, current_user = nil, author = nil)
...@@ -17,7 +18,7 @@ module Gitlab ...@@ -17,7 +18,7 @@ module Gitlab
super(text, context.merge(project: project)) super(text, context.merge(project: project))
end end
%i(user label milestone merge_request snippet commit commit_range).each do |type| REFERABLES.each do |type|
define_method("#{type}s") do define_method("#{type}s") do
@references[type] ||= references(type, reference_context) @references[type] ||= references(type, reference_context)
end end
...@@ -31,6 +32,21 @@ module Gitlab ...@@ -31,6 +32,21 @@ module Gitlab
end end
end end
def all
REFERABLES.each { |referable| send(referable.to_s.pluralize) }
@references.values.flatten
end
def self.references_pattern
return @pattern if @pattern
patterns = REFERABLES.map do |ref|
ref.to_s.classify.constantize.try(:reference_pattern)
end
@pattern = Regexp.union(patterns.compact)
end
private private
def reference_context def reference_context
......
...@@ -2,7 +2,7 @@ require('spec_helper') ...@@ -2,7 +2,7 @@ require('spec_helper')
describe Projects::IssuesController do describe Projects::IssuesController do
describe "GET #index" do describe "GET #index" do
let(:project) { create(:project) } let(:project) { create(:project_empty_repo) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
...@@ -41,7 +41,7 @@ describe Projects::IssuesController do ...@@ -41,7 +41,7 @@ describe Projects::IssuesController do
end end
describe 'Confidential Issues' do describe 'Confidential Issues' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:project_empty_repo, :public) }
let(:assignee) { create(:assignee) } let(:assignee) { create(:assignee) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
......
require 'rails_helper'
feature 'issue move to another project' do
let(:user) { create(:user) }
let(:old_project) { create(:project) }
let(:text) { 'Some issue description' }
let(:issue) do
create(:issue, description: text, project: old_project, author: user)
end
background { login_as(user) }
context 'user does not have permission to move issue' do
background do
old_project.team << [user, :guest]
edit_issue(issue)
end
scenario 'moving issue to another project not allowed' do
expect(page).to have_no_select('move_to_project_id')
end
end
context 'user has permission to move issue' do
let!(:mr) { create(:merge_request, source_project: old_project) }
let(:new_project) { create(:project) }
let(:text) { 'Text with !1' }
let(:cross_reference) { old_project.to_reference }
background do
old_project.team << [user, :reporter]
new_project.team << [user, :reporter]
edit_issue(issue)
end
scenario 'moving issue to another project' do
select(new_project.name_with_namespace, from: 'move_to_project_id')
click_button('Save changes')
expect(current_url).to include project_path(new_project)
page.within('.issue') do
expect(page).to have_content("Text with #{cross_reference}!1")
expect(page).to have_content("Moved from #{cross_reference}#1")
expect(page).to have_content(issue.title)
end
end
context 'projects user does not have permission to move issue to exist' do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
options = [ '', 'No project', new_project.name_with_namespace ]
expect(page).to have_select('move_to_project_id', options: options)
end
end
context 'issue has been already moved' do
let(:new_issue) { create(:issue, project: new_project) }
let(:issue) do
create(:issue, project: old_project, author: user, moved_to: new_issue)
end
scenario 'user wants to move issue that has already been moved' do
expect(page).to have_no_select('move_to_project_id')
end
end
end
def edit_issue(issue)
visit issue_path(issue)
page.within('.issuable-header') { click_link 'Edit' }
end
def issue_path(issue)
namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
def project_path(project)
namespace_project_path(new_project.namespace, new_project)
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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