Commit 5a8f727f authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into faster-diffs

# Conflicts:
#	app/helpers/notes_helper.rb
#	app/views/projects/diffs/_line.html.haml
#	app/views/projects/diffs/_parallel_view.html.haml
#	app/views/projects/diffs/_text_file.html.haml
#	features/steps/shared/diff_note.rb
parents d925aea9 86d238e4
......@@ -8,6 +8,7 @@ v 8.10.0 (unreleased)
- Wrap code blocks on Activies and Todos page. !4783 (winniehell)
- Align flash messages with left side of page content !4959 (winniehell)
- Display last commit of deleted branch in push events !4699 (winniehell)
- Escape file extension when parsing search results !5141 (winniehell)
- Apply the trusted_proxies config to the rack request object for use with rack_attack
- Add Sidekiq queue duration to transaction metrics.
- Add a new column `artifacts_size` to table `ci_builds` !4964
......@@ -15,10 +16,13 @@ v 8.10.0 (unreleased)
- Make images fit to the size of the viewport !4810
- Fix check for New Branch button on Issue page !4630 (winniehell)
- Fix MR-auto-close text added to description. !4836
- Fix issue, preventing users w/o push access to sort tags !5105 (redetection)
- Add Spring EmojiOne updates.
- Fix viewing notification settings when a project is pending deletion
- Fix pagination when sorting by columns with lots of ties (like priority)
- Updated project header design
- Exclude email check from the standard health check
- Updated layout for Projects, Groups, Users on Admin area !4424
- Fix changing issue state columns in milestone view
- Add notification settings dropdown for groups
- Allow importing from Github using Personal Access Tokens. (Eric K Idema)
......@@ -28,7 +32,9 @@ v 8.10.0 (unreleased)
- Reduce size of HTML used by diff comment forms
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- PipelinesFinder uses git cache data
- Throttle the update of `project.pushes_since_gc` to 1 minute.
- Check for conflicts with existing Project's wiki path when creating a new project.
- Show last push widget in upstream after push to fork
- Don't instantiate a git tree on Projects show default view
- Bump Rinku to 2.0.0
- Remove unused front-end variable -> default_issues_tracker
......@@ -39,20 +45,30 @@ v 8.10.0 (unreleased)
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
- Set import_url validation to be more strict
- Add basic system information like memory and disk usage to the admin panel
- Don't garbage collect commits that have related DB records like comments
- More descriptive message for git hooks and file locks
- Handle custom Git hook result in GitLab UI
v 8.9.5 (unreleased)
- Allow '?', or '&' for label names
- Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests
- Add date when user joined the team on the member page
- Fix 404 redirect after validation fails importing a GitLab project
v 8.9.5
- Add more debug info to import/export and memory killer. !5108
- Fixed avatar alignment in new MR view. !5095
- Fix diff comments not showing up in activity feed. !5069
- Add index on both Award Emoji user and name. !5061
- Downgrade to Redis 3.2.2 due to massive memory leak with Sidekiq. !5056
- Re-enable import button when import process fails due to namespace already being taken. !5053
- Fix snippets comments not displayed. !5045
- Fix emoji paths in relative root configurations. !5027
- Fix issues importing events in Import/Export. !4987
- Fixed 'use shortcuts' button on docs. !4979
- Admin should be able to turn shared runners into specific ones. !4961
- Update RedCloth to 4.3.2 for CVE-2012-6684. !4929 (Takuya Noguchi)
- Improve the request / withdraw access button. !4860
- Fix assigning shared runners as admins. !4961
- Show "locked" label for locked runners on runners admin. !4961
- Downgrade to Redis 3.2.2 due to massive memory leak with Sidekiq
- Add index on the user and emoji name on AwardEmoji table !5061
- Fixes issues importing events in Import/Export. Import/Export version bumped to 0.1.1
- Fix import button disabled when import process fail due to the namespace already been taken.
- Security: Update RedCloth to 4.3.2 (Takuya Noguchi)
v 8.9.4
- Fix privilege escalation issue with OAuth external users.
......
......@@ -2,7 +2,7 @@ class @CiBuild
@interval: null
@state: null
constructor: (@build_url, @build_status, @state) ->
constructor: (@page_url, @build_url, @build_status, @state) ->
clearInterval(CiBuild.interval)
# Init breakpoint checker
......@@ -41,7 +41,7 @@ class @CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
if window.location.href.split("#").first() is @build_url
if window.location.href.split("#").first() is @page_url
@getBuildTrace()
, 4000
......@@ -57,7 +57,7 @@ class @CiBuild
getBuildTrace: ->
$.ajax
url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}"
url: "#{@page_url}/trace.json?state=#{encodeURIComponent(@state)}"
dataType: "json"
success: (log) =>
if log.state
......@@ -70,7 +70,7 @@ class @CiBuild
$('.js-build-output').html log.html
@checkAutoscroll()
else if log.status isnt @build_status
Turbolinks.visit @build_url
Turbolinks.visit @page_url
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
......
......@@ -127,7 +127,7 @@ class Dispatcher
when 'groups'
new UsersSelect()
when 'projects'
new NamespaceSelect()
new NamespaceSelects()
when 'dashboard', 'root'
shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles'
......
......@@ -6,7 +6,6 @@ class @FilesCommentButton
@COMMENT_BUTTON_CLASS = '.add-diff-note'
@COMMENT_BUTTON_TEMPLATE = _.template '<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'
@LINE_HOLDER_CLASS = '.line_holder'
@LINE_NUMBER_CLASS = 'diff-line-num'
@LINE_CONTENT_CLASS = 'line_content'
@UNFOLDABLE_LINE_CLASS = 'js-unfold'
......@@ -33,20 +32,20 @@ class @FilesCommentButton
render: (e) ->
currentTarget = $(e.currentTarget)
textFileElement = @getTextFileElement(currentTarget)
lineHolderElement = @getLineHolder(currentTarget)
lineContentElement = @getLineContent(currentTarget)
buttonParentElement = @getButtonParent(currentTarget)
return unless @shouldRender e, buttonParentElement
buttonParentElement.append @buildButton
noteable_type: textFileElement.attr 'data-noteable-type'
noteable_id: textFileElement.attr 'data-noteable-id'
commit_id: textFileElement.attr 'data-commit-id'
discussion_id: lineContentElement.attr('data-discussion-id') or lineHolderElement.attr('data-discussion-id')
line_code: lineContentElement.attr('data-line-code') or lineHolderElement.attr('id')
note_type: lineContentElement.attr 'data-note-type'
position: lineContentElement.attr 'data-position'
line_type: lineContentElement.attr 'data-line-type'
note_type: textFileElement.attr 'data-note-type'
noteable_id: textFileElement.attr 'data-noteable-id'
noteable_type: textFileElement.attr 'data-noteable-type'
discussion_id: lineContentElement.attr 'data-discussion-id'
line_code: lineContentElement.attr 'data-line-code'
return
destroy: (e) =>
......@@ -58,21 +57,18 @@ class @FilesCommentButton
initializedButtonTemplate = @COMMENT_BUTTON_TEMPLATE
COMMENT_BUTTON_CLASS: @COMMENT_BUTTON_CLASS.substr 1
$(initializedButtonTemplate).attr
'data-noteable-type': buttonAttributes.noteable_type
'data-noteable-id': buttonAttributes.noteable_id
'data-commit-id': buttonAttributes.commit_id
'data-discussion-id': buttonAttributes.discussion_id
'data-noteable-type': buttonAttributes.noteable_type
'data-line-type': buttonAttributes.line_type
'data-note-type': buttonAttributes.note_type
'data-line-code': buttonAttributes.line_code
'data-position': buttonAttributes.position
'data-discussion-id': buttonAttributes.discussion_id
'data-line-type': buttonAttributes.line_type
getTextFileElement: (hoveredElement) ->
$(hoveredElement.closest(@TEXT_FILE_SELECTOR))
getLineHolder: (hoveredElement) ->
return hoveredElement if hoveredElement.hasClass @LINE_HOLDER_CLASS
$(hoveredElement.parent())
getLineContent: (hoveredElement) ->
return hoveredElement if hoveredElement.hasClass @LINE_CONTENT_CLASS
......
......@@ -190,7 +190,7 @@ GitLab.GfmAutoComplete =
callbacks:
beforeSave: (merges) ->
sanitizeLabelTitle = (title)->
if /\w+\s+\w+/g.test(title)
if /[\w\?&]+\s+[\w\?&]+/g.test(title)
"\"#{sanitize(title)}\""
else
sanitize(title)
......
......@@ -32,7 +32,7 @@ class @LabelsSelect
if issueUpdateURL
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
<a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- label.title %>">
<a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>">
<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
<%- label.title %>
</span>
......@@ -261,7 +261,7 @@ class @LabelsSelect
$a.attr('data-label-id', label.id)
$a.addClass(selectedClass.join(' '))
.html("#{colorEl} #{_.escape(label.title)}")
.html("#{colorEl} #{label.title}")
# Return generated html
$li.html($a).prop('outerHTML')
......@@ -288,7 +288,7 @@ class @LabelsSelect
fieldName: $dropdown.data('field-name')
id: (label) ->
if $dropdown.hasClass("js-filter-submit") and not label.isAny?
_.escape label.title
label.title
else
label.id
......
class @NamespaceSelect
constructor: ->
namespaceFormatResult = (namespace) ->
markup = "<div class='namespace-result'>"
markup += "<span class='namespace-kind'>" + namespace.kind + "</span>"
markup += "<span class='namespace-path'>" + namespace.path + "</span>"
markup += "</div>"
markup
formatSelection = (namespace) ->
namespace.kind + ": " + namespace.path
$('.ajax-namespace-select').each (i, select) ->
$(select).select2
placeholder: "Search for namespace"
multiple: $(select).hasClass('multiselect')
minimumInputLength: 0
query: (query) ->
Api.namespaces query.term, (namespaces) ->
data = { results: namespaces }
query.callback(data)
dropdownCssClass: "ajax-namespace-dropdown"
formatResult: namespaceFormatResult
formatSelection: formatSelection
constructor: (opts) ->
{
@dropdown
} = opts
showAny = true
fieldName = 'namespace_id'
if @dropdown.attr 'data-field-name'
fieldName = @dropdown.data 'fieldName'
if @dropdown.attr 'data-show-any'
showAny = @dropdown.data 'showAny'
@dropdown.glDropdown(
filterable: true
selectable: true
filterRemote: true
search:
fields: ['path']
fieldName: fieldName
toggleLabel: (selected) ->
return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}"
data: (term, dataCallback) ->
Api.namespaces term, (namespaces) ->
if showAny
anyNamespace =
text: 'Any namespace'
id: null
namespaces.unshift(anyNamespace)
namespaces.splice 1, 0, 'divider'
dataCallback(namespaces)
text: (namespace) ->
return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}"
renderRow: @renderRow
clicked: @onSelectItem
)
onSelectItem: (item, el, e) =>
e.preventDefault()
class @NamespaceSelects
constructor: (opts = {}) ->
{
@$dropdowns = $('.js-namespace-select')
} = opts
@$dropdowns.each (i, dropdown) ->
$dropdown = $(dropdown)
new NamespaceSelect(
dropdown: $dropdown
)
......@@ -100,13 +100,40 @@ class @Notes
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
keydownNoteText: (e) ->
$this = $(this)
if $this.val() is '' and e.which is 38 and not isMetaKey e
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])
keydownNoteText: (e) =>
return if isMetaKey e
$textarea = $(e.target)
# Edit previous note when UP arrow is hit
switch e.which
when 38
return unless $textarea.val() is ''
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])
# Cancel creating diff note or editing any note when ESCAPE is hit
when 27
discussionNoteForm = $textarea.closest('.js-discussion-note-form')
if discussionNoteForm.length
if $textarea.val() isnt ''
return unless confirm('Are you sure you want to cancel creating this comment?')
@removeDiscussionNoteForm(discussionNoteForm)
return
editNote = $textarea.closest('.note')
if editNote.length
originalText = $textarea.closest('form').data('original-note')
newText = $textarea.val()
if originalText isnt newText
return unless confirm('Are you sure you want to cancel editing this comment?')
@removeNoteEditForm(editNote)
isMetaKey = (e) ->
(e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
......@@ -213,12 +240,16 @@ class @Notes
@note_ids.push(note.id)
form = $("#new-discussion-note-form-#{note.discussion_id}")
if note.original_discussion_id? and form.length is 0
form = $("#new-discussion-note-form-#{note.original_discussion_id}")
row = form.closest("tr")
note_html = $(note.html)
note_html.syntaxHighlight()
# is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
if note.original_discussion_id? and discussionContainer.length is 0
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']")
if discussionContainer.length is 0
# insert the note and the reply button after the temp row
row.after note.discussion_html
......@@ -291,6 +322,7 @@ class @Notes
form.addClass "js-main-target-form"
form.find("#note_line_code").remove()
form.find("#note_position").remove()
form.find("#note_type").remove()
###
......@@ -308,10 +340,12 @@ class @Notes
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
form.find("#note_line_code").val()
form.find("#note_noteable_type").val()
form.find("#note_noteable_id").val()
form.find("#note_commit_id").val()
form.find("#note_type").val()
form.find("#note_line_code").val()
form.find("#note_position").val()
]
###
......@@ -401,9 +435,12 @@ class @Notes
Hides edit form and restores the original note text to the editor textarea.
###
cancelEdit: (e) ->
cancelEdit: (e) =>
e.preventDefault()
note = $(this).closest(".note")
note = $(e.target).closest('.note')
@removeNoteEditForm(note)
removeNoteEditForm: (note) ->
form = note.find(".current-note-edit-form")
note.removeClass "is-editting"
form.removeClass("current-note-edit-form")
......@@ -482,10 +519,12 @@ class @Notes
setupDiscussionNoteForm: (dataHolder, form) =>
# setup note target
form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
form.attr "data-line-code", dataHolder.data("lineCode")
form.find("#note_type").val dataHolder.data("noteType")
form.find("#line_type").val dataHolder.data("lineType")
form.find("#note_commit_id").val dataHolder.data("commitId")
form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_position").val dataHolder.attr("data-position")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
form.find('.js-note-discard')
......
......@@ -23,6 +23,9 @@
.dropdown-menu,
.dropdown-menu-nav {
display: block;
@media (max-width: $screen-xs-max) {
width: 100%;
}
}
.dropdown-menu-toggle {
......@@ -65,6 +68,10 @@
color: $dropdown-toggle-hover-icon-color;
}
}
&.large {
width: 200px;
}
}
.dropdown-menu,
......
......@@ -50,7 +50,7 @@
}
a:not(.btn) {
color: $gl-dark-link-color;
color: $gl-text-color;
}
.left-options {
......
......@@ -137,6 +137,15 @@ ul.content-list {
padding-top: 1px;
float: right;
> .control-text {
margin-right: $gl-padding-top;
line-height: 40px;
&:last-child {
margin-right: 0;
}
}
> .btn,
> .btn-group {
margin-right: $gl-padding-top;
......
......@@ -134,6 +134,11 @@
margin-bottom: 0;
border-bottom: none;
&.wide {
width: 100%;
display: block;
}
li a {
padding: 16px 10px 11px;
}
......@@ -164,6 +169,7 @@
> .btn {
margin-right: $gl-padding-top;
display: inline-block;
vertical-align: top;
&:last-child {
margin-right: 0;
......
......@@ -71,3 +71,36 @@
@extend .broadcast-message;
margin-bottom: 20px;
}
// Users List
.users-list {
.user-row {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.user-details {
flex: 1 1 auto;
}
.user-name {
display: inline-block;
font-weight: bold;
}
.controls {
> .btn, > .dropdown {
margin-left: 5px;
}
}
.dropdown {
.btn-block {
margin-bottom: 0;
line-height: inherit;
}
}
}
......@@ -434,13 +434,3 @@
}
}
}
.discussion {
.diff-content {
.diff-line-num {
&:before {
content: attr(data-linenumber);
}
}
}
}
......@@ -38,6 +38,39 @@
margin-right: 15px;
}
}
&.group-admin {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
.group-avatar, .group-details, .group-controls {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.group-details {
flex: 1 1 auto;
flex-direction: column;
min-width: 0;
}
.group-controls {
align-items: center;
a {
margin-left: 5px;
}
}
}
}
.ldap-group-links {
.form-actions {
margin-bottom: $gl-padding;
}
}
.groups-cover-block {
......
......@@ -475,10 +475,6 @@ pre.light-well {
a:hover {
text-decoration: none;
}
> span {
margin-left: 10px;
}
}
}
......
......@@ -208,7 +208,7 @@
margin-top: 5px;
@media (min-width: $screen-sm-min) {
width: 160px;
width: 180px;
margin-top: 0;
}
}
......
......@@ -5,11 +5,12 @@ class Admin::ProjectsController < Admin::ApplicationController
def index
@projects = Project.all
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.with_push if params[:with_push].present?
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present?
@projects = @projects.non_archived unless params[:with_archived].present?
@projects = @projects.non_archived unless params[:archived].present?
@projects = @projects.personal(current_user) if params[:personal].present?
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
......
......@@ -27,10 +27,7 @@ class Import::GitlabProjectsController < Import::BaseController
notice: "Project '#{@project.name}' is being imported."
)
else
redirect_to(
new_import_gitlab_project_path,
alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}"
)
redirect_back_or_default(options: { alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" })
end
end
......
......@@ -57,7 +57,7 @@ class Projects::BlobController < Projects::ApplicationController
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
@diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight
@diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight
render layout: false
end
......
......@@ -121,7 +121,6 @@ class Projects::CommitController < Projects::ApplicationController
opts[:ignore_whitespace_change] = true if params[:format] == 'diff'
@diffs = commit.diffs(opts)
@diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count
@statuses = CommitStatus.where(pipeline: pipelines)
......
......@@ -14,14 +14,22 @@ class Projects::CompareController < Projects::ApplicationController
def show
compare = CompareService.new.
execute(@project, @head_ref, @project, @base_ref, diff_options)
execute(@project, @head_ref, @project, @start_ref, diff_options)
if compare
@commits = Commit.decorate(compare.commits, @project)
@start_commit = @project.commit(@start_ref)
@commit = @project.commit(@head_ref)
@base_commit = @project.merge_base_commit(@base_ref, @head_ref)
@base_commit = @project.merge_base_commit(@start_ref, @head_ref)
@diffs = compare.diffs(diff_options)
@diff_refs = [@base_commit, @commit]
@diff_refs = Gitlab::Diff::DiffRefs.new(
base_sha: @base_commit.try(:sha),
start_sha: @start_commit.try(:sha),
head_sha: @commit.try(:sha)
)
@diff_notes_disabled = true
@grouped_diff_notes = {}
end
......@@ -35,12 +43,12 @@ class Projects::CompareController < Projects::ApplicationController
private
def assign_ref_vars
@base_ref = Addressable::URI.unescape(params[:from])
@start_ref = Addressable::URI.unescape(params[:from])
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
def merge_request
@merge_request ||= @project.merge_requests.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref)
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
end
end
......@@ -58,14 +58,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json { render json: @merge_request }
format.json do
render json: @merge_request
end
format.patch do
headers.store(*Gitlab::Workhorse.send_git_patch(@project.repository,
@merge_request.diff_base_commit.id,
@merge_request.last_commit.id))
headers['Content-Disposition'] = 'inline'
head :ok
return render_404 unless @merge_request.diff_refs
send_git_patch @project.repository, @merge_request.diff_refs
end
format.diff do
return render_404 unless @merge_request.diff_refs
......@@ -77,18 +80,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
@commit = @merge_request.last_commit
@base_commit = @merge_request.diff_base_commit
# MRs created before 8.4 don't have a diff_base_commit,
# but we need it for the "View file @ ..." link by deleted files
@base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
@comments_target = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
@use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
@grouped_diff_notes = @merge_request.notes.grouped_diff_notes
Banzai::NoteRenderer.render(
......@@ -134,7 +134,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
......@@ -212,7 +212,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
if params[:sha] != @merge_request.source_sha
if params[:sha] != @merge_request.diff_head_sha
@status = :sha_mismatch
return
end
......@@ -274,16 +274,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
status ||= "preparing"
else
ci_service = @merge_request.source_project.ci_service
status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage)
coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch)
end
end
response = {
title: merge_request.title,
sha: merge_request.last_commit_short_sha,
sha: merge_request.diff_head_commit.short_id,
status: status,
coverage: coverage
}
......
......@@ -128,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController
elsif note.valid?
Banzai::NoteRenderer.render([note], @project, current_user)
{
attrs = {
valid: true,
id: note.id,
discussion_id: note.discussion_id,
......@@ -138,6 +138,23 @@ class Projects::NotesController < Projects::ApplicationController
discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
}
# The discussion_id is used to add the comment to the correct discussion
# element on the merge request page. Among other things, the discussion_id
# contains the sha of head commit of the merge request.
# When new commits are pushed into the merge request after the initial
# load of the merge request page, the discussion elements will still have
# the old discussion_ids, with the old head commit sha. The new comment,
# however, will have the new discussion_id with the new commit sha.
# To ensure that these new comments will still end up in the correct
# discussion element, we also send the original discussion_id, with the
# old commit sha, along, and fall back on this value when no discussion
# element with the new discussion_id could be found.
if note.new_diff_note? && note.position != note.original_position
attrs[:original_discussion_id] = note.original_discussion_id
end
attrs
else
{
valid: false,
......@@ -154,7 +171,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_params
params.require(:note).permit(
:note, :noteable, :noteable_id, :noteable_type, :project_id,
:attachment, :line_code, :commit_id, :type
:attachment, :line_code, :commit_id, :type, :position
)
end
......
......@@ -306,4 +306,15 @@ module ApplicationHelper
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
# While similarly named to Rails's `link_to_if`, this method behaves quite differently.
# If `condition` is truthy, a link will be returned with the result of the block
# as its body. If `condition` is falsy, only the result of the block will be returned.
def conditional_link_to(condition, options, html_options = {}, &block)
if condition
link_to options, html_options, &block
else
capture(&block)
end
end
end
......@@ -30,12 +30,8 @@ module DiffHelper
options
end
def safe_diff_files(diffs, diff_refs)
diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) }
end
def generate_line_code(file_path, line)
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
def safe_diff_files(diffs, diff_refs: nil, repository: nil)
diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
end
def unfold_bottom_class(bottom)
......@@ -93,6 +89,8 @@ module DiffHelper
end
def commit_for_diff(diff_file)
return diff_file.content_commit if diff_file.content_commit
if diff_file.deleted_file
@base_commit || @commit.parent || @commit
else
......@@ -100,10 +98,11 @@ module DiffHelper
end
end
def diff_file_html_data(project, diff_commit, diff_file)
def diff_file_html_data(project, diff_file)
commit = commit_for_diff(diff_file)
{
blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
tree_join(diff_commit.id, diff_file.file_path))
tree_join(commit.id, diff_file.file_path))
}
end
......
......@@ -39,7 +39,7 @@ module DropdownsHelper
end
end
def dropdown_toggle(toggle_text, data_attr, options)
def dropdown_toggle(toggle_text, data_attr, options = {})
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
output << icon('chevron-down')
......
......@@ -27,7 +27,7 @@ module MergeRequestsHelper
end
def ci_build_details_path(merge_request)
build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch)
return nil unless build_url
parsed_url = URI.parse(build_url)
......
......@@ -24,33 +24,54 @@ module NotesHelper
}.to_json
end
def note_text_file_data
def diff_view_data
return {} unless defined?(@comments_target) && @comments_target.any?
@comments_target.slice(:noteable_id, :noteable_type, :commit_id).merge(note_type: LegacyDiffNote.name)
@comments_target.slice(:noteable_id, :noteable_type, :commit_id)
end
def note_line_parallel_data(line_code, line_type)
data = {
def use_legacy_diff_notes?(line_code)
return @use_legacy_diff_notes if defined?(@use_legacy_diff_notes)
# If the controller doesn't force the use of legacy diff notes, we
# determine this on a line-by-line basis by seeing if there already exist
# active legacy diff notes at this line, in which case newly created notes
# will use the legacy technology as well.
# We do this because the discussion_id values of legacy and "new" diff
# notes, which are used to group notes on the merge request discussion tab,
# are incompatible.
# If we didn't, diff notes that would show for the same line on the changes
# tab, would show in different discussions on the discussion tab.
line_diff_notes = @grouped_diff_notes[line_code]
@use_legacy_diff_notes = line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?)
end
def diff_view_line_data(line_code, position, line_type)
return if @diff_notes_disabled
{
line_code: line_code,
position: position.to_json,
line_type: line_type,
note_type: (use_legacy_diff_notes?(line_code) ? LegacyDiffNote.name : DiffNote.name),
discussion_id: discussion_id(line_code, position)
}
end
unless @diff_notes_disabled
data.merge!(
discussion_id: discussion_id(line_code)
def discussion_id(line_code, position)
if use_legacy_diff_notes?(line_code)
LegacyDiffNote.build_discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
line_code
)
else
discussion_id = DiffNote.build_discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
position
)
end
data
end
def discussion_id(line_code)
LegacyDiffNote.build_discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
line_code
)
end
def link_to_reply_discussion(note, line_type = nil)
......@@ -65,14 +86,15 @@ module NotesHelper
}
if note.diff_note?
data.merge!(
line_code: note.line_code,
note_type: LegacyDiffNote.name
)
data[:note_type] = note.type
data.merge!(note.diff_attributes)
end
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
content_tag(:div, class: "discussion-reply-holder") do
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
end
end
def note_max_access_for_user(note)
......@@ -84,4 +106,14 @@ module NotesHelper
full_key = { project: note.project, user_id: note.author_id }
@max_access_by_user_id[full_key]
end
def diff_note_path(note)
return unless note.diff_note?
if note.for_merge_request? && note.active?
diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code)
elsif note.for_commit?
namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code)
end
end
end
......@@ -293,7 +293,11 @@ module ProjectsHelper
end
def last_push_event
if current_user
return unless current_user
if fork = current_user.fork_of(@project)
current_user.recent_push(fork.id)
else
current_user.recent_push(@project.id)
end
end
......
# Helpers to send Git blobs, diffs or archives through Workhorse.
# Helpers to send Git blobs, diffs, patches or archives through Workhorse.
# Workhorse will also serve files when using `send_file`.
module WorkhorseHelper
# Send a Git blob through Workhorse
......@@ -16,6 +16,13 @@ module WorkhorseHelper
head :ok
end
# Send a Git patch through Workhorse
def send_git_patch(repository, diff_refs)
headers.store(*Gitlab::Workhorse.send_git_patch(repository, diff_refs))
headers['Content-Disposition'] = 'inline'
head :ok
end
# Archive a Git repository and send it through Workhorse
def send_git_archive(repository, ref:, format:)
headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
......
......@@ -214,6 +214,13 @@ class Commit
@raw.short_id(7)
end
def diff_refs
Gitlab::Diff::DiffRefs.new(
base_sha: self.parent_id || self.sha,
head_sha: self.sha
)
end
def pipelines
@pipeline ||= project.pipelines.where(sha: sha)
end
......
module NoteOnDiff
extend ActiveSupport::Concern
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
included do
delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
end
def diff_note?
true
end
def diff_file
raise NotImplementedError
end
def diff_line
raise NotImplementedError
end
def for_line?(line)
raise NotImplementedError
end
def diff_attributes
raise NotImplementedError
end
def can_be_award_emoji?
false
end
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines
prev_lines = []
highlighted_diff_lines.each do |line|
if line.meta?
prev_lines.clear
else
prev_lines << line
break if for_line?(line)
prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
end
end
prev_lines
end
end
......@@ -11,6 +11,8 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
after_save :keep_around_commit
def commit
project.commit(sha)
end
......@@ -26,4 +28,8 @@ class Deployment < ActiveRecord::Base
def last?
self == environment.last_deployment
end
def keep_around_commit
project.repository.keep_around(self.sha)
end
end
class DiffNote < Note
include NoteOnDiff
serialize :original_position, Gitlab::Diff::Position
serialize :position, Gitlab::Diff::Position
validates :original_position, presence: true
validates :position, presence: true
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
validate :positions_complete
validate :verify_supported
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code
after_save :keep_around_commits
class << self
def build_discussion_id(noteable_type, noteable_id, position)
[super(noteable_type, noteable_id), *position.key].join("-")
end
end
def new_diff_note?
true
end
def diff_attributes
{ position: position.to_json }
end
def discussion_id
@discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
end
def original_discussion_id
@original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
end
def position=(new_position)
if new_position.is_a?(String)
new_position = JSON.parse(new_position) rescue nil
end
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
end
super(new_position)
end
def diff_file
@diff_file ||= self.original_position.diff_file(self.project.repository)
end
def diff_line
@diff_line ||= diff_file.line_for_position(self.original_position) if diff_file
end
def for_line?(line)
diff_file.position(line) == self.original_position
end
def active?(diff_refs = nil)
return false unless supported?
return true if for_commit?
diff_refs ||= self.noteable.diff_refs
self.position.diff_refs == diff_refs
end
private
def supported?
!self.for_merge_request? || self.noteable.support_new_diff_notes?
end
def set_original_position
self.original_position = self.position.dup
end
def set_line_code
self.line_code = self.position.line_code(self.project.repository)
end
def update_position
return unless supported?
return if for_commit?
return if active?
Notes::DiffPositionUpdateService.new(
self.project,
nil,
old_diff_refs: self.position.diff_refs,
new_diff_refs: self.noteable.diff_refs,
paths: self.position.paths
).execute(self)
end
def verify_supported
return if supported?
errors.add(:noteable, "doesn't support new-style diff notes")
end
def positions_complete
return if self.original_position.complete? && self.position.complete?
errors.add(:position, "is invalid")
end
def keep_around_commits
project.repository.keep_around(self.original_position.base_sha)
project.repository.keep_around(self.original_position.start_sha)
project.repository.keep_around(self.original_position.head_sha)
if self.position != self.original_position
project.repository.keep_around(self.position.base_sha)
project.repository.keep_around(self.position.start_sha)
project.repository.keep_around(self.position.head_sha)
end
end
end
......@@ -67,7 +67,7 @@ class Event < ActiveRecord::Base
elsif issue? || issue_note?
Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
else
((merge_request? || note?) && target) || milestone?
((merge_request? || note?) && target.present?) || milestone?
end
end
......@@ -136,7 +136,7 @@ class Event < ActiveRecord::Base
end
def note?
target_type == "Note"
target.is_a?(Note)
end
def issue?
......
......@@ -20,10 +20,10 @@ class Label < ActiveRecord::Base
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles
# Don't allow ',' for label titles
validates :title,
presence: true,
format: { with: /\A[^&\?,]+\z/ },
format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id }
before_save :nullify_priority
......@@ -58,8 +58,8 @@ class Label < ActiveRecord::Base
(?:
(?<label_id>\d+) | # Integer-based label ID, or
(?<label_name>
[A-Za-z0-9_-]+ | # String-based single-word label title, or
"[^&\?,]+" # String-based multi-word label surrounded in quotes
[A-Za-z0-9_\-\?&]+ | # String-based single-word label title, or
"[^,]+" # String-based multi-word label surrounded in quotes
)
)
}x
......@@ -114,7 +114,7 @@ class Label < ActiveRecord::Base
end
def title=(value)
write_attribute(:title, Sanitize.clean(value.to_s)) if value.present?
write_attribute(:title, sanitize_title(value)) if value.present?
end
private
......@@ -132,4 +132,8 @@ class Label < ActiveRecord::Base
def nullify_priority
self.priority = nil if priority.blank?
end
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end
end
class LegacyDiffNote < Note
include NoteOnDiff
serialize :st_diff
validates :line_code, presence: true, line_code: true
......@@ -11,12 +13,12 @@ class LegacyDiffNote < Note
end
end
def diff_note?
def legacy_diff_note?
true
end
def legacy_diff_note?
true
def diff_attributes
{ line_code: line_code }
end
def discussion_id
......@@ -27,61 +29,20 @@ class LegacyDiffNote < Note
line_code.split('_')[0] if line_code
end
def diff_old_line
line_code.split('_')[1].to_i if line_code
end
def diff_new_line
line_code.split('_')[2].to_i if line_code
end
def diff
@diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
end
def diff_file_path
diff.new_path.presence || diff.old_path
end
def diff_lines
@diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
def diff_file
@diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff
end
def diff_line
@diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code }
@diff_line ||= diff_file.line_for_line_code(self.line_code)
end
def diff_line_text
diff_line.try(:text)
end
def diff_line_type
diff_line.try(:type)
end
def highlighted_diff_lines
Gitlab::Diff::Highlight.new(diff_lines).highlight
end
def truncated_diff_lines
max_number_of_lines = 16
prev_match_line = nil
prev_lines = []
highlighted_diff_lines.each do |line|
if line.type == "match"
prev_lines.clear
prev_match_line = line
else
prev_lines << line
break if generate_line_code(line) == self.line_code
prev_lines.shift if prev_lines.length >= max_number_of_lines
end
end
prev_lines
def for_line?(line)
!line.meta? && diff_file.line_code(line) == self.line_code
end
# Check if this note is part of an "active" discussion
......@@ -102,7 +63,7 @@ class LegacyDiffNote < Note
if noteable_diff
parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text }
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line.text }
else
@active = false
end
......@@ -110,10 +71,6 @@ class LegacyDiffNote < Note
@active
end
def award_emoji_supported?
false
end
private
def find_diff
......@@ -149,10 +106,6 @@ class LegacyDiffNote < Note
self.class.where(attributes).last.try(:diff)
end
def generate_line_code(line)
Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos)
end
# Find the diff on noteable that matches our own
def find_noteable_diff
diffs = noteable.diffs(Commit.max_diff_options)
......
......@@ -16,7 +16,7 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash
after_create :create_merge_request_diff, unless: :importing
after_create :create_merge_request_diff, unless: :importing?
after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
......@@ -29,10 +29,6 @@ class MergeRequest < ActiveRecord::Base
# when creating new merge request
attr_accessor :can_be_created, :compare_commits, :compare
# Temporary fields to store target_sha, and base_sha to
# compare when importing pull requests from GitHub
attr_accessor :base_target_sha, :head_source_sha
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
......@@ -89,12 +85,7 @@ class MergeRequest < ActiveRecord::Base
state :cannot_be_merged
around_transition do |merge_request, transition, block|
merge_request.record_timestamps = false
begin
block.call
ensure
merge_request.record_timestamps = true
end
Gitlab::Timeless.timeless(merge_request, &block)
end
end
......@@ -169,10 +160,6 @@ class MergeRequest < ActiveRecord::Base
reference
end
def last_commit
merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
end
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
......@@ -182,15 +169,86 @@ class MergeRequest < ActiveRecord::Base
end
def diff_base_commit
if merge_request_diff
if persisted?
merge_request_diff.base_commit
elsif source_sha
self.target_project.merge_base_commit(self.source_sha, self.target_branch)
elsif diff_start_commit && diff_head_commit
self.target_project.merge_base_commit(diff_start_sha, diff_head_sha)
end
end
# MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
# but we need to get a commit for the "View file @ ..." link by deleted files,
# so we find the likely one if we can't get the actual one.
# This will not be the actual base commit if the target branch was merged into
# the source branch after the merge request was created, but it is good enough
# for the specific purpose of linking to a commit.
# It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
# true base commit, so we can't simply have `#diff_base_commit` fall back on
# this method.
def likely_diff_base_commit
first_commit.parent || first_commit
end
def diff_start_commit
if persisted?
merge_request_diff.start_commit
else
target_branch_head
end
end
def diff_head_commit
if persisted?
merge_request_diff.head_commit
else
source_branch_head
end
end
def last_commit_short_sha
last_commit.short_id
def diff_start_sha
diff_start_commit.try(:sha)
end
def diff_base_sha
diff_base_commit.try(:sha)
end
def diff_head_sha
diff_head_commit.try(:sha)
end
# When importing a pull request from GitHub, the old and new branches may no
# longer actually exist by those names, but we need to recreate the merge
# request diff with the right source and target shas.
# We use these attributes to force these to the intended values.
attr_writer :target_branch_sha, :source_branch_sha
def source_branch_head
source_branch_ref = @source_branch_sha || source_branch
source_project.repository.commit(source_branch) if source_branch_ref
end
def target_branch_head
target_branch_ref = @target_branch_sha || target_branch
target_project.repository.commit(target_branch) if target_branch_ref
end
def target_branch_sha
target_branch_head.try(:sha)
end
def source_branch_sha
source_branch_head.try(:sha)
end
def diff_refs
return unless diff_start_commit || diff_base_commit
Gitlab::Diff::DiffRefs.new(
base_sha: diff_base_sha,
start_sha: diff_start_sha,
head_sha: diff_head_sha
)
end
def validate_branches
......@@ -227,21 +285,30 @@ class MergeRequest < ActiveRecord::Base
def update_merge_request_diff
if source_branch_changed? || target_branch_changed?
reload_code
reload_diff
end
end
def reload_code
if merge_request_diff && open?
merge_request_diff.reload_content
end
def reload_diff
return unless merge_request_diff && open?
old_diff_refs = self.diff_refs
merge_request_diff.reload_content
new_diff_refs = self.diff_refs
update_diff_notes_positions(
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs
)
end
def check_if_can_be_merged
return unless unchecked?
can_be_merged =
!broken? && project.repository.can_be_merged?(source_sha, target_branch)
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
if can_be_merged
mark_as_mergeable
......@@ -293,7 +360,7 @@ class MergeRequest < ActiveRecord::Base
!source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.abilities.allowed?(current_user, :push_code, source_project) &&
last_commit == source_project.commit(source_branch)
diff_head_commit == source_branch_head
end
def should_remove_source_branch?
......@@ -331,8 +398,8 @@ class MergeRequest < ActiveRecord::Base
work_in_progress: work_in_progress?
}
if last_commit
attrs.merge!(last_commit: last_commit.hook_attrs)
if diff_head_commit
attrs.merge!(last_commit: diff_head_commit.hook_attrs)
end
attributes.merge!(attrs)
......@@ -510,22 +577,6 @@ class MergeRequest < ActiveRecord::Base
end
end
def target_sha
return @base_target_sha if defined?(@base_target_sha)
target_project.repository.commit(target_branch).try(:sha)
end
def source_sha
return @head_source_sha if defined?(@head_source_sha)
last_commit.try(:sha) || source_tip.try(:sha)
end
def source_tip
source_branch && source_project.repository.commit(source_branch)
end
def fetch_ref
target_project.repository.fetch_ref(
source_project.repository.path_to_repo,
......@@ -558,10 +609,10 @@ class MergeRequest < ActiveRecord::Base
def diverged_commits_count
cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits")
if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha
if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha
cache = {
source_sha: source_sha,
target_sha: target_sha,
source_sha: source_branch_sha,
target_sha: target_branch_sha,
diverged_commits_count: compute_diverged_commits_count
}
Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache)
......@@ -571,9 +622,9 @@ class MergeRequest < ActiveRecord::Base
end
def compute_diverged_commits_count
return 0 unless source_sha && target_sha
return 0 unless source_branch_sha && target_branch_sha
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
end
private :compute_diverged_commits_count
......@@ -582,13 +633,7 @@ class MergeRequest < ActiveRecord::Base
end
def pipeline
@pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project
end
def diff_refs
return nil unless diff_base_commit
[diff_base_commit, last_commit]
@pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
end
def merge_commit
......@@ -603,6 +648,36 @@ class MergeRequest < ActiveRecord::Base
merge_commit
end
def support_new_diff_notes?
diff_refs && diff_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
return unless support_new_diff_notes?
return if new_diff_refs == old_diff_refs
active_diff_notes = self.notes.diff_notes.select do |note|
note.new_diff_note? && note.active?(old_diff_refs)
end
return if active_diff_notes.empty?
paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq
service = Notes::DiffPositionUpdateService.new(
self.project,
nil,
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
paths: paths
)
active_diff_notes.each do |note|
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
end
end
def keep_around_commit
project.repository.keep_around(self.merge_commit_sha)
end
......
......@@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
state_machine :state, initial: :empty do
state :collected
......@@ -24,7 +24,7 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_diffs
after_create :reload_content, unless: :importing?
after_save :keep_around_commit
after_save :keep_around_commits, unless: :importing?
def reload_content
reload_commits
......@@ -39,9 +39,9 @@ class MergeRequestDiff < ActiveRecord::Base
if options[:ignore_whitespace_change]
@diffs_no_whitespace ||= begin
compare = Gitlab::Git::Compare.new(
self.repository.raw_repository,
self.base,
self.head,
repository.raw_repository,
self.start_commit_sha || self.target_branch_sha,
self.head_commit_sha || self.source_branch_sha,
)
compare.diffs(options)
end
......@@ -63,37 +63,39 @@ class MergeRequestDiff < ActiveRecord::Base
end
def base_commit
return nil unless self.base_commit_sha
return unless self.base_commit_sha
merge_request.target_project.commit(self.base_commit_sha)
project.commit(self.base_commit_sha)
end
def last_commit_short_sha
@last_commit_short_sha ||= last_commit.short_id
end
def start_commit
return unless self.start_commit_sha
def dump_commits(commits)
commits.map(&:to_hash)
project.commit(self.start_commit_sha)
end
def load_commits(array)
array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
end
def head_commit
return last_commit unless self.head_commit_sha
def dump_diffs(diffs)
if diffs.respond_to?(:map)
diffs.map(&:to_hash)
end
project.commit(self.head_commit_sha)
end
def load_diffs(raw, options)
if raw.respond_to?(:each)
Gitlab::Git::DiffCollection.new(raw, options)
else
Gitlab::Git::DiffCollection.new([])
end
def compare
@compare ||=
begin
# Update ref for merge request
merge_request.fetch_ref
Gitlab::Git::Compare.new(
repository.raw_repository,
self.target_branch_sha,
self.source_branch_sha
)
end
end
private
# Collect array of Git::Commit objects
# between target and source branches
def unmerged_commits
......@@ -106,6 +108,14 @@ class MergeRequestDiff < ActiveRecord::Base
commits
end
def dump_commits(commits)
commits.map(&:to_hash)
end
def load_commits(array)
array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
end
# Reload all commits related to current merge request from repo
# and save it as array of hashes in st_commits db field
def reload_commits
......@@ -120,6 +130,26 @@ class MergeRequestDiff < ActiveRecord::Base
update_columns_serialized(new_attributes)
end
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
compare.diffs(Commit.max_diff_options)
end
def dump_diffs(diffs)
if diffs.respond_to?(:map)
diffs.map(&:to_hash)
end
end
def load_diffs(raw, options)
if raw.respond_to?(:each)
Gitlab::Git::DiffCollection.new(raw, options)
else
Gitlab::Git::DiffCollection.new([])
end
end
# Reload diffs between branches related to current merge request from repo
# and save it as array of hashes in st_diffs db field
def reload_diffs
......@@ -147,59 +177,33 @@ class MergeRequestDiff < ActiveRecord::Base
new_attributes[:st_diffs] = new_diffs
base_commit_sha = self.repository.merge_base(self.head, self.base)
new_attributes[:base_commit_sha] = base_commit_sha
self.repository.keep_around(base_commit_sha)
new_attributes[:start_commit_sha] = self.target_branch_sha
new_attributes[:head_commit_sha] = self.source_branch_sha
new_attributes[:base_commit_sha] = branch_base_sha
update_columns_serialized(new_attributes)
end
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
compare.diffs(Commit.max_diff_options)
keep_around_commits
end
def repository
merge_request.target_project.repository
def project
merge_request.target_project
end
def source_sha
return head_source_sha if head_source_sha.present?
source_commit = merge_request.source_project.commit(source_branch)
source_commit.try(:sha)
def repository
project.repository
end
def target_sha
merge_request.target_sha
end
def branch_base_commit
return unless self.source_branch_sha && self.target_branch_sha
def base
self.target_sha || self.target_branch
project.merge_base_commit(self.source_branch_sha, self.target_branch_sha)
end
def head
self.source_sha
def branch_base_sha
branch_base_commit.try(:sha)
end
def compare
@compare ||=
begin
# Update ref for merge request
merge_request.fetch_ref
Gitlab::Git::Compare.new(
self.repository.raw_repository,
self.base,
self.head
)
end
end
private
#
# #save or #update_attributes providing changes on serialized attributes do a lot of
# serialization and deserialization calls resulting in bad performance.
......@@ -223,7 +227,9 @@ class MergeRequestDiff < ActiveRecord::Base
reload
end
def keep_around_commit
self.repository.keep_around(self.base_commit_sha)
def keep_around_commits
repository.keep_around(target_branch_sha)
repository.keep_around(source_branch_sha)
repository.keep_around(branch_base_sha)
end
end
......@@ -56,7 +56,7 @@ class Note < ActiveRecord::Base
scope :inc_author, ->{ includes(:author) }
scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) }
scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') }
scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
scope :with_associations, -> do
......@@ -82,7 +82,7 @@ class Note < ActiveRecord::Base
end
def grouped_diff_notes
legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
end
# Searches for notes matching the given query.
......@@ -115,6 +115,10 @@ class Note < ActiveRecord::Base
false
end
def new_diff_note?
false
end
def active?
true
end
......@@ -193,7 +197,7 @@ class Note < ActiveRecord::Base
end
def award_emoji?
award_emoji_supported? && contains_emoji_only?
can_be_award_emoji? && contains_emoji_only?
end
def emoji_awardable?
......@@ -204,7 +208,7 @@ class Note < ActiveRecord::Base
self.line_code = nil if self.line_code.blank?
end
def award_emoji_supported?
def can_be_award_emoji?
noteable.is_a?(Awardable)
end
......
......@@ -5,6 +5,7 @@ class NotificationSetting < ActiveRecord::Base
belongs_to :user
belongs_to :source, polymorphic: true
belongs_to :project, foreign_key: 'source_id'
validates :user, presence: true
validates :level, presence: true
......@@ -13,7 +14,13 @@ class NotificationSetting < ActiveRecord::Base
allow_nil: true }
scope :for_groups, -> { where(source_type: 'Namespace') }
scope :for_projects, -> { where(source_type: 'Project') }
# Exclude projects not included by the Project model's default scope (those that are
# pending delete).
#
scope :for_projects, -> do
includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil })
end
EMAIL_EVENTS = [
:new_note,
......
......@@ -162,9 +162,7 @@ class Project < ActiveRecord::Base
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
url: { protocols: %w(ssh git http https) },
if: :external_import?
validates :import_url, addressable_url: true, if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
......@@ -463,6 +461,8 @@ class Project < ActiveRecord::Base
end
def import_url=(value)
return super(value) unless Gitlab::UrlSanitizer.valid?(value)
import_url = Gitlab::UrlSanitizer.new(value)
create_or_update_import_data(credentials: import_url.credentials)
super(import_url.sanitized_url)
......
......@@ -144,7 +144,7 @@ class JiraService < IssueTrackerService
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
entity.last_commit.id
entity.diff_head_sha
end
commit_url = build_entity_url(:commit, commit_id)
......
......@@ -653,16 +653,6 @@ class Repository
end
end
def blob_for_diff(commit, diff)
blob_at(commit.id, diff.file_path)
end
def prev_blob_for_diff(commit, diff)
if commit.parent_id
blob_at(commit.parent_id, diff.old_path)
end
end
def refs_contains_sha(ref_type, sha)
args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
names = Gitlab::Popen.popen(args, path_to_repo).first
......@@ -911,7 +901,7 @@ class Repository
if line =~ /^.*:.*:\d+:/
ref, filename, startline = line.split(':')
startline = startline.to_i - index
extname = File.extname(filename)
extname = Regexp.escape(File.extname(filename))
basename = filename.sub(/#{extname}$/, '')
break
end
......
class SentNotification < ActiveRecord::Base
serialize :position, Gitlab::Diff::Position
belongs_to :project
belongs_to :noteable, polymorphic: true
belongs_to :recipient, class_name: "User"
......@@ -7,7 +9,7 @@ class SentNotification < ActiveRecord::Base
validates :reply_key, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
validates :line_code, line_code: true, allow_blank: true
validate :note_valid
after_save :keep_around_commit
......@@ -20,7 +22,7 @@ class SentNotification < ActiveRecord::Base
find_by(reply_key: reply_key)
end
def record(noteable, recipient_id, reply_key, params = {})
def record(noteable, recipient_id, reply_key, attrs = {})
return unless reply_key
noteable_id = nil
......@@ -31,7 +33,7 @@ class SentNotification < ActiveRecord::Base
noteable_id = noteable.id
end
params.reverse_merge!(
attrs.reverse_merge!(
project: noteable.project,
noteable_type: noteable.class.name,
noteable_id: noteable_id,
......@@ -40,13 +42,17 @@ class SentNotification < ActiveRecord::Base
reply_key: reply_key
)
create(params)
create(attrs)
end
def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code
def record_note(note, recipient_id, reply_key, attrs = {})
if note.diff_note?
attrs[:note_type] = note.type
attrs.merge!(note.diff_attributes)
end
record(note.noteable, recipient_id, reply_key, params)
record(note.noteable, recipient_id, reply_key, attrs)
end
end
......@@ -70,8 +76,33 @@ class SentNotification < ActiveRecord::Base
self.reply_key
end
def note_attributes
{
project: self.project,
author: self.recipient,
type: self.note_type,
noteable_type: self.noteable_type,
noteable_id: self.noteable_id,
commit_id: self.commit_id,
line_code: self.line_code,
position: self.position.to_json
}
end
def create_note(note)
Notes::CreateService.new(
self.project,
self.recipient,
self.note_attributes.merge(note: note)
).execute
end
private
def note_valid
Note.new(note_attributes.merge(note: "Test")).valid?
end
def keep_around_commit
project.repository.keep_around(self.commit_id)
end
......
......@@ -7,7 +7,7 @@ class AuditEventService
@details = {
with: @details[:with],
target_id: @author.id,
target_type: "User",
target_type: 'User',
target_details: @author.name,
}
......
......@@ -20,9 +20,11 @@ module Auth
token.issuer = registry.issuer
token.audience = AUDIENCE
token.expire_time = token_expire_at
token[:access] = names.map do |name|
{ type: 'repository', name: name, actions: %w(*) }
end
token.encoded
end
......
......@@ -3,17 +3,20 @@ require_relative 'base_service'
class CreateBranchService < BaseService
def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
if valid_branch == false
unless valid_branch
return error('Branch name is invalid')
end
repository = project.repository
existing_branch = repository.find_branch(branch_name)
if existing_branch
return error('Branch already exists')
end
new_branch = nil
if source_project != @project
repository.with_tmp_ref do |tmp_ref|
repository.fetch_ref(
......@@ -29,7 +32,6 @@ class CreateBranchService < BaseService
end
if new_branch
# GitPushService handles execution of services and hooks for branch pushes
success(new_branch)
else
error('Invalid reference name')
......@@ -39,8 +41,6 @@ class CreateBranchService < BaseService
end
def success(branch)
out = super()
out[:branch] = branch
out
super().merge(branch: branch)
end
end
......@@ -23,8 +23,6 @@ class CreateReleaseService < BaseService
end
def success(release)
out = super()
out[:release] = release
out
super().merge(release: release)
end
end
class CreateSnippetService < BaseService
def execute
if project.nil?
snippet = PersonalSnippet.new(params)
else
snippet = project.snippets.build(params)
end
snippet = if project
project.snippets.build(params)
else
PersonalSnippet.new(params)
end
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
deny_visibility_level(snippet)
......
......@@ -9,6 +9,7 @@ class CreateTagService < BaseService
message.strip! if message
new_tag = nil
begin
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError
......@@ -22,6 +23,7 @@ class CreateTagService < BaseService
CreateReleaseService.new(@project, @current_user).
execute(tag_name, release_description)
end
success.merge(tag: new_tag)
else
error("Target #{target} is invalid")
......
......@@ -5,7 +5,6 @@ class DeleteBranchService < BaseService
repository = project.repository
branch = repository.find_branch(branch_name)
# No such branch
unless branch
return error('No such branch', 404)
end
......@@ -14,18 +13,15 @@ class DeleteBranchService < BaseService
return error('Cannot remove HEAD branch', 405)
end
# Dont allow remove of protected branch
if project.protected_branch?(branch_name)
return error('Protected branch cant be removed', 405)
end
# Dont allow user to remove branch if he is not allowed to push
unless current_user.can?(:push_code, project)
return error('You dont have push access to repo', 405)
end
if repository.rm_branch(current_user, branch_name)
# GitPushService handles execution of services and hooks for branch pushes
success('Branch was removed')
else
error('Failed to remove branch')
......@@ -35,15 +31,11 @@ class DeleteBranchService < BaseService
end
def error(message, return_code = 400)
out = super(message)
out[:return_code] = return_code
out
super(message).merge(return_code: return_code)
end
def success(message)
out = super()
out[:message] = message
out
super().merge(message: message)
end
def build_push_data(branch)
......
......@@ -5,7 +5,6 @@ class DeleteTagService < BaseService
repository = project.repository
tag = repository.find_tag(tag_name)
# No such tag
unless tag
return error('No such tag', 404)
end
......@@ -26,15 +25,11 @@ class DeleteTagService < BaseService
end
def error(message, return_code = 400)
out = super(message)
out[:return_code] = return_code
out
super(message).merge(return_code: return_code)
end
def success(message)
out = super()
out[:message] = message
out
super().merge(message: message)
end
def build_push_data(tag)
......
......@@ -15,7 +15,6 @@ module Files
params[:file_content]
end
# Validate parameters
validate
# Create new branch if it different from source_branch
......@@ -26,7 +25,7 @@ module Files
if commit
success
else
error("Something went wrong. Your changes were not committed")
error('Something went wrong. Your changes were not committed')
end
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message)
......@@ -51,12 +50,12 @@ module Files
unless project.empty_repo?
unless @source_project.repository.branch_names.include?(@source_branch)
raise_error("You can only create or edit files when you are on a branch")
raise_error('You can only create or edit files when you are on a branch')
end
if different_branch?
if repository.branch_names.include?(@target_branch)
raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes")
raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
end
end
end
......
......@@ -29,7 +29,7 @@ module Files
blob = repository.blob_at_branch(@source_branch, @file_path)
if blob
raise_error("Your changes could not be committed because a file with the same name already exists")
raise_error('Your changes could not be committed because a file with the same name already exists')
end
end
end
......
......@@ -26,6 +26,7 @@ class GitTagPushService < BaseService
unless Gitlab::Git.blank_ref?(params[:newrev])
tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name)
if tag && tag.target == params[:newrev]
commit = project.commit(tag.target)
commits = [commit].compact
......
......@@ -9,6 +9,7 @@ module Issues
end
issues = Issue.where(id: issues_ids)
issues.each do |issue|
next unless can?(current_user, :update_issue, issue)
......
......@@ -34,7 +34,7 @@ module MergeRequests
committer: committer
}
commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
commit_id = repository.merge(current_user, merge_request.diff_head_sha, merge_request.target_branch, options)
merge_request.update(merge_commit_sha: commit_id)
rescue GitHooksService::PreReceiveError => e
merge_request.update(merge_error: e.message)
......
......@@ -12,7 +12,7 @@ module MergeRequests
merge_request.merge_when_build_succeeds = true
merge_request.merge_user = @current_user
SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit)
SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
end
merge_request.save
......
......@@ -20,6 +20,7 @@ module MergeRequests
return unless merge_request.target_branch == project.default_branch
closed_issues = merge_request.closes_issues(current_user)
closed_issues.each do |issue|
if can?(current_user, :update_issue, issue)
Issues::CloseService.new(project, current_user, {}).execute(issue, commit: merge_request)
......
......@@ -34,10 +34,10 @@ module MergeRequests
def close_merge_requests
commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:last_commit)
merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
commit_ids.include?(merge_request.last_commit.id)
commit_ids.include?(merge_request.diff_head_sha)
end
merge_requests.uniq.select(&:source_project).each do |merge_request|
......@@ -60,7 +60,7 @@ module MergeRequests
merge_requests.each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_code
merge_request.reload_diff
merge_request.mark_as_unchecked
else
mr_commit_ids = merge_request.commits.map(&:id)
......@@ -68,7 +68,7 @@ module MergeRequests
matches = mr_commit_ids & push_commit_ids
if matches.any?
merge_request.reload_code
merge_request.reload_diff
merge_request.mark_as_unchecked
else
merge_request.mark_as_unchecked
......@@ -94,12 +94,10 @@ module MergeRequests
merge_request = merge_requests_for_source_branch.first
return unless merge_request
last_commit = merge_request.last_commit
begin
# Since any number of commits could have been made to the restored branch,
# find the common root to see what has been added.
common_ref = @project.repository.merge_base(last_commit.id, @newrev)
common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev)
# If the a commit no longer exists in this repo, gitlab_git throws
# a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
@commits = @project.repository.commits_between(common_ref, @newrev) if common_ref
......
......@@ -6,7 +6,7 @@ module MergeRequests
create_note(merge_request)
notification_service.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
merge_request.reload_code
merge_request.reload_diff
merge_request.mark_as_unchecked
end
......
module Notes
class DiffPositionUpdateService < BaseService
def execute(note)
new_position = tracer.trace(note.position)
# Don't update the position if the type doesn't match, since that means
# the diff line commented on was changed, and the comment is now outdated
old_position = note.position
if new_position &&
new_position != old_position &&
new_position.type == old_position.type
note.position = new_position
end
note
end
private
def tracer
@tracer ||= Gitlab::Diff::PositionTracer.new(
repository: project.repository,
old_diff_refs: params[:old_diff_refs],
new_diff_refs: params[:new_diff_refs],
paths: params[:paths]
)
end
end
end
......@@ -153,6 +153,7 @@ class NotificationService
else
mentioned_users
end
recipients = recipients.concat(participants)
# Merge project watchers
......@@ -176,6 +177,7 @@ class NotificationService
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
......
......@@ -27,7 +27,7 @@ module Projects
GitlabShellOneShotWorker.perform_async(:gc, @project.repository_storage_path, @project.path_with_namespace)
ensure
Gitlab::Metrics.measure(:reset_pushes_since_gc) do
@project.update_column(:pushes_since_gc, 0)
update_pushes_since_gc(0)
end
end
......@@ -37,12 +37,18 @@ module Projects
def increment!
Gitlab::Metrics.measure(:increment_pushes_since_gc) do
@project.increment!(:pushes_since_gc)
update_pushes_since_gc(@project.pushes_since_gc + 1)
end
end
private
def update_pushes_since_gc(new_value)
if Gitlab::ExclusiveLease.new("project_housekeeping:update_pushes_since_gc:#{project.id}", timeout: 60).try_obtain
@project.update_column(:pushes_since_gc, new_value)
end
end
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
......
......@@ -38,6 +38,8 @@ module Projects
end
def cleanup_and_notify
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
FileUtils.rm_rf(@shared.export_path)
notify_error
......@@ -45,6 +47,8 @@ module Projects
end
def notify_success
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
notification_service.project_exported(@project, @current_user)
end
......
......@@ -3,10 +3,11 @@ module Projects
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != project.visibility_level
unless can?(current_user, :change_visibility_level, project) &&
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
return project
end
......
......@@ -82,7 +82,7 @@ class SystemNoteService
end
body << ' ' << 'label'.pluralize(labels_count)
body = "#{body.capitalize}"
body = body.capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
......@@ -125,7 +125,7 @@ class SystemNoteService
# Returns the created Note object
def self.change_status(noteable, project, author, status, source)
body = "Status changed to #{status}"
body += " by #{source.gfm_reference(project)}" if source
body << " by #{source.gfm_reference(project)}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
......@@ -139,7 +139,7 @@ class SystemNoteService
# Called when 'merge when build succeeds' is canceled
def self.cancel_merge_when_build_succeeds(noteable, project, author)
body = "Canceled the automatic merge"
body = 'Canceled the automatic merge'
create_note(noteable: noteable, project: project, author: author, note: body)
end
......@@ -236,6 +236,7 @@ class SystemNoteService
else
'deleted'
end
body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
......
......@@ -21,8 +21,6 @@ class UpdateReleaseService < BaseService
end
def success(release)
out = super()
out[:release] = release
out
super().merge(release: release)
end
end
......@@ -9,6 +9,7 @@ class UpdateSnippetService < BaseService
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
......
# AddressableUrlValidator
#
# Custom validator for URLs. This is a stricter version of UrlValidator - it also checks
# for using the right protocol, but it actually parses the URL checking for any syntax errors.
# The regex is also different from `URI` as we use `Addressable::URI` here.
#
# By default, only URLs for http, https, ssh, and git protocols will be considered valid.
# Provide a `:protocols` option to configure accepted protocols.
#
# Example:
#
# class User < ActiveRecord::Base
# validates :personal_url, addressable_url: true
#
# validates :ftp_url, addressable_url: { protocols: %w(ftp) }
#
# validates :git_url, addressable_url: { protocols: %w(http https ssh git) }
# end
#
class AddressableUrlValidator < ActiveModel::EachValidator
DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }
def validate_each(record, attribute, value)
unless valid_url?(value)
record.errors.add(attribute, "must be a valid URL")
end
end
private
def valid_url?(value)
return false unless value
valid_protocol?(value) && valid_uri?(value)
end
def valid_uri?(value)
Gitlab::UrlSanitizer.valid?(value)
end
def valid_protocol?(value)
options = DEFAULT_OPTIONS.merge(self.options)
value =~ /\A#{URI.regexp(options[:protocols])}\z/
end
end
- css_class = '' unless local_assigns[:css_class]
- css_class += ' no-description' if group.description.blank?
%li.group-row{ class: css_class }
.controls.hidden-xs
= link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm'
= link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove'
%li.group-row.group-admin{ class: css_class }
.group-avatar
= image_tag group_icon(group), class: 'avatar hidden-xs'
.group-details
.title
= link_to [:admin, group], class: 'group-name' do
= group.name
.group-stats
%span>= pluralize(number_with_delimiter(group.projects.count), 'project')
,
%span= pluralize(number_with_delimiter(group.users.count), 'member')
.stats
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
%span
= icon('users')
= number_with_delimiter(group.users.count)
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
= image_tag group_icon(group), class: 'avatar s40 hidden-xs'
.title
= link_to [:admin, group], class: 'group-name' do
= group.name
- if group.description.present?
.description
= markdown(group.description, pipeline: :description)
- if group.description.present?
.description
= markdown(group.description, pipeline: :description)
.group-controls.hidden-xs
= link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
......@@ -3,41 +3,32 @@
= render "admin/dashboard/head"
%div{ class: container_class }
%h3.page-title
Groups (#{number_with_delimiter(@groups.total_count)})
%p.light
Group allows you to keep projects organized.
Use groups for uniting related projects.
.top-area
.nav-search
= form_tag admin_groups_path, method: :get, class: 'form-inline' do
.prepend-top-default.append-bottom-default
= form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f|
= hidden_field_tag :sort, @sort
= text_field_tag :name, params[:name], class: "form-control"
= button_tag "Search", class: "btn submit btn-primary"
.nav-controls
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
%b.caret
%ul.dropdown-menu
%li
= link_to admin_groups_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to admin_groups_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
= link_to admin_groups_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
= link_to 'New Group', new_admin_group_path, class: "btn btn-new"
.search-holder
- project_name = params[:name].present? ? params[:name] : nil
.search-field-holder
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name'
= icon("search", class: "search-icon")
.dropdown
- toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right
%li.dropdown-header
Sort by
%li
= link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do
= sort_title_recently_created
= link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do
= sort_title_oldest_created
= link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
= sort_title_oldest_updated
= link_to new_admin_group_path, class: "btn btn-new" do
New Group
%ul.content-list
= render @groups
......
- @no_container = true
- page_title "Projects"
= render 'shared/show_aside'
- params[:visibility_level] ||= []
= render "admin/dashboard/head"
%div{ class: container_class }
.row.prepend-top-default
%aside.col-md-3
.panel.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
.form-group
= label_tag :name, 'Name:'
= text_field_tag :name, params[:name], class: "form-control"
.top-area
.prepend-top-default
= form_tag admin_namespaces_projects_path, method: :get do |f|
.search-holder
.search-field-holder
= search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
- if params[:visibility_level].present?
= hidden_field_tag 'visibility_level', params[:visibility_level]
- if params[:sort].present?
= hidden_field_tag 'sort', params[:sort]
- if params[:personal].present?
= hidden_field_tag 'visibility_level', 'true'
- if params[:archived].present?
= hidden_field_tag 'archived', 'true'
= icon("search", class: "search-icon")
.dropdown
- toggle_text = 'Search for Namespace'
- if params[:namespace_id].present?
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.path}"
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
= dropdown_content
= dropdown_loading
= button_tag "Search", class: "btn btn-primary btn-search"
%ul.nav-links
- opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
= nav_link(opts) do
= link_to admin_namespaces_projects_path do
All
.form-group
= label_tag :namespace_id, "Namespace"
= namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large'
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
= link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
= link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
= link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
.form-group
%strong Activity
.checkbox
= label_tag :with_push do
= check_box_tag :with_push, 1, params[:with_push]
%span Projects with push events
.checkbox
= label_tag :abandoned do
= check_box_tag :abandoned, 1, params[:abandoned]
%span No activity over 6 month
.checkbox
= label_tag :with_archived do
= check_box_tag :with_archived, 1, params[:with_archived]
%span Show archived projects
.nav-controls
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'btn btn-new' do
New Project
%fieldset
%strong Visibility level:
.visibility-levels
- Project.visibility_levels.each do |label, level|
.checkbox
%label
= check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s)
%span.descr
= visibility_level_icon(level)
= label
%fieldset
%strong Problems
.checkbox
= label_tag :last_repository_check_failed do
= check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed]
%span Last repository check failed
.projects-list-holder
- if @projects.any?
%ul.projects-list.content-list
- @projects.each_with_index do |project|
%li.project-row
.controls.pull-right
- if project.archived
%span.label.label-warning archived
%span.label.label-gray
= repository_size(project)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
%span.project-name.filter-title
= project.name
= hidden_field_tag :sort, params[:sort]
= button_tag "Search", class: "btn submit btn-primary"
= link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
- if project.description.present?
.description
= markdown(project.description, pipeline: :description)
%section.col-md-9
.panel.panel-default
.panel-heading
Projects (#{@projects.total_count})
.controls
.dropdown.inline
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
%b.caret
%ul.dropdown-menu
%li
= link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
= link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do
= sort_title_recently_updated
= link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
= link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do
= sort_title_largest_repo
= link_to 'New Project', new_project_path, class: "btn btn-sm btn-success"
%ul.well-list
- @projects.each do |project|
%li
.list-item-name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
.pull-right
- if project.archived
%span.label.label-warning archived
%span.label.label-gray
= repository_size(project)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove"
- if @projects.blank?
.nothing-here-block 0 projects matches
= paginate @projects, theme: "gitlab"
= paginate @projects, theme: 'gitlab'
- else
.nothing-here-block No projects found
......@@ -99,7 +99,13 @@
.form-group
= f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10
= namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large'
.dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
= dropdown_content
= dropdown_loading
.form-group
.col-sm-offset-2.col-sm-10
......
%li.user-row
.user-avatar
= image_tag avatar_icon(user), class: "avatar", alt: ''
.user-details
.user-name
= link_to user.name, [:admin, user]
- if user.blocked?
%span.label.label-danger blocked
- if user.admin?
%span.label.label-success Admin
- if user.external?
%span.label.label-default External
- if user == current_user
%span It's you!
.user-email
= mail_to user.email, user.email
.controls.pull-right
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
- unless user == current_user
.dropdown.inline
%a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li.dropdown-header
Settings
%li
- if user.ldap_blocked?
%span.small Cannot unblock LDAP blocked users
- elsif user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put
- else
= link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put
- if user.access_locked?
%li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- if user.can_be_removed?
%li.divider
%li
= link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
class: 'btn btn-remove btn-block',
method: :delete
- @no_container = true
- page_title "Users"
= render 'shared/show_aside'
= render "admin/dashboard/head"
%div{ class: container_class }
.admin-filter
%ul.nav-links
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
%small.badge= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
%small.badge= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
%li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
= link_to admin_users_path(filter: 'external') do
External
%small.badge= number_with_delimiter(User.external.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.badge= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
.top-area
.prepend-top-default
= form_tag admin_users_path, method: :get do
- if params[:filter].present?
= hidden_field_tag "filter", h(params[:filter])
.search-holder
.search-field-holder
= search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
= icon("search", class: "search-icon")
.dropdown
- toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right
%li.dropdown-header
Sort by
%li
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: 'btn btn-new btn-search'
.row-content-block.second-block
.pull-right
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_name
%b.caret
%ul.dropdown-menu
%li
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
.nav-block
%ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
.fade-left
= nav_link(html_options: { class: ('active' unless params[:filter]) }) do
= link_to admin_users_path do
Active
%small.badge= number_with_delimiter(User.active.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
%small.badge= number_with_delimiter(User.admins.count)
= nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count)
= nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
%small.badge= number_with_delimiter(User.external.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.badge= number_with_delimiter(User.blocked.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
.fade-right
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
%ul.users-list.content-list
- if @users.empty?
%li
.nothing-here-block No users found.
- else
= render partial: 'admin/users/user', collection: @users
.panel.panel-default
%ul.well-list
- @users.each do |user|
%li
.list-item-name
- if user.blocked?
= icon("lock", class: "cred")
- else
= icon("user", class: "cgreen")
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
- if user.external?
%strong.cred (External)
- if user == current_user
%span.cred It's you!
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
.pull-right
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- unless user == current_user
- if user.ldap_blocked?
= link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
%i.fa.fa-lock
Unblock
- elsif user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
= link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
- if user.access_locked?
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- if user.can_be_removed?
= link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
= paginate @users, theme: "gitlab"
- if @note.legacy_diff_note?
- if @note.diff_note?
%p.details
New comment on diff for
= link_to @note.diff_file_path, @target_url
= link_to @note.diff_file.file_path, @target_url
\:
= render 'note_message'
......@@ -72,12 +72,11 @@
The diff for this file was not included because it is too large.
- else
%hr
- diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last
- blob = @message.project.repository.blob_for_diff(diff_commit, diff_file)
- blob = diff_file.blob
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- diff_file.highlighted_diff_lines.each do |line|
= render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true}
= render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
- else
No preview for this file type
%br
......@@ -7,7 +7,9 @@
%span You pushed to
= link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
%strong= event.ref_name
branch
- if @project && event.project != @project
%span at
%strong= link_to_project event.project
#{time_ago_with_tooltip(event.created_at)}
.pull-right
......
......@@ -4,12 +4,10 @@
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
Edit File
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
= icon('eye')
= editing_preview_title(@blob.name)
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
......
......@@ -67,4 +67,4 @@
= render "sidebar"
:javascript
new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
......@@ -7,7 +7,7 @@
= ci_icon_for_status(status)
- if stage
&nbsp;
= stage.titleize.pluralize
= stage.titleize
= render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
= render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
%tr
......
......@@ -7,8 +7,7 @@
= render "ci_menu"
- else
%div.block-connector
= render "projects/diffs/diffs", diffs: @diffs, project: @project,
diff_refs: @diff_refs
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
......
......@@ -2,7 +2,7 @@
- if diff_view == 'parallel'
- fluid_layout true
- diff_files = safe_diff_files(diffs, diff_refs)
- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository)
.content-block.oneline-block.files-changed
.inline-parallel-buttons
......@@ -24,7 +24,7 @@
.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, @project))}}
- diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file)
- blob = project.repository.blob_for_diff(diff_commit, diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- blob.load_all_data!(project.repository) unless blob.only_display_raw?
......
.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)}
.file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- if diff_file.diff.submodule?
%span
= icon('archive fw')
%span
= submodule_link(blob, @commit.id, project.repository)
- else
= blob_icon blob.mode, blob.name
= link_to "#diff-#{i}" do
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
= old_path
&rarr;
= new_path
- else
%span
= diff_file.new_path
- if diff_file.deleted_file
deleted
- if diff_file.mode_changed?
%small
= "#{diff_file.diff.a_mode}#{diff_file.diff.b_mode}"
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{i}"
- unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
......@@ -42,17 +21,23 @@
- return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif blob_text_viewable?(blob)
- if diff_view == 'parallel'
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- else
= render "projects/diffs/text_file", diff_file: diff_file, index: i
- elsif blob.only_display_raw?
.nothing-here-block This file is too large to display.
- elsif blob_text_viewable?(blob)
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.diff_lines.length > 0
- if diff_view == 'parallel'
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- else
= render "projects/diffs/text_file", diff_file: diff_file, index: i
- else
- if diff_file.mode_changed?
.nothing-here-block File mode changed
- elsif diff_file.renamed_file
.nothing-here-block File moved
- elsif blob.image?
- 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, diff_refs: diff_refs
- old_blob = diff_file.old_blob(diff_commit)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i
- else
.nothing-here-block No preview for this file type
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
%span
= submodule_link(blob, diff_commit.id, project.repository)
- else
= conditional_link_to url.present?, url do
= blob_icon diff_file.b_mode, diff_file.file_path
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong
= old_path
&rarr;
%strong
= new_path
- else
%strong
= diff_file.new_path
- if diff_file.deleted_file
deleted
- if diff_file.mode_changed?
%small
= "#{diff_file.a_mode}#{diff_file.b_mode}"
- 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(diff_file.new_ref, diff.new_path))
// diff_refs will be nil for orphaned commits (e.g. first commit in repo)
- if diff_refs
- 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_file.old_ref
- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
......@@ -16,7 +15,7 @@
%div.two-up.view
%span.wrap
.frame.deleted
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))}
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))}
%img{src: old_file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
......@@ -28,7 +27,7 @@
%span.meta-height
%span.wrap
.frame.added
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))}
%a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))}
%img{src: file_raw_path}
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
......
- plain = local_assigns.fetch(:plain, false)
- line_code = diff_file.line_code(line)
- position = diff_file.position(line)
- type = line.type
- line_data = @diff_notes_disabled ? {} : { data: { discussion_id: discussion_id(line_code) } }
%tr.line_holder{ line_data, id: line_code, class: type }
%tr.line_holder{ id: line_code, class: type }
- case type
- when 'match'
= render "projects/diffs/match_line", { line: line.text,
......@@ -10,16 +12,16 @@
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
%td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
- link_text = type == "new" ? " " : line.old_pos
- if defined?(plain) && plain
- if plain
= link_text
- else
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if defined?(plain) && plain
- if plain
= link_text
- else
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content{ class: ['noteable_line', type], data: { line_code: line_code, line_type: type } }= diff_line_content(line.text, type)
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, position, type) unless plain) }= diff_line_content(line.text, type)
/ Side-by-side diff view
%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: note_text_file_data }
%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
%table
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
......@@ -13,26 +13,24 @@
%td.new_line.diff-line-num.empty-cell
%td.line_content.parallel.match= left[:text]
- else
%td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' unless left[:number]}", data: { linenumber: left[:number] }}
%td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])], data: { linenumber: left[:number] }}
%a{href: "##{left[:line_code]}" }= raw(left[:number])
%td.line_content{class: "parallel noteable_line #{left[:type]} #{'empty-cell' if left[:text].empty?}", data: note_line_parallel_data(left[:line_code], left[:type]) }= diff_line_content(left[:text])
%td.line_content.parallel.noteable_line{class: [left[:type], ('empty-cell' if left[:text].empty?)], data: diff_view_line_data(left[:line_code], left[:position], left[:type])}= diff_line_content(left[:text])
- if right[:type] == 'new'
- new_line_class = 'new'
- new_line_type = 'new'
- new_line_code = right[:line_code]
- new_position = right[:position]
- else
- new_line_class = nil
- new_line_type = nil
- new_line_code = left[:line_code]
- new_position = left[:position]
%td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' unless right[:number]}", data: { linenumber: right[:number] } }
%td.new_line.diff-line-num{id: new_line_code, class: [new_line_type, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }}
%a{href: "##{new_line_code}" }= raw(right[:number])
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{'empty-cell' if right[:text].empty?}", data: note_line_parallel_data(new_line_code, new_line_class) }= diff_line_content(right[:text])
%td.line_content.parallel.noteable_line{class: [new_line_type, ('empty-cell' if right[:text].empty?)], data: diff_view_line_data(new_line_code, new_position, new_line_type)}= diff_line_content(right[:text])
- unless @diff_notes_disabled
- notes_left, notes_right = organize_comments(left, right)
- if notes_left.present? || notes_right.present?
= render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right
- if diff_file.diff.diff.blank? && diff_file.mode_changed?
.file-mode-changed
File mode changed
......@@ -3,23 +3,18 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
%table.text-file.code.js-syntax-highlight{ data: note_text_file_data, class: too_big ? 'hide' : '' }
%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- last_line = 0
- diff_file.highlighted_diff_lines.each_with_index do |line, index|
- line_code = generate_line_code(diff_file.file_path, line)
- diff_file.highlighted_diff_lines.each do |line|
- last_line = line.new_pos
= render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code}
= render "projects/diffs/line", line: line, diff_file: diff_file
- unless @diff_notes_disabled
- diff_notes = @grouped_diff_notes[line_code]
- line_code = diff_file.line_code(line)
- diff_notes = @grouped_diff_notes[line_code] if line_code
- if diff_notes
= render "projects/notes/diff_notes_with_reply", notes: diff_notes
- if last_line > 0
= render "projects/diffs/match_line", { line: "",
line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file }
- if diff_file.diff.blank? && diff_file.mode_changed?
.file-mode-changed
File mode changed
......@@ -7,7 +7,7 @@
CI build
= ci_label_for_status(status)
for
- commit = @merge_request.last_commit
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage
......@@ -24,7 +24,7 @@
CI build
= ci_label_for_status(status)
for
- commit = @merge_request.last_commit
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
%span.ci-coverage
......@@ -33,12 +33,12 @@
.ci_widget
= icon("spinner spin")
Checking CI status for #{@merge_request.last_commit_short_sha}&hellip;
Checking CI status for #{@merge_request.diff_head_commit.short_id}&hellip;
.ci_widget.ci-not_found{style: "display:none"}
= icon("times-circle")
Could not find CI status for #{@merge_request.last_commit_short_sha}.
Could not find CI status for #{@merge_request.diff_head_commit.short_id}.
.ci_widget.ci-error{style: "display:none"}
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
\ No newline at end of file
Could not connect to the CI server. Please check your settings and try again.
......@@ -2,7 +2,7 @@
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
= hidden_field_tag :sha, @merge_request.source_sha
= hidden_field_tag :sha, @merge_request.diff_head_sha
.accept-merge-holder.clearfix.js-toggle-container
.clearfix
.accept-action
......
......@@ -16,7 +16,7 @@
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
- if remove_source_branch_button
= link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.source_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= icon('times')
Remove Source Branch When Merged
......
......@@ -4,5 +4,4 @@
%td.notes_content
%ul.notes{ data: { discussion_id: note.discussion_id } }
= render partial: "projects/notes/note", collection: notes, as: :note
.discussion-reply-holder
= link_to_reply_discussion(note)
= link_to_reply_discussion(note)
......@@ -8,8 +8,7 @@
%ul.notes{ data: { discussion_id: note_left.discussion_id } }
= render partial: "projects/notes/note", collection: notes_left, as: :note
.discussion-reply-holder
= link_to_reply_discussion(note_left, 'old')
= link_to_reply_discussion(note_left, 'old')
- else
%td.notes_line.old= ""
%td.notes_content.parallel.old= ""
......@@ -20,8 +19,7 @@
%ul.notes{ data: { discussion_id: note_right.discussion_id } }
= render partial: "projects/notes/note", collection: notes_right, as: :note
.discussion-reply-holder
= link_to_reply_discussion(note_right, 'new')
= link_to_reply_discussion(note_right, 'new')
- else
%td.notes_line.new= ""
%td.notes_content.parallel.new= ""
......@@ -7,6 +7,7 @@
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
= f.hidden_field :type
= f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
......
- note = discussion_notes.first
- diff = note.diff
- return unless diff
- diff_file = note.diff_file
- return unless diff_file
- blob = note.blob
.diff-file.file-holder
.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note)
.diff-file
.diff-header
%span
- if diff.deleted_file
= diff.old_path
- else
= diff.new_path
- if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
%span.file-mode= "#{diff.a_mode}#{diff.b_mode}"
.diff-content.code.js-syntax-highlight
%table
- note.truncated_diff_lines.each do |line|
- type = line.type
- line_code = generate_line_code(note.diff_file_path, line)
%tr.line_holder{ id: line_code, class: "#{type}" }
- if type == "match"
%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{ data: { linenumber: type == "new" ? "&nbsp;".html_safe : line.old_pos } }
%td.new_line.diff-line-num{ data: { linenumber: type == "old" ? "&nbsp;".html_safe : line.new_pos } }
%td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type)
= render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
- if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
- if note.for_line?(line)
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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