Commit 39c2677e authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into after-script

parents 63bd1f92 2b8fc138
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.7.0 (unreleased) v 8.7.0 (unreleased)
- Method instrumentation now uses Module#prepend instead of aliasing methods
- Repository.clean_old_archives is now instrumented
- The Projects::HousekeepingService class has extra instrumentation - The Projects::HousekeepingService class has extra instrumentation
- All service classes (those residing in app/services) are now instrumented - All service classes (those residing in app/services) are now instrumented
- Developers can now add custom tags to transactions - Developers can now add custom tags to transactions
...@@ -57,6 +59,7 @@ v 8.7.0 (unreleased) ...@@ -57,6 +59,7 @@ v 8.7.0 (unreleased)
- Decouple membership and notifications - Decouple membership and notifications
- Fix creation of merge requests for orphaned branches (Stan Hu) - Fix creation of merge requests for orphaned branches (Stan Hu)
- API: Ability to retrieve a single tag (Robert Schilling) - API: Ability to retrieve a single tag (Robert Schilling)
- While signing up, don't persist the user password across form redisplays
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
- Fix admin/projects when using visibility levels on search (PotHix) - Fix admin/projects when using visibility levels on search (PotHix)
...@@ -73,13 +76,19 @@ v 8.7.0 (unreleased) ...@@ -73,13 +76,19 @@ v 8.7.0 (unreleased)
- Delete tags using Rugged for performance reasons (Robert Schilling) - Delete tags using Rugged for performance reasons (Robert Schilling)
- Diffs load at the correct point when linking from from number - Diffs load at the correct point when linking from from number
- Selected diff rows highlight - Selected diff rows highlight
- Fix emoji catgories in the emoji picker - Fix emoji categories in the emoji picker
- Add encrypted credentials for imported projects and migrate old ones - Add encrypted credentials for imported projects and migrate old ones
- Author and participants are displayed first on users autocompletion
- Show number sign on external issue reference text (Florent Baldino)
v 8.6.6 v 8.6.6
- Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413 - Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654 - Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690 - Fix revoking of authorized OAuth applications (Connor Shea). !3690
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- Project switcher uses new dropdown styling
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
v 8.6.5 v 8.6.5
- Fix importing from GitHub Enterprise. !3529 - Fix importing from GitHub Enterprise. !3529
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
window.GitLab ?= {} window.GitLab ?= {}
GitLab.GfmAutoComplete = GitLab.GfmAutoComplete =
dataLoading: false
dataSource: '' dataSource: ''
# Emoji # Emoji
...@@ -17,17 +19,41 @@ GitLab.GfmAutoComplete = ...@@ -17,17 +19,41 @@ GitLab.GfmAutoComplete =
template: '<li><small>${id}</small> ${title}</li>' template: '<li><small>${id}</small> ${title}</li>'
# Add GFM auto-completion to all input fields, that accept GFM input. # Add GFM auto-completion to all input fields, that accept GFM input.
setup: -> setup: (wrap) ->
input = $('.js-gfm-input') @input = $('.js-gfm-input')
# destroy previous instances
@destroyAtWho()
# set up instances
@setupAtWho()
if @dataSource
if !@dataLoading
@dataLoading = true
# We should wait until initializations are done
# and only trigger the last .setup since
# The previous .dataSource belongs to the previous issuable
# and the last one will have the **proper** .dataSource property
# TODO: Make this a singleton and turn off events when moving to another page
setTimeout( =>
fetch = @fetchData(@dataSource)
fetch.done (data) =>
@dataLoading = false
@loadData(data)
, 1000)
setupAtWho: ->
# Emoji # Emoji
input.atwho @input.atwho
at: ':' at: ':'
displayTpl: @Emoji.template displayTpl: @Emoji.template
insertTpl: ':${name}:' insertTpl: ':${name}:'
# Team Members # Team Members
input.atwho @input.atwho
at: '@' at: '@'
displayTpl: @Members.template displayTpl: @Members.template
insertTpl: '${atwho-at}${username}' insertTpl: '${atwho-at}${username}'
...@@ -42,7 +68,7 @@ GitLab.GfmAutoComplete = ...@@ -42,7 +68,7 @@ GitLab.GfmAutoComplete =
title: sanitize(title) title: sanitize(title)
search: sanitize("#{m.username} #{m.name}") search: sanitize("#{m.username} #{m.name}")
input.atwho @input.atwho
at: '#' at: '#'
alias: 'issues' alias: 'issues'
searchKey: 'search' searchKey: 'search'
...@@ -55,7 +81,7 @@ GitLab.GfmAutoComplete = ...@@ -55,7 +81,7 @@ GitLab.GfmAutoComplete =
title: sanitize(i.title) title: sanitize(i.title)
search: "#{i.iid} #{i.title}" search: "#{i.iid} #{i.title}"
input.atwho @input.atwho
at: '!' at: '!'
alias: 'mergerequests' alias: 'mergerequests'
searchKey: 'search' searchKey: 'search'
...@@ -68,13 +94,18 @@ GitLab.GfmAutoComplete = ...@@ -68,13 +94,18 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.iid} #{m.title}" search: "#{m.iid} #{m.title}"
if @dataSource destroyAtWho: ->
$.getJSON(@dataSource).done (data) -> @input.atwho('destroy')
# load members
input.atwho 'load', '@', data.members fetchData: (dataSource) ->
# load issues $.getJSON(dataSource)
input.atwho 'load', 'issues', data.issues
# load merge requests loadData: (data) ->
input.atwho 'load', 'mergerequests', data.mergerequests # load members
# load emojis @input.atwho 'load', '@', data.members
input.atwho 'load', ':', data.emojis # load issues
@input.atwho 'load', 'issues', data.issues
# load merge requests
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
...@@ -75,6 +75,9 @@ class @Notes ...@@ -75,6 +75,9 @@ class @Notes
# when issue status changes, we need to refresh data # when issue status changes, we need to refresh data
$(document).on "issuable:change", @refresh $(document).on "issuable:change", @refresh
# when a key is clicked on the notes
$(document).on "keydown", ".js-note-text", @keydownNoteText
cleanBinding: -> cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form" $(document).off "ajax:success", ".js-discussion-note-form"
...@@ -92,10 +95,19 @@ class @Notes ...@@ -92,10 +95,19 @@ class @Notes
$(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard" $(document).off "click", ".js-note-discard"
$(document).off "keydown", ".js-note-text"
$('.note .js-task-list-container').taskList('disable') $('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container' $(document).off 'tasklist:changed', '.note .js-task-list-container'
keydownNoteText: (e) ->
$this = $(this)
if $this.val() is '' and e.which is 38 #aka the up key
myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
if myLastNote.length
myLastNoteEditBtn = myLastNote.find('.js-note-edit')
myLastNoteEditBtn.trigger('click', [true, myLastNote])
initRefresh: -> initRefresh: ->
clearInterval(Notes.interval) clearInterval(Notes.interval)
Notes.interval = setInterval => Notes.interval = setInterval =>
...@@ -343,7 +355,7 @@ class @Notes ...@@ -343,7 +355,7 @@ class @Notes
Adds a hidden div with the original content of the note to fill the edit note form with Adds a hidden div with the original content of the note to fill the edit note form with
if the user cancels if the user cancels
### ###
showEditForm: (e) -> showEditForm: (e, scrollTo, myLastNote) ->
e.preventDefault() e.preventDefault()
note = $(this).closest(".note") note = $(this).closest(".note")
note.addClass "is-editting" note.addClass "is-editting"
...@@ -354,9 +366,27 @@ class @Notes ...@@ -354,9 +366,27 @@ class @Notes
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
new GLForm form done = ($noteText) ->
# Neat little trick to put the cursor at the end
noteTextVal = $noteText.val()
$noteText.val('').val(noteTextVal);
form.find(".js-note-text").focus() new GLForm form
if scrollTo? and myLastNote?
# scroll to the bottom
# so the open of the last element doesn't make a jump
$('html, body').scrollTop($(document).height());
$('html, body').animate({
scrollTop: myLastNote.offset().top - 150
}, 500, ->
$noteText = form.find(".js-note-text")
$noteText.focus()
done($noteText)
);
else
$noteText = form.find('.js-note-text')
$noteText.focus()
done($noteText)
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
*/ */
.status-box { .status-box {
/* Extra small devices (phones, less than 768px) */ /* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */ /* No media query since this is the default in Bootstrap */
padding: 5px 11px; padding: 5px 11px;
......
...@@ -70,13 +70,6 @@ ...@@ -70,13 +70,6 @@
display: none; display: none;
} }
.issue-details {
.creator,
.page-title .btn-close {
display: none;
}
}
%ul.notes .note-role, .note-actions { %ul.notes .note-role, .note-actions {
display: none; display: none;
} }
......
.detail-page-header { .detail-page-header {
padding: 11px 0; padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
color: #5c5d5e; color: #5c5d5e;
font-size: 16px; font-size: 16px;
...@@ -16,11 +16,6 @@ ...@@ -16,11 +16,6 @@
.issue_created_ago, .author_link { .issue_created_ago, .author_link {
white-space: nowrap; white-space: nowrap;
} }
.issue-meta {
display: inline-block;
line-height: 20px;
}
} }
.detail-page-description { .detail-page-description {
......
...@@ -273,10 +273,6 @@ ...@@ -273,10 +273,6 @@
} }
} }
.btn-default.gutter-toggle {
margin-top: 4px;
}
.detail-page-description { .detail-page-description {
small { small {
color: $gray-darkest; color: $gray-darkest;
...@@ -322,3 +318,50 @@ ...@@ -322,3 +318,50 @@
padding-top: 7px; padding-top: 7px;
} }
} }
.issuable-status-box {
float: none;
display: inline-block;
margin-top: 0;
@media (max-width: $screen-xs-max) {
position: absolute;
top: 0;
left: 0;
}
}
.issuable-header {
position: relative;
padding-left: 45px;
padding-right: 45px;
line-height: 35px;
@media (min-width: $screen-sm-min) {
float: left;
padding-left: 0;
padding-right: 0;
}
}
.issuable-actions {
padding-top: 10px;
@media (min-width: $screen-sm-min) {
float: right;
padding-top: 0;
}
}
.issuable-gutter-toggle {
@media (max-width: $screen-sm-max) {
position: absolute;
top: 0;
right: 0;
}
}
.issuable-meta {
display: inline-block;
line-height: 18px;
}
...@@ -86,41 +86,9 @@ form.edit-issue { ...@@ -86,41 +86,9 @@ form.edit-issue {
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.issue-btn-group { .issue-btn-group {
width: 100%; width: 100%;
margin-top: 5px;
.btn-group {
width: 100%;
ul {
width: 100%;
text-align: center;
}
}
.btn { .btn {
width: 100%; width: 100%;
&:first-child:not(:last-child) {
}
&:not(:first-child):not(:last-child) {
margin-top: 10px;
}
&:last-child:not(:first-child) {
margin-top: 10px;
}
}
}
.issue {
&:hover .issue-actions {
display: none !important;
}
.issue-updated-at {
display: none;
} }
} }
} }
...@@ -133,11 +101,3 @@ form.edit-issue { ...@@ -133,11 +101,3 @@ form.edit-issue {
color: $gl-text-color; color: $gl-text-color;
margin-left: 52px; margin-left: 52px;
} }
.editor-details {
display: block;
@media (min-width: $screen-sm-min) {
display: inline-block;
}
}
...@@ -51,6 +51,7 @@ class HelpController < ApplicationController ...@@ -51,6 +51,7 @@ class HelpController < ApplicationController
end end
def ui def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end end
private private
......
...@@ -55,6 +55,15 @@ module IssuablesHelper ...@@ -55,6 +55,15 @@ module IssuablesHelper
h(milestone_title.presence || default_label) h(milestone_title.presence || default_label)
end end
def issuable_meta(issuable, project, text)
output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs")
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -37,4 +37,10 @@ class ExternalIssue ...@@ -37,4 +37,10 @@ class ExternalIssue
def to_reference(_from_project = nil) def to_reference(_from_project = nil)
id id
end end
def reference_link_text(from_project = nil)
return "##{id}" if /^\d+$/.match(id)
id
end
end end
...@@ -12,11 +12,13 @@ class Repository ...@@ -12,11 +12,13 @@ class Repository
attr_accessor :path_with_namespace, :project attr_accessor :path_with_namespace, :project
def self.clean_old_archives def self.clean_old_archives
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path Gitlab::Metrics.measure(:clean_old_archives) do
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
return unless File.directory?(repository_downloads_path) return unless File.directory?(repository_downloads_path)
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
end end
def initialize(path_with_namespace, project) def initialize(path_with_namespace, project)
......
module Projects module Projects
class ParticipantsService < BaseService class ParticipantsService < BaseService
def execute(note_type, note_id) def execute(noteable_type, noteable_id)
participating = @noteable_type = noteable_type
if note_type && note_id @noteable_id = noteable_id
participants_in(note_type, note_id)
else
[]
end
project_members = sorted(project.team.members) project_members = sorted(project.team.members)
participants = all_members + groups + project_members + participating participants = target_owner + participants_in_target + all_members + groups + project_members
participants.uniq participants.uniq
end end
def participants_in(type, id) def target
target = @target ||=
case type case @noteable_type
when "Issue" when "Issue"
project.issues.find_by_iid(id) project.issues.find_by_iid(@noteable_id)
when "MergeRequest" when "MergeRequest"
project.merge_requests.find_by_iid(id) project.merge_requests.find_by_iid(@noteable_id)
when "Commit" when "Commit"
project.commit(id) project.commit(@noteable_id)
else
nil
end end
end
def target_owner
return [] unless target && target.author.present?
[{
name: target.author.name,
username: target.author.username
}]
end
def participants_in_target
return [] unless target return [] unless target
users = target.participants(current_user) users = target.participants(current_user)
...@@ -30,13 +39,13 @@ module Projects ...@@ -30,13 +39,13 @@ module Projects
end end
def sorted(users) def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user| users.uniq.to_a.compact.sort_by(&:username).map do |user|
{ username: user.username, name: user.name } { username: user.username, name: user.name }
end end
end end
def groups def groups
current_user.authorized_groups.sort_by(&:path).map do |group| current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count count = group.users.count
{ username: group.path, name: group.name, count: count } { username: group.path, name: group.name, count: count }
end end
......
...@@ -6,18 +6,17 @@ ...@@ -6,18 +6,17 @@
.login-heading .login-heading
%h3 Create an account %h3 Create an account
.login-body .login-body
- user = params[:user].present? ? params[:user] : {}
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
%div %div
= f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true = f.text_field :name, class: "form-control top", placeholder: "Name", required: true
%div %div
= f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
%div %div
= f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength .form-group.append-bottom-20#password-strength
= f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
%div %div
- if current_application_settings.recaptcha_enabled - if current_application_settings.recaptcha_enabled
= recaptcha_tags = recaptcha_tags
......
...@@ -345,11 +345,11 @@ ...@@ -345,11 +345,11 @@
%ul %ul
%li %li
%a.dropdown-menu-user-link.is-active{href: "#"} %a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30) = link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name %strong.dropdown-menu-user-full-name
= current_user.name = @user.name
.dropdown-menu-user-username .dropdown-menu-user-username
= current_user.to_reference = @user.to_reference
.example .example
%div %div
...@@ -372,11 +372,11 @@ ...@@ -372,11 +372,11 @@
%ul %ul
%li %li
%a.dropdown-menu-user-link.is-active{href: "#"} %a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30) = link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name %strong.dropdown-menu-user-full-name
= current_user.name = @user.name
.dropdown-menu-user-username .dropdown-menu-user-username
= current_user.to_reference = @user.to_reference
.dropdown-page-two .dropdown-page-two
.dropdown-title .dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
......
...@@ -52,6 +52,9 @@ ...@@ -52,6 +52,9 @@
%li %li
phpunit --coverage-text --colors=never (PHP) - phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\% %code ^\s*Lines:\s*\d+.\d+\%
%li
gcovr (C/C++) -
%code ^TOTAL.*\s+(\d+\%)$
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
......
.md-area .md-area
.md-header .md-header
%ul.nav-links %ul.nav-links.clearfix
%li.active %li.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write Write
......
- page_title "#{@issue.title} (##{@issue.iid})", "Issues" - page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_description @issue.description - page_description @issue.description
- page_card_attributes @issue.card_attributes - page_card_attributes @issue.card_attributes
- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
= render "header_title" .clearfix.detail-page-header
.issuable-header
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
= icon('check', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
Closed
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
= icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs Open
.issue %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
.detail-page-header.issuable-header
.pull-left
.status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"}
%span.hidden-xs
Closed
%span.hidden-sm.hidden-md.hidden-lg
= icon('check')
.status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"}
%span.hidden-xs
Open
%span.hidden-sm.hidden-md.hidden-lg
= icon('circle-o')
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left') = icon('angle-double-left')
.issue-meta .issuable-meta
= confidential_icon(@issue) = confidential_icon(@issue)
%strong.identifier = issuable_meta(@issue, @project, "Issue")
Issue ##{@issue.iid}
%span.creator
opened
.editor-details
.editor-details
= time_ago_with_tooltip(@issue.created_at)
by
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
.pull-right.issue-btn-group - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
- if can?(current_user, :create_issue, @project) .issuable-actions
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do .clearfix.issue-btn-group.dropdown
= icon('plus') %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
New issue %span.caret
- if can?(current_user, :update_issue, @issue) Options
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' .dropdown-menu.dropdown-menu-align-right.hidden-lg
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %ul
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do - if can?(current_user, :create_issue, @project)
= icon('pencil-square-o') %li
Edit = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- if can?(current_user, :update_issue, @issue)
%li
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
New issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
%h2.title %h2.title
= markdown escape_once(@issue.title), pipeline: :single_line = markdown escape_once(@issue.title), pipeline: :single_line
%div - if @issue.description.present?
- if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''} .wiki
.wiki = preserve do
= preserve do = markdown(@issue.description, cache_key: [@issue, "description"])
= markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field
%textarea.hidden.js-task-list-field = @issue.description
= @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
#merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)} #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript. // This element is filled in using JavaScript.
#related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)} #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript. // This element is filled in using JavaScript.
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch' = render 'new_branch'
= render 'votes/votes_block', votable: @issue = render 'votes/votes_block', votable: @issue
.row %section.issuable-discussion
%section.col-md-12 = render 'projects/issues/discussion'
.issuable-discussion
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue = render 'shared/issuable/sidebar', issuable: @issue
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description - page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes - page_card_attributes @merge_request.card_attributes
- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
= render "header_title"
- if params[:view] == 'parallel' - if params[:view] == 'parallel'
- fluid_layout true - fluid_layout true
......
.detail-page-header .clearfix.detail-page-header
.status-box{ class: status_box_class(@merge_request) } .issuable-header
%span.hidden-xs .issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= @merge_request.state_human_name = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
%span.hidden-sm.hidden-md.hidden-lg
= icon(@merge_request.state_icon_name)
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.issue-meta
%strong.identifier
%span.hidden-sm.hidden-md.hidden-lg
MR
%span.hidden-xs %span.hidden-xs
Merge Request = @merge_request.state_human_name
!#{@merge_request.iid}
%span.creator
opened
.editor-details
= time_ago_with_tooltip(@merge_request.created_at)
by
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
.issue-btn-group.pull-right %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
- if can?(current_user, :update_merge_request, @merge_request) = icon('angle-double-left')
- if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request' .issuable-meta
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do = issuable_meta(@merge_request, @project, "Merge Request")
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
%li{ class: issue_button_visibility(@merge_request, true) }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: issue_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request'
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do
= icon('pencil-square-o') = icon('pencil-square-o')
Edit Edit
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] } - note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
.timeline-entry-inner .timeline-entry-inner
.timeline-icon .timeline-icon
%a{href: user_path(note.author)} %a{href: user_path(note.author)}
...@@ -15,16 +16,16 @@ ...@@ -15,16 +16,16 @@
- if access - if access
%span.note-role %span.note-role
= access = access
- if note_editable?(note) - if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o') = icon('trash-o')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"]) = markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note) - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad js-gfm-input', required: true class: 'form-control pad', required: true
- if issuable.is_a?(MergeRequest) - if issuable.is_a?(MergeRequest)
%p.help-block %p.help-block
......
...@@ -13,7 +13,7 @@ GitLab offers a [continuous integration][ci] service. If you ...@@ -13,7 +13,7 @@ GitLab offers a [continuous integration][ci] service. If you
and configure your GitLab project to use a [Runner], then each merge request or and configure your GitLab project to use a [Runner], then each merge request or
push triggers a build. push triggers a build.
The `.gitlab-ci.yml` file tells the GitLab runner what do to. By default it The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it
runs three [stages]: `build`, `test`, and `deploy`. runs three [stages]: `build`, `test`, and `deploy`.
If everything runs OK (no non-zero return values), you'll get a nice green If everything runs OK (no non-zero return values), you'll get a nice green
......
...@@ -2,7 +2,7 @@ module SharedIssuable ...@@ -2,7 +2,7 @@ module SharedIssuable
include Spinach::DSL include Spinach::DSL
def edit_issuable def edit_issuable
find(:css, '.issuable-edit').click find('.issuable-edit', visible: true).click
end end
step 'project "Community" has "Community issue" open issue' do step 'project "Community" has "Community issue" open issue' do
......
...@@ -11,6 +11,8 @@ module Gitlab ...@@ -11,6 +11,8 @@ module Gitlab
module Instrumentation module Instrumentation
SERIES = 'method_calls' SERIES = 'method_calls'
PROXY_IVAR = :@__gitlab_instrumentation_proxy
def self.configure def self.configure
yield self yield self
end end
...@@ -91,6 +93,18 @@ module Gitlab ...@@ -91,6 +93,18 @@ module Gitlab
end end
end end
# Returns true if a module is instrumented.
#
# mod - The module to check
def self.instrumented?(mod)
mod.instance_variable_defined?(PROXY_IVAR)
end
# Returns the proxy module (if any) of `mod`.
def self.proxy_module(mod)
mod.instance_variable_get(PROXY_IVAR)
end
# Instruments a method. # Instruments a method.
# #
# type - The type (:class or :instance) of method to instrument. # type - The type (:class or :instance) of method to instrument.
...@@ -99,9 +113,8 @@ module Gitlab ...@@ -99,9 +113,8 @@ module Gitlab
def self.instrument(type, mod, name) def self.instrument(type, mod, name)
return unless Metrics.enabled? return unless Metrics.enabled?
name = name.to_sym name = name.to_sym
alias_name = :"_original_#{name}" target = type == :instance ? mod : mod.singleton_class
target = type == :instance ? mod : mod.singleton_class
if type == :instance if type == :instance
target = mod target = mod
...@@ -113,6 +126,12 @@ module Gitlab ...@@ -113,6 +126,12 @@ module Gitlab
method = mod.method(name) method = mod.method(name)
end end
unless instrumented?(target)
target.instance_variable_set(PROXY_IVAR, Module.new)
end
proxy_module = self.proxy_module(target)
# Some code out there (e.g. the "state_machine" Gem) checks the arity of # Some code out there (e.g. the "state_machine" Gem) checks the arity of
# a method to make sure it only passes arguments when the method expects # a method to make sure it only passes arguments when the method expects
# any. If we were to always overwrite a method to take an `*args` # any. If we were to always overwrite a method to take an `*args`
...@@ -125,17 +144,13 @@ module Gitlab ...@@ -125,17 +144,13 @@ module Gitlab
args_signature = '*args, &block' args_signature = '*args, &block'
end end
send_signature = "__send__(#{alias_name.inspect}, #{args_signature})" proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
target.class_eval <<-EOF, __FILE__, __LINE__ + 1
alias_method #{alias_name.inspect}, #{name.inspect}
def #{name}(#{args_signature}) def #{name}(#{args_signature})
trans = Gitlab::Metrics::Instrumentation.transaction trans = Gitlab::Metrics::Instrumentation.transaction
if trans if trans
start = Time.now start = Time.now
retval = #{send_signature} retval = super
duration = (Time.now - start) * 1000.0 duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold if duration >= Gitlab::Metrics.method_call_threshold
...@@ -148,10 +163,12 @@ module Gitlab ...@@ -148,10 +163,12 @@ module Gitlab
retval retval
else else
#{send_signature} super
end end
end end
EOF EOF
target.prepend(proxy_module)
end end
# Small layer of indirection to make it easier to stub out the current # Small layer of indirection to make it easier to stub out the current
......
...@@ -42,11 +42,9 @@ feature 'issue move to another project' do ...@@ -42,11 +42,9 @@ feature 'issue move to another project' do
expect(current_url).to include project_path(new_project) 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("Text with #{cross_reference}!1") expect(page).to have_content("Moved from #{cross_reference}#1")
expect(page).to have_content("Moved from #{cross_reference}#1") expect(page).to have_content(issue.title)
expect(page).to have_content(issue.title)
end
end end
context 'projects user does not have permission to move issue to exist' do context 'projects user does not have permission to move issue to exist' do
...@@ -74,7 +72,7 @@ feature 'issue move to another project' do ...@@ -74,7 +72,7 @@ feature 'issue move to another project' do
def edit_issue(issue) def edit_issue(issue)
visit issue_path(issue) visit issue_path(issue)
page.within('.issuable-header') { click_link 'Edit' } page.within('.issuable-actions') { first(:link, 'Edit').click }
end end
def issue_path(issue) def issue_path(issue)
......
...@@ -292,6 +292,23 @@ describe 'Issues', feature: true do ...@@ -292,6 +292,23 @@ describe 'Issues', feature: true do
end end
end end
describe 'new issue' do
context 'dropzone upload file', js: true do
before do
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'should upload file when dragging into textarea' do
drop_in_dropzone test_image_file
# Wait for the file to upload
sleep 1
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end
end
end
def first_issue def first_issue
page.all('ul.issues-list > li').first.text page.all('ul.issues-list > li').first.text
end end
...@@ -299,4 +316,25 @@ describe 'Issues', feature: true do ...@@ -299,4 +316,25 @@ describe 'Issues', feature: true do
def last_issue def last_issue
page.all('ul.issues-list > li').last.text page.all('ul.issues-list > li').last.text
end end
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
var fakeFileInput = window.$('<input/>').attr(
{id: 'fakeFileInput', type: 'file'}
).appendTo('body');
JS
# Attach the file to the fake input selector with Capybara
attach_file("fakeFileInput", file_path)
# Add the file to a fileList array and trigger the fake drop event
page.execute_script <<-JS
var fileList = [$('#fakeFileInput')[0].files[0]];
var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
$('.div-dropzone')[0].dropzone.listeners[0].events.drop(e);
JS
end
def test_image_file
File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
end
end end
require 'spec_helper'
feature 'Member autocomplete', feature: true do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:participant) { create(:user) }
let(:author) { create(:user) }
before do
allow_any_instance_of(Commit).to receive(:author).and_return(author)
login_as user
end
shared_examples "open suggestions" do
it 'suggestions are displayed' do
expect(page).to have_selector('.atwho-view', visible: true)
end
it 'author is suggested' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(author.username)
end
end
it 'participant is suggested' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(participant.username)
end
end
end
context 'adding a new note on a Issue', js: true do
before do
issue = create(:issue, author: author, project: project)
create(:note, note: 'Ultralight Beam', noteable: issue, author: participant)
visit_issue(project, issue)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
context 'adding a new note on a Merge Request ', js: true do
before do
merge = create(:merge_request, source_project: project, target_project: project, author: author)
create(:note, note: 'Ultralight Beam', noteable: merge, author: participant)
visit_merge_request(project, merge)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
context 'adding a new note on a Commit ', js: true do
let(:commit) { project.commit }
before do
allow(commit).to receive(:author).and_return(author)
create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA')
visit_commit(project, commit)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
def open_member_suggestions
sleep 1
page.within('.new-note') do
sleep 1
find('#note_note').native.send_keys('@')
end
end
def visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, issue)
end
def visit_merge_request(project, merge)
visit namespace_project_merge_request_path(project.namespace, project, merge)
end
def visit_commit(project, commit)
visit namespace_project_commit_path(project.namespace, project, commit)
end
end
require 'spec_helper'
feature 'Signup', feature: true do
describe 'signup with no errors' do
it 'creates the user account and sends a confirmation email' do
user = build(:user)
visit root_path
fill_in 'user_name', with: user.name
fill_in 'user_username', with: user.username
fill_in 'user_email', with: user.email
fill_in 'user_password_sign_up', with: user.password
click_button "Sign up"
expect(current_path).to eq user_session_path
expect(page).to have_content("A message with a confirmation link has been sent to your email address.")
end
end
describe 'signup with errors' do
it "displays the errors" do
existing_user = create(:user)
user = build(:user)
visit root_path
fill_in 'user_name', with: user.name
fill_in 'user_username', with: user.username
fill_in 'user_email', with: existing_user.email
fill_in 'user_password_sign_up', with: user.password
click_button "Sign up"
expect(current_path).to eq user_registration_path
expect(page).to have_content("error prohibited this user from being saved")
expect(page).to have_content("Email has already been taken")
end
it 'does not redisplay the password' do
existing_user = create(:user)
user = build(:user)
visit root_path
fill_in 'user_name', with: user.name
fill_in 'user_username', with: user.username
fill_in 'user_email', with: existing_user.email
fill_in 'user_password_sign_up', with: user.password
click_button "Sign up"
expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/)
end
end
end
...@@ -33,8 +33,16 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -33,8 +33,16 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_method(@dummy, :foo) described_class.instrument_method(@dummy, :foo)
end end
it 'renames the original method' do it 'instruments the Class' do
expect(@dummy).to respond_to(:_original_foo) target = @dummy.singleton_class
expect(described_class.instrumented?(target)).to eq(true)
end
it 'defines a proxy method' do
mod = described_class.proxy_module(@dummy.singleton_class)
expect(mod.method_defined?(:foo)).to eq(true)
end end
it 'calls the instrumented method with the correct arguments' do it 'calls the instrumented method with the correct arguments' do
...@@ -76,6 +84,14 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -76,6 +84,14 @@ describe Gitlab::Metrics::Instrumentation do
expect(dummy.method(:test).arity).to eq(0) expect(dummy.method(:test).arity).to eq(0)
end end
describe 'when a module is instrumented multiple times' do
it 'calls the instrumented method with the correct arguments' do
described_class.instrument_method(@dummy, :foo)
expect(@dummy.foo).to eq('foo')
end
end
end end
describe 'with metrics disabled' do describe 'with metrics disabled' do
...@@ -86,7 +102,9 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -86,7 +102,9 @@ describe Gitlab::Metrics::Instrumentation do
it 'does not instrument the method' do it 'does not instrument the method' do
described_class.instrument_method(@dummy, :foo) described_class.instrument_method(@dummy, :foo)
expect(@dummy).to_not respond_to(:_original_foo) target = @dummy.singleton_class
expect(described_class.instrumented?(target)).to eq(false)
end end
end end
end end
...@@ -100,8 +118,14 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -100,8 +118,14 @@ describe Gitlab::Metrics::Instrumentation do
instrument_instance_method(@dummy, :bar) instrument_instance_method(@dummy, :bar)
end end
it 'renames the original method' do it 'instruments instances of the Class' do
expect(@dummy.method_defined?(:_original_bar)).to eq(true) expect(described_class.instrumented?(@dummy)).to eq(true)
end
it 'defines a proxy method' do
mod = described_class.proxy_module(@dummy)
expect(mod.method_defined?(:bar)).to eq(true)
end end
it 'calls the instrumented method with the correct arguments' do it 'calls the instrumented method with the correct arguments' do
...@@ -144,7 +168,7 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -144,7 +168,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class. described_class.
instrument_instance_method(@dummy, :bar) instrument_instance_method(@dummy, :bar)
expect(@dummy.method_defined?(:_original_bar)).to eq(false) expect(described_class.instrumented?(@dummy)).to eq(false)
end end
end end
end end
...@@ -167,18 +191,17 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -167,18 +191,17 @@ describe Gitlab::Metrics::Instrumentation do
it 'recursively instruments a class hierarchy' do it 'recursively instruments a class hierarchy' do
described_class.instrument_class_hierarchy(@dummy) described_class.instrument_class_hierarchy(@dummy)
expect(@child1).to respond_to(:_original_child1_foo) expect(described_class.instrumented?(@child1.singleton_class)).to eq(true)
expect(@child2).to respond_to(:_original_child2_foo) expect(described_class.instrumented?(@child2.singleton_class)).to eq(true)
expect(@child1.method_defined?(:_original_child1_bar)).to eq(true) expect(described_class.instrumented?(@child1)).to eq(true)
expect(@child2.method_defined?(:_original_child2_bar)).to eq(true) expect(described_class.instrumented?(@child2)).to eq(true)
end end
it 'does not instrument the root module' do it 'does not instrument the root module' do
described_class.instrument_class_hierarchy(@dummy) described_class.instrument_class_hierarchy(@dummy)
expect(@dummy).to_not respond_to(:_original_foo) expect(described_class.instrumented?(@dummy)).to eq(false)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end end
end end
...@@ -190,7 +213,7 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -190,7 +213,7 @@ describe Gitlab::Metrics::Instrumentation do
it 'instruments all public class methods' do it 'instruments all public class methods' do
described_class.instrument_methods(@dummy) described_class.instrument_methods(@dummy)
expect(@dummy).to respond_to(:_original_foo) expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
end end
it 'only instruments methods directly defined in the module' do it 'only instruments methods directly defined in the module' do
...@@ -223,7 +246,7 @@ describe Gitlab::Metrics::Instrumentation do ...@@ -223,7 +246,7 @@ describe Gitlab::Metrics::Instrumentation do
it 'instruments all public instance methods' do it 'instruments all public instance methods' do
described_class.instrument_instance_methods(@dummy) described_class.instrument_instance_methods(@dummy)
expect(@dummy.method_defined?(:_original_bar)).to eq(true) expect(described_class.instrumented?(@dummy)).to eq(true)
end end
it 'only instruments methods directly defined in the module' do it 'only instruments methods directly defined in the module' do
......
...@@ -36,4 +36,19 @@ describe ExternalIssue, models: true do ...@@ -36,4 +36,19 @@ describe ExternalIssue, models: true do
expect(issue.title).to eq "External Issue #{issue}" expect(issue.title).to eq "External Issue #{issue}"
end end
end end
describe '#reference_link_text' do
context 'if issue id has a prefix' do
it 'returns the issue ID' do
expect(issue.reference_link_text).to eq 'EXT-1234'
end
end
context 'if issue id is a number' do
let(:issue) { described_class.new('1234', project) }
it 'returns the issue ID prefixed by #' do
expect(issue.reference_link_text).to eq '#1234'
end
end
end
end end
...@@ -910,9 +910,32 @@ describe Repository, models: true do ...@@ -910,9 +910,32 @@ describe Repository, models: true do
end end
end end
describe '.clean_old_archives' do
let(:path) { Gitlab.config.gitlab.repository_downloads_path }
context 'when the downloads directory does not exist' do
it 'does not remove any archives' do
expect(File).to receive(:directory?).with(path).and_return(false)
expect(Gitlab::Popen).not_to receive(:popen)
described_class.clean_old_archives
end
end
context 'when the downloads directory exists' do
it 'removes old archives' do
expect(File).to receive(:directory?).with(path).and_return(true)
expect(Gitlab::Popen).to receive(:popen)
described_class.clean_old_archives
end
end
end
def create_remote_branch(remote_name, branch_name, target) def create_remote_branch(remote_name, branch_name, target)
rugged = repository.rugged rugged = repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
end end
end end
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