summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee63
-rw-r--r--app/assets/javascripts/importer_status.js.coffee39
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee7
-rw-r--r--app/assets/javascripts/notes.js.coffee36
-rw-r--r--app/assets/javascripts/subscription.js.coffee2
-rw-r--r--app/assets/javascripts/users_select.js.coffee3
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/mobile.scss7
-rw-r--r--app/assets/stylesheets/framework/timeline.scss3
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss38
-rw-r--r--app/assets/stylesheets/pages/import.scss21
-rw-r--r--app/assets/stylesheets/pages/issuable.scss57
-rw-r--r--app/assets/stylesheets/pages/issues.scss40
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss27
-rw-r--r--app/assets/stylesheets/print.scss44
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/admin/projects_controller.rb12
-rw-r--r--app/controllers/application_controller.rb33
-rw-r--r--app/controllers/autocomplete_controller.rb9
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/help_controller.rb1
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/users_controller.rb23
-rw-r--r--app/helpers/diff_helper.rb3
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/selects_helper.rb16
-rw-r--r--app/mailers/repository_check_mailer.rb14
-rw-r--r--app/models/ability.rb29
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/ci/build.rb18
-rw-r--r--app/models/external_issue.rb6
-rw-r--r--app/models/oauth_access_token.rb19
-rw-r--r--app/models/project.rb33
-rw-r--r--app/models/project_import_data.rb14
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/services/projects/participants_service.rb43
-rw-r--r--app/services/projects/transfer_service.rb5
-rw-r--r--app/views/admin/application_settings/_form.html.haml23
-rw-r--r--app/views/admin/logs/show.html.haml3
-rw-r--r--app/views/admin/projects/index.html.haml10
-rw-r--r--app/views/admin/projects/show.html.haml36
-rw-r--r--app/views/devise/shared/_signup_box.html.haml9
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/help/ui.html.haml12
-rw-r--r--app/views/import/base/create.js.haml4
-rw-r--r--app/views/import/bitbucket/status.html.haml20
-rw-r--r--app/views/import/fogbugz/status.html.haml15
-rw-r--r--app/views/import/github/status.html.haml15
-rw-r--r--app/views/import/gitlab/status.html.haml15
-rw-r--r--app/views/import/gitorious/status.html.haml15
-rw-r--r--app/views/import/google_code/status.html.haml19
-rw-r--r--app/views/projects/_builds_settings.html.haml3
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/blob/diff.html.haml12
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_line.html.haml20
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml127
-rw-r--r--app/views/projects/issues/update.js.haml3
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml32
-rw-r--r--app/views/projects/merge_requests/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/dropdowns/_branch.html.haml5
-rw-r--r--app/views/projects/merge_requests/dropdowns/_project.html.haml5
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml59
-rw-r--r--app/views/projects/merge_requests/update.js.haml3
-rw-r--r--app/views/projects/merge_requests/update_branches.html.haml8
-rw-r--r--app/views/projects/notes/_discussion.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml9
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/notes/discussions/_active.html.haml6
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml9
-rw-r--r--app/views/projects/notes/discussions/_diff.html.haml8
-rw-r--r--app/views/projects/notes/discussions/_outdated.html.haml6
-rw-r--r--app/views/repository_check_mailer/notify.html.haml5
-rw-r--r--app/views/repository_check_mailer/notify.text.haml3
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/workers/admin_email_worker.rb12
-rw-r--r--app/workers/repository_check/batch_worker.rb63
-rw-r--r--app/workers/repository_check/clear_worker.rb17
-rw-r--r--app/workers/repository_check/single_repository_worker.rb36
89 files changed, 981 insertions, 431 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 4718bcf7a1e..61e3f811e73 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -2,6 +2,8 @@
window.GitLab ?= {}
GitLab.GfmAutoComplete =
+ dataLoading: false
+
dataSource: ''
# Emoji
@@ -17,17 +19,41 @@ GitLab.GfmAutoComplete =
template: '<li><small>${id}</small> ${title}</li>'
# Add GFM auto-completion to all input fields, that accept GFM input.
- setup: ->
- input = $('.js-gfm-input')
+ setup: (wrap) ->
+ @input = $('.js-gfm-input')
+
+ # destroy previous instances
+ @destroyAtWho()
+
+ # set up instances
+ @setupAtWho()
+
+ if @dataSource
+ if !@dataLoading
+ @dataLoading = true
+ # We should wait until initializations are done
+ # and only trigger the last .setup since
+ # The previous .dataSource belongs to the previous issuable
+ # and the last one will have the **proper** .dataSource property
+ # TODO: Make this a singleton and turn off events when moving to another page
+ setTimeout( =>
+ fetch = @fetchData(@dataSource)
+ fetch.done (data) =>
+ @dataLoading = false
+ @loadData(data)
+ , 1000)
+
+
+ setupAtWho: ->
# Emoji
- input.atwho
+ @input.atwho
at: ':'
displayTpl: @Emoji.template
insertTpl: ':${name}:'
# Team Members
- input.atwho
+ @input.atwho
at: '@'
displayTpl: @Members.template
insertTpl: '${atwho-at}${username}'
@@ -42,7 +68,7 @@ GitLab.GfmAutoComplete =
title: sanitize(title)
search: sanitize("#{m.username} #{m.name}")
- input.atwho
+ @input.atwho
at: '#'
alias: 'issues'
searchKey: 'search'
@@ -55,7 +81,7 @@ GitLab.GfmAutoComplete =
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
- input.atwho
+ @input.atwho
at: '!'
alias: 'mergerequests'
searchKey: 'search'
@@ -68,13 +94,18 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
- if @dataSource
- $.getJSON(@dataSource).done (data) ->
- # load members
- input.atwho 'load', '@', data.members
- # load issues
- input.atwho 'load', 'issues', data.issues
- # load merge requests
- input.atwho 'load', 'mergerequests', data.mergerequests
- # load emojis
- input.atwho 'load', ':', data.emojis
+ destroyAtWho: ->
+ @input.atwho('destroy')
+
+ fetchData: (dataSource) ->
+ $.getJSON(dataSource)
+
+ loadData: (data) ->
+ # load members
+ @input.atwho 'load', '@', data.members
+ # load issues
+ @input.atwho 'load', 'issues', data.issues
+ # load merge requests
+ @input.atwho 'load', 'mergerequests', data.mergerequests
+ # load emojis
+ @input.atwho 'load', ':', data.emojis
diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee
index be8d225e73b..b0edc895649 100644
--- a/app/assets/javascripts/importer_status.js.coffee
+++ b/app/assets/javascripts/importer_status.js.coffee
@@ -4,18 +4,33 @@ class @ImporterStatus
this.setAutoUpdate()
initStatusPage: ->
- $(".js-add-to-import").click (event) =>
- new_namespace = null
- tr = $(event.currentTarget).closest("tr")
- id = tr.attr("id").replace("repo_", "")
- if tr.find(".import-target input").length > 0
- new_namespace = tr.find(".import-target input").prop("value")
- tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name"))
- $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script'
-
- $(".js-import-all").click (event) =>
- $(".js-add-to-import").each ->
- $(this).click()
+ $('.js-add-to-import')
+ .off 'click'
+ .on 'click', (e) =>
+ new_namespace = null
+ $btn = $(e.currentTarget)
+ $tr = $btn.closest('tr')
+ id = $tr.attr('id').replace('repo_', '')
+ if $tr.find('.import-target input').length > 0
+ new_namespace = $tr.find('.import-target input').prop('value')
+ $tr.find('.import-target').empty().append("#{new_namespace} / #{$tr.find('.import-target').data('project_name')}")
+
+ $btn
+ .disable()
+ .addClass 'is-loading'
+
+ $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script'
+
+ $('.js-import-all')
+ .off 'click'
+ .on 'click', (e) ->
+ $btn = $(@)
+ $btn
+ .disable()
+ .addClass 'is-loading'
+
+ $('.js-add-to-import').each ->
+ $(this).trigger('click')
setAutoUpdate: ->
setInterval (=>
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 1ab6e5114bc..e8613cab72b 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -183,9 +183,10 @@ class @MergeRequestTabs
else
$diffLine = $('td', $diffLine)
- $diffLine.addClass 'hll'
- diffLineTop = $diffLine.offset().top
- navBarHeight = $('.navbar-gitlab').outerHeight()
+ if $diffLine.length
+ $diffLine.addClass 'hll'
+ diffLineTop = $diffLine.offset().top
+ navBarHeight = $('.navbar-gitlab').outerHeight()
loadBuilds: (source) ->
return if @buildsLoaded
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index fa91baa07c0..82e210fed7d 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -75,6 +75,9 @@ class @Notes
# when issue status changes, we need to refresh data
$(document).on "issuable:change", @refresh
+ # when a key is clicked on the notes
+ $(document).on "keydown", ".js-note-text", @keydownNoteText
+
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
@@ -92,10 +95,19 @@ class @Notes
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard"
+ $(document).off "keydown", ".js-note-text"
$('.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 #aka the up key
+ myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
+ if myLastNote.length
+ myLastNoteEditBtn = myLastNote.find('.js-note-edit')
+ myLastNoteEditBtn.trigger('click', [true, myLastNote])
+
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
@@ -343,7 +355,7 @@ class @Notes
Adds a hidden div with the original content of the note to fill the edit note form with
if the user cancels
###
- showEditForm: (e) ->
+ showEditForm: (e, scrollTo, myLastNote) ->
e.preventDefault()
note = $(this).closest(".note")
note.addClass "is-editting"
@@ -354,9 +366,27 @@ class @Notes
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
- new GLForm form
+ done = ($noteText) ->
+ # Neat little trick to put the cursor at the end
+ noteTextVal = $noteText.val()
+ $noteText.val('').val(noteTextVal);
- form.find(".js-note-text").focus()
+ new GLForm form
+ if scrollTo? and myLastNote?
+ # scroll to the bottom
+ # so the open of the last element doesn't make a jump
+ $('html, body').scrollTop($(document).height());
+ $('html, body').animate({
+ scrollTop: myLastNote.offset().top - 150
+ }, 500, ->
+ $noteText = form.find(".js-note-text")
+ $noteText.focus()
+ done($noteText)
+ );
+ else
+ $noteText = form.find('.js-note-text')
+ $noteText.focus()
+ done($noteText)
###
Called in response to clicking the edit note link
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index e4b7a3172ec..1a430f3aa47 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -2,7 +2,7 @@ class @Subscription
constructor: (container) ->
$container = $(container)
@url = $container.attr('data-url')
- @subscribe_button = $container.find('.subscribe-button')
+ @subscribe_button = $container.find('.js-subscribe-button')
@subscription_status = $container.find('.subscription-status')
@subscribe_button.unbind('click').click(@toggleSubscription)
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index eee9b6e690e..a7e934936e9 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -12,6 +12,7 @@ class @UsersSelect
showNullUser = $dropdown.data('null-user')
showAnyUser = $dropdown.data('any-user')
firstUser = $dropdown.data('first-user')
+ @authorId = $dropdown.data('author-id')
selectedId = $dropdown.data('selected')
defaultLabel = $dropdown.data('default-label')
issueURL = $dropdown.data('issueUpdate')
@@ -207,6 +208,7 @@ class @UsersSelect
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
@showCurrentUser = $(select).data('current-user')
+ @authorId = $(select).data('author-id')
showNullUser = $(select).data('null-user')
showAnyUser = $(select).data('any-user')
showEmailUser = $(select).data('email-user')
@@ -312,6 +314,7 @@ class @UsersSelect
project_id: @projectId
group_id: @groupId
current_user: @showCurrentUser
+ author_id: @authorId
dataType: "json"
).done (users) ->
callback(users)
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 7f7b7c806e7..8bfc0d583c5 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -5,7 +5,7 @@
*/
.status-box {
-
+
/* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */
padding: 5px 11px;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 66180f38a4f..7eb451c124e 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -70,13 +70,6 @@
display: none;
}
- .issue-details {
- .creator,
- .page-title .btn-close {
- display: none;
- }
- }
-
%ul.notes .note-role, .note-actions {
display: none;
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index b91f2f6f898..f0ec250de2b 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -39,8 +39,7 @@
.diff-file {
border: 1px solid $border-color;
border-bottom: none;
- margin-left: 0;
- margin-right: 0;
+ margin: 0;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 6453c91d955..c8c6bbde084 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -75,6 +75,11 @@ li.commit {
}
}
+ .item-title {
+ display: inline-block;
+ max-width: 70%;
+ }
+
.commit-row-description {
font-size: 14px;
border-left: 1px solid #eee;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 5917f089720..2c2ac903f29 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,5 +1,5 @@
.detail-page-header {
- padding: 11px 0;
+ padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
@@ -16,11 +16,6 @@
.issue_created_ago, .author_link {
white-space: nowrap;
}
-
- .issue-meta {
- display: inline-block;
- line-height: 20px;
- }
}
.detail-page-description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index d0855f66911..183f22a1b24 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -67,6 +67,24 @@
line-height: $code_line_height;
font-size: $code_font_size;
+ &.noteable_line {
+ position: relative;
+
+ &.old {
+ &:before {
+ content: '-';
+ position: absolute;
+ }
+ }
+
+ &.new {
+ &:before {
+ content: '+';
+ position: absolute;
+ }
+ }
+ }
+
span {
white-space: pre;
}
@@ -391,3 +409,23 @@
margin-bottom: 0;
}
}
+
+.file-holder {
+ .diff-line-num:not(.js-unfold-bottom) {
+ a {
+ &:before {
+ content: attr(data-linenumber);
+ }
+ }
+ }
+}
+
+.discussion {
+ .diff-content {
+ .diff-line-num {
+ &:before {
+ content: attr(data-linenumber);
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 6a99cd9cb94..84cc35239f9 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -16,3 +16,24 @@ i.icon-gitorious-big {
width: 18px;
height: 18px;
}
+
+.import-jobs-from-col,
+.import-jobs-to-col {
+ width: 40%;
+}
+
+.import-jobs-status-col {
+ width: 20%;
+}
+
+.btn-import {
+ .loading-icon {
+ display: none;
+ }
+
+ &.is-loading {
+ .loading-icon {
+ display: inline-block;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d9218e15095..5bf44c1cdb6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -173,12 +173,6 @@
}
}
- .subscribe-button {
- span {
- margin-top: 0;
- }
- }
-
&.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */
display: none;
@@ -279,10 +273,6 @@
}
}
-.btn-default.gutter-toggle {
- margin-top: 4px;
-}
-
.detail-page-description {
small {
color: $gray-darkest;
@@ -328,3 +318,50 @@
padding-top: 7px;
}
}
+
+.issuable-status-box {
+ float: none;
+ display: inline-block;
+ margin-top: 0;
+
+ @media (max-width: $screen-xs-max) {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+}
+
+.issuable-header {
+ position: relative;
+ padding-left: 45px;
+ padding-right: 45px;
+ line-height: 35px;
+
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+
+.issuable-actions {
+ padding-top: 10px;
+
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ padding-top: 0;
+ }
+}
+
+.issuable-gutter-toggle {
+ @media (max-width: $screen-sm-max) {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+}
+
+.issuable-meta {
+ display: inline-block;
+ line-height: 18px;
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 6a1d28590c2..fc9db97132d 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -86,41 +86,9 @@ form.edit-issue {
@media (max-width: $screen-xs-max) {
.issue-btn-group {
width: 100%;
- margin-top: 5px;
-
- .btn-group {
- width: 100%;
-
- ul {
- width: 100%;
- text-align: center;
- }
- }
.btn {
width: 100%;
-
- &:first-child:not(:last-child) {
-
- }
-
- &:not(:first-child):not(:last-child) {
- margin-top: 10px;
- }
-
- &:last-child:not(:first-child) {
- margin-top: 10px;
- }
- }
- }
-
- .issue {
- &:hover .issue-actions {
- display: none !important;
- }
-
- .issue-updated-at {
- display: none;
}
}
}
@@ -133,11 +101,3 @@ form.edit-issue {
color: $gl-text-color;
margin-left: 52px;
}
-
-.editor-details {
- display: block;
-
- @media (min-width: $screen-sm-min) {
- display: inline-block;
- }
-}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index da20fa28802..e179bdf0048 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -117,7 +117,7 @@
padding: 6px;
color: $gl-text-color;
- &.subscribe-button {
+ &.label-subscribe-button {
padding-left: 0;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e421a31549a..a0fbf7d67c5 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -198,6 +198,12 @@ ul.notes {
color: $notes-light-color;
}
+.discussion-headline-light {
+ a {
+ color: $gl-link-color;
+ }
+}
+
/**
* Actions for Discussions/Notes
*/
@@ -209,6 +215,17 @@ ul.notes {
color: $notes-action-color;
}
+.discussion-actions {
+ @media (max-width: $screen-sm-max) {
+ float: none;
+ margin-left: 0;
+
+ .note-action-button {
+ margin-left: 0;
+ }
+ }
+}
+
.note-action-button,
.discussion-action-button {
display: inline-block;
@@ -276,8 +293,7 @@ ul.notes {
.diff-file tr.line_holder {
@mixin show-add-diff-note {
- filter: alpha(opacity=100);
- opacity: 1.0;
+ display: inline-block;
}
.add-diff-note {
@@ -291,13 +307,8 @@ ul.notes {
position: absolute;
z-index: 10;
width: 32px;
-
- transition: all 0.2s ease;
-
// "hide" it by default
- opacity: 0.0;
- filter: alpha(opacity=0);
-
+ display: none;
&:hover {
background: $gl-info;
color: #fff;
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 1be0551ad3b..a30b6492572 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -1,17 +1,37 @@
-/* Generic print styles */
-header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {display: none!important;}
-.profiler-results {display: none;}
-
-/* Styles targeted specifically at printing files */
-.tree-ref-holder, .tree-holder .breadcrumb, .blob-commit-info {display: none;}
-.file-title {display: none;}
-.file-holder {border: none;}
-
.wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; }
.wiki h1 {font-size: 30px;}
.wiki h2 {font-size: 22px;}
.wiki h3 {font-size: 18px; font-weight: bold; }
-.sidebar-wrapper { display: none; }
-.nav { display: none; }
-.btn { display: none; }
+header,
+nav,
+nav.main-nav,
+nav.navbar-collapse,
+nav.navbar-collapse.collapse,
+.profiler-results,
+.tree-ref-holder,
+.tree-holder .breadcrumb,
+.blob-commit-info,
+.file-title,
+.file-holder,
+.sidebar-wrapper,
+.nav,
+.btn,
+ul.notes-form,
+.merge-request-ci-status .ci-status-link:after,
+.issuable-gutter-toggle,
+.gutter-toggle,
+.issuable-details .content-block-small,
+.edit-link,
+.note-action-button {
+ display: none!important;
+}
+
+.page-gutter {
+ padding-top: 0;
+ padding-left: 0;
+}
+
+.right-sidebar {
+ top: 0;
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index f010436bd36..b4a28b8dd3f 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -19,6 +19,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to admin_runners_path
end
+ def clear_repository_check_states
+ RepositoryCheck::ClearWorker.perform_async
+
+ redirect_to(
+ admin_application_settings_path,
+ notice: 'Started asynchronous removal of all repository check states.'
+ )
+ end
+
private
def set_application_setting
@@ -82,6 +91,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_enabled,
:akismet_api_key,
:email_author_in_body,
+ :repository_checks_enabled,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index c6b3105544a..87986fdf8b1 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -1,5 +1,5 @@
class Admin::ProjectsController < Admin::ApplicationController
- before_action :project, only: [:show, :transfer]
+ before_action :project, only: [:show, :transfer, :repository_check]
before_action :group, only: [:show, :transfer]
def index
@@ -8,6 +8,7 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].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.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
@@ -30,6 +31,15 @@ class Admin::ProjectsController < Admin::ApplicationController
redirect_to admin_namespace_project_path(@project.namespace, @project)
end
+ def repository_check
+ RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id)
+
+ redirect_to(
+ admin_namespace_project_path(@project.namespace, @project),
+ notice: 'Repository check was triggered.'
+ )
+ end
+
protected
def project
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 97d53acde94..1c53b0b21a3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,6 +3,7 @@ require 'fogbugz'
class ApplicationController < ActionController::Base
include Gitlab::CurrentSettings
+ include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
@@ -13,7 +14,7 @@ class ApplicationController < ActionController::Base
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
- before_action :sentry_user_context
+ before_action :sentry_context
before_action :default_headers
before_action :add_gon_variables
before_action :configure_permitted_parameters, if: :devise_controller?
@@ -40,13 +41,15 @@ class ApplicationController < ActionController::Base
protected
- def sentry_user_context
- if Rails.env.production? && current_application_settings.sentry_enabled && current_user
- Raven.user_context(
- id: current_user.id,
- email: current_user.email,
- username: current_user.username,
- )
+ def sentry_context
+ if Rails.env.production? && current_application_settings.sentry_enabled
+ if current_user
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ username: current_user.username,
+ )
+ end
Raven.tags_context(program: sentry_program_context)
end
@@ -158,20 +161,6 @@ class ApplicationController < ActionController::Base
end
end
- def add_gon_variables
- gon.api_version = API::API.version
- gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
- gon.default_issues_tracker = Project.new.default_issue_tracker.to_param
- gon.max_file_size = current_application_settings.max_attachment_size
- gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
- gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
-
- if current_user
- gon.current_user_id = current_user.id
- gon.api_token = current_user.private_token
- end
- end
-
def validate_user_service_ticket!
return unless signed_in? && session[:service_tickets]
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 81ba58ce49c..eb0abc80ab4 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -12,8 +12,15 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user] && current_user
- @users = [*@users, current_user].uniq
+ @users = [*@users, current_user]
end
+
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users = [author, *@users] if author
+ end
+
+ @users.uniq!
end
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index c1adc999567..ee4fcc4e360 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -40,6 +40,7 @@ class GroupsController < Groups::ApplicationController
@last_push = current_user.recent_push if current_user
@projects = @projects.includes(:namespace)
+ @projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 55050615473..9b5c43b17e2 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -51,6 +51,7 @@ class HelpController < ApplicationController
end
def ui
+ @user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end
private
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index d1e4ac10f6c..c6bdd0602c1 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,9 +1,11 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
+ include Gitlab::GonHelper
include PageLayoutHelper
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
+ before_action :add_gon_variables
layout 'profile'
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index b88c080352b..a12549d6bcb 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -10,6 +10,11 @@ class Profiles::KeysController < Profiles::ApplicationController
@key = current_user.keys.find(params[:id])
end
+ # Back-compat: We need to support this URL since git-annex webapp points to it
+ def new
+ redirect_to profile_keys_path
+ end
+
def create
@key = current_user.keys.new(key_params)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c26cfeccf1d..38214f04793 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -60,8 +60,8 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- @note = @project.notes.new(noteable: @issue)
- @notes = @issue.notes.nonawards.with_associations.fresh
+ @note = @project.notes.new(noteable: @issue)
+ @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
respond_to do |format|
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 8e7956da48f..2ae180c8a12 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,6 +1,7 @@
class UsersController < ApplicationController
skip_before_action :authenticate_user!
- before_action :set_user
+ before_action :user
+ before_action :authorize_read_user!, only: [:show]
def show
respond_to do |format|
@@ -75,22 +76,26 @@ class UsersController < ApplicationController
private
- def set_user
- @user = User.find_by_username!(params[:username])
+ def authorize_read_user!
+ render_404 unless can?(current_user, :read_user, user)
+ end
+
+ def user
+ @user ||= User.find_by_username!(params[:username])
end
def contributed_projects
- ContributedProjectsFinder.new(@user).execute(current_user)
+ ContributedProjectsFinder.new(user).execute(current_user)
end
def contributions_calendar
@contributions_calendar ||= Gitlab::ContributionsCalendar.
- new(contributed_projects, @user)
+ new(contributed_projects, user)
end
def load_events
# Get user activity feed for projects common for both users
- @events = @user.recent_events.
+ @events = user.recent_events.
merge(projects_for_current_user).
references(:project).
with_associations.
@@ -99,16 +104,16 @@ class UsersController < ApplicationController
def load_projects
@projects =
- PersonalProjectsFinder.new(@user).execute(current_user)
+ PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page])
end
def load_contributed_projects
- @contributed_projects = contributed_projects.joined(@user)
+ @contributed_projects = contributed_projects.joined(user)
end
def load_groups
- @groups = JoinedGroupsFinder.new(@user).execute(current_user)
+ @groups = JoinedGroupsFinder.new(user).execute(current_user)
end
def projects_for_current_user
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index ff32e834499..6a3ec83b8c0 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -40,10 +40,11 @@ module DiffHelper
(unfold) ? 'unfold js-unfold' : ''
end
- def diff_line_content(line)
+ def diff_line_content(line, line_type = nil)
if line.blank?
" &nbsp;".html_safe
else
+ line[0] = ' ' if %w[new old].include?(line_type)
line
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index b14b8218d02..49b6b79ce35 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -55,6 +55,15 @@ module IssuablesHelper
h(milestone_title.presence || default_label)
end
+ def issuable_meta(issuable, project, text)
+ output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
+ output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe
+ output << content_tag(:strong) do
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs")
+ author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
+ end
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 05386d790ca..4fc6de59a8b 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -6,12 +6,13 @@ module SelectsHelper
value = opts[:selected] || ''
placeholder = opts[:placeholder] || 'Search for a user'
- null_user = opts[:null_user] || false
- any_user = opts[:any_user] || false
- email_user = opts[:email_user] || false
- first_user = opts[:first_user] && current_user ? current_user.username : false
- current_user = opts[:current_user] || false
- project = opts[:project] || @project
+ null_user = opts[:null_user] || false
+ any_user = opts[:any_user] || false
+ email_user = opts[:email_user] || false
+ first_user = opts[:first_user] && current_user ? current_user.username : false
+ current_user = opts[:current_user] || false
+ author_id = opts[:author_id] || ''
+ project = opts[:project] || @project
html = {
class: css_class,
@@ -21,7 +22,8 @@ module SelectsHelper
any_user: any_user,
email_user: email_user,
first_user: first_user,
- current_user: current_user
+ current_user: current_user,
+ author_id: author_id
}
}
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
new file mode 100644
index 00000000000..2bff5b63cc4
--- /dev/null
+++ b/app/mailers/repository_check_mailer.rb
@@ -0,0 +1,14 @@
+class RepositoryCheckMailer < BaseMailer
+ def notify(failed_count)
+ if failed_count == 1
+ @message = "One project failed its last repository check"
+ else
+ @message = "#{failed_count} projects failed their last repository check"
+ end
+
+ mail(
+ to: User.admins.pluck(:email),
+ subject: @message
+ )
+ end
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index c0bf6def7c5..6103a2947e2 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -18,6 +18,7 @@ class Ability
when Namespace then namespace_abilities(user, subject)
when GroupMember then group_member_abilities(user, subject)
when ProjectMember then project_member_abilities(user, subject)
+ when User then user_abilities
else []
end.concat(global_abilities(user))
end
@@ -35,6 +36,8 @@ class Ability
anonymous_project_abilities(subject)
when subject.is_a?(Group) || subject.respond_to?(:group)
anonymous_group_abilities(subject)
+ when subject.is_a?(User)
+ anonymous_user_abilities
else
[]
end
@@ -81,17 +84,17 @@ class Ability
end
def anonymous_group_abilities(subject)
+ rules = []
+
group = if subject.is_a?(Group)
subject
else
subject.group
end
- if group && group.public?
- [:read_group]
- else
- []
- end
+ rules << :read_group if group.public?
+
+ rules
end
def anonymous_personal_snippet_abilities(snippet)
@@ -110,9 +113,14 @@ class Ability
end
end
+ def anonymous_user_abilities
+ [:read_user] unless restricted_public_level?
+ end
+
def global_abilities(user)
rules = []
rules << :create_group if user.can_create_group
+ rules << :read_users_list
rules
end
@@ -163,7 +171,7 @@ class Ability
@public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project,
- :read_commit_status,
+ :read_commit_status
]
end
@@ -284,7 +292,6 @@ class Ability
def group_abilities(user, group)
rules = []
-
rules << :read_group if can_read_group?(user, group)
# Only group masters and group owners can create new projects
@@ -456,6 +463,10 @@ class Ability
rules
end
+ def user_abilities
+ [:read_user]
+ end
+
def abilities
@abilities ||= begin
abilities = Six.new
@@ -470,6 +481,10 @@ class Ability
private
+ def restricted_public_level?
+ current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
def named_abilities(name)
[
:"read_#{name}",
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 052cd874733..36f88154232 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -153,7 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
- akismet_enabled: false
+ akismet_enabled: false,
+ repository_checks_enabled: true,
)
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7d33838044b..85ef0523b31 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -365,11 +365,23 @@ module Ci
self.update(erased_by: user, erased_at: Time.now)
end
- private
-
def yaml_variables
+ global_yaml_variables + job_yaml_variables
+ end
+
+ def global_yaml_variables
+ if commit.config_processor
+ commit.config_processor.global_variables.map do |key, value|
+ { key: key, value: value, public: true }
+ end
+ else
+ []
+ end
+ end
+
+ def job_yaml_variables
if commit.config_processor
- commit.config_processor.variables.map do |key, value|
+ commit.config_processor.job_variables(name).map do |key, value|
{ key: key, value: value, public: true }
end
else
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index b8585d4e577..b7894c99846 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -37,4 +37,10 @@ class ExternalIssue
def to_reference(_from_project = nil)
id
end
+
+ def reference_link_text(from_project = nil)
+ return "##{id}" if /^\d+$/.match(id)
+
+ id
+ end
end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
new file mode 100644
index 00000000000..c78c7f4aa0e
--- /dev/null
+++ b/app/models/oauth_access_token.rb
@@ -0,0 +1,19 @@
+# == Schema Information
+#
+# Table name: oauth_access_tokens
+#
+# id :integer not null, primary key
+# resource_owner_id :integer
+# application_id :integer
+# token :string not null
+# refresh_token :string
+# expires_in :integer
+# revoked_at :datetime
+# created_at :datetime not null
+# scopes :string
+#
+
+class OauthAccessToken < ActiveRecord::Base
+ belongs_to :resource_owner, class_name: 'User'
+ belongs_to :application, class_name: 'Doorkeeper::Application'
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index fadc8bb2c9e..8f20922e3c5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -409,6 +409,35 @@ class Project < ActiveRecord::Base
self.import_data.destroy if self.import_data
end
+ def import_url=(value)
+ import_url = Gitlab::ImportUrl.new(value)
+ create_or_update_import_data(credentials: import_url.credentials)
+ super(import_url.sanitized_url)
+ end
+
+ def import_url
+ if import_data && super
+ import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials)
+ import_url.full_url
+ else
+ super
+ end
+ end
+
+ def create_or_update_import_data(data: nil, credentials: nil)
+ project_import_data = import_data || build_import_data
+ if data
+ project_import_data.data ||= {}
+ project_import_data.data = project_import_data.data.merge(data)
+ end
+ if credentials
+ project_import_data.credentials ||= {}
+ project_import_data.credentials = project_import_data.credentials.merge(credentials)
+ end
+
+ project_import_data.save
+ end
+
def import?
external_import? || forked?
end
@@ -865,7 +894,9 @@ class Project < ActiveRecord::Base
def change_head(branch)
repository.before_change_head
- gitlab_shell.update_repository_head(self.path_with_namespace, branch)
+ repository.rugged.references.create('HEAD',
+ "refs/heads/#{branch}",
+ force: true)
reload_default_branch
end
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index cd3319f077e..79efb403058 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -12,8 +12,20 @@ require 'file_size_validator'
class ProjectImportData < ActiveRecord::Base
belongs_to :project
-
+ attr_encrypted :credentials,
+ key: Gitlab::Application.secrets.db_key_base,
+ marshal: true,
+ encode: true,
+ mode: :per_attribute_iv_and_salt
+
serialize :data, JSON
validates :project, presence: true
+
+ before_validation :symbolize_credentials
+
+ def symbolize_credentials
+ # bang doesn't work here - attr_encrypted makes it not to work
+ self.credentials = self.credentials.deep_symbolize_keys unless self.credentials.blank?
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0b2289cfa39..589756f8531 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -12,11 +12,13 @@ class Repository
attr_accessor :path_with_namespace, :project
def self.clean_old_archives
- repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
+ Gitlab::Metrics.measure(:clean_old_archives) do
+ repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
- return unless File.directory?(repository_downloads_path)
+ return unless File.directory?(repository_downloads_path)
- Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
+ Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
+ end
end
def initialize(path_with_namespace, project)
@@ -169,7 +171,12 @@ class Repository
def rm_tag(tag_name)
before_remove_tag
- gitlab_shell.rm_tag(path_with_namespace, tag_name)
+ begin
+ rugged.tags.delete(tag_name)
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
end
def branch_names
@@ -797,7 +804,7 @@ class Repository
def search_files(query, ref)
offset = 2
- args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 0004a399f47..02c4eee3d02 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,28 +1,37 @@
module Projects
class ParticipantsService < BaseService
- def execute(note_type, note_id)
- participating =
- if note_type && note_id
- participants_in(note_type, note_id)
- else
- []
- end
+ def execute(noteable_type, noteable_id)
+ @noteable_type = noteable_type
+ @noteable_id = noteable_id
project_members = sorted(project.team.members)
- participants = all_members + groups + project_members + participating
+ participants = target_owner + participants_in_target + all_members + groups + project_members
participants.uniq
end
- def participants_in(type, id)
- target =
- case type
+ def target
+ @target ||=
+ case @noteable_type
when "Issue"
- project.issues.find_by_iid(id)
+ project.issues.find_by_iid(@noteable_id)
when "MergeRequest"
- project.merge_requests.find_by_iid(id)
+ project.merge_requests.find_by_iid(@noteable_id)
when "Commit"
- project.commit(id)
+ project.commit(@noteable_id)
+ else
+ nil
end
-
+ end
+
+ def target_owner
+ return [] unless target && target.author.present?
+
+ [{
+ name: target.author.name,
+ username: target.author.username
+ }]
+ end
+
+ def participants_in_target
return [] unless target
users = target.participants(current_user)
@@ -30,13 +39,13 @@ module Projects
end
def sorted(users)
- users.uniq.to_a.compact.sort_by(&:username).map do |user|
+ users.uniq.to_a.compact.sort_by(&:username).map do |user|
{ username: user.username, name: user.name }
end
end
def groups
- current_user.authorized_groups.sort_by(&:path).map do |group|
+ current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
{ username: group.path, name: group.name, count: count }
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 2e734654466..79a27f4af7e 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -34,8 +34,9 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists")
end
- # Apply new namespace id
+ # Apply new namespace id and visibility level
project.namespace = new_namespace
+ project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
project.save!
# Notifications
@@ -56,7 +57,7 @@ module Projects
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
project.old_path_with_namespace = old_path
-
+
SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index a8cca1a81cb..aadd2c54f20 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -26,7 +26,9 @@
.btn-group{ data: data_attrs }
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
= level
- %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets
+ %span.help-block#restricted-visibility-help
+ Selected levels cannot be used by non-admin users for projects or snippets.
+ If the public level is restricted, user profiles are only visible to logged in users.
.form-group
= f.label :import_sources, class: 'control-label col-sm-2'
.col-sm-10
@@ -271,5 +273,24 @@
.col-sm-10
= f.text_field :sentry_dsn, class: 'form-control'
+ %fieldset
+ %legend Repository Checks
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :repository_checks_enabled do
+ = f.check_box :repository_checks_enabled
+ Enable Repository Checks
+ .help-block
+ GitLab will periodically run
+ %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
+ in all project and wiki repositories to look for silent disk corruption issues.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
+ .help-block
+ If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index af9fdeb0734..4b475a4d8fa 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,6 +1,7 @@
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
- Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
+ Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
+ Gitlab::RepositoryCheckLogger]
%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index d39c0f44031..aa07afa0d62 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -3,7 +3,7 @@
.row.prepend-top-default
%aside.col-md-3
- .admin-filter
+ .panel.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
.form-group
= label_tag :name, 'Name:'
@@ -38,7 +38,13 @@
%span.descr
= visibility_level_icon(level)
= label
- %hr
+ %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
+
= 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"
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index c638c32a654..73986d21bcf 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -5,6 +5,16 @@
%i.fa.fa-pencil-square-o
Edit
%hr
+- if @project.last_repository_check_failed?
+ .row
+ .col-md-12
+ .panel
+ .panel-heading.alert.alert-danger
+ Last repository check
+ = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)"
+ failed. See
+ = link_to 'repocheck.log', admin_logs_path
+ for error messages.
.row
.col-md-6
.panel.panel-default
@@ -95,6 +105,32 @@
.col-sm-offset-2.col-sm-10
= f.submit 'Transfer', class: 'btn btn-primary'
+ .panel.panel-default.repository-check
+ .panel-heading
+ Repository check
+ .panel-body
+ = form_for @project, url: repository_check_admin_namespace_project_path(@project.namespace, @project), method: :post do |f|
+ .form-group
+ - if @project.last_repository_check_at.nil?
+ This repository has never been checked.
+ - else
+ This repository was last checked
+ = @project.last_repository_check_at.to_s(:medium) + '.'
+ The check
+ - if @project.last_repository_check_failed?
+ = succeed '.' do
+ %strong.cred failed
+ See
+ = link_to 'repocheck.log', admin_logs_path
+ for error messages.
+ - else
+ passed.
+
+ = link_to icon('question-circle'), help_page_path('administration', 'repository_checks')
+
+ .form-group
+ = f.submit 'Trigger repository check', class: 'btn btn-primary'
+
.col-md-6
- if @group
.panel.panel-default
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index cb93ff2465e..e5607dacd0d 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -6,18 +6,17 @@
.login-heading
%h3 Create an account
.login-body
- - user = params[:user].present? ? params[:user] : {}
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
.devise-errors
= devise_error_messages!
%div
- = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true
+ = f.text_field :name, class: "form-control top", placeholder: "Name", required: true
%div
- = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true
+ = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
%div
- = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true
+ = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength
- = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
+ = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
%div
- if current_application_settings.recaptcha_enabled
= recaptcha_tags
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 55f4a6f287d..0aff79749ef 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -68,7 +68,7 @@
%td= app.name
%td= token.created_at
%td= token.scopes
- %td= render 'delete_form', application: app
+ %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- @authorized_anonymous_tokens.each do |token|
%tr
%td
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d084559abc3..f12df5c8ffe 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -345,11 +345,11 @@
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
- = link_to_member_avatar(current_user, size: 30)
+ = link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
- = current_user.name
+ = @user.name
.dropdown-menu-user-username
- = current_user.to_reference
+ = @user.to_reference
.example
%div
@@ -372,11 +372,11 @@
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
- = link_to_member_avatar(current_user, size: 30)
+ = link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
- = current_user.name
+ = @user.name
.dropdown-menu-user-username
- = current_user.to_reference
+ = @user.to_reference
.dropdown-page-two
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index d8af0295b2d..dfebf7768d9 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -20,10 +20,10 @@
job.attr("id", "project_#{@project.id}")
target_field = job.find(".import-target")
target_field.empty()
- target_field.append('<strong>#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}</strong>')
+ target_field.append('#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}')
$("table.import-jobs tbody").prepend(job)
job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started")
- else
:plain
job = $("tr#repo_#{@repo_id}")
- job.find(".import-actions").html("<i class='fa fa-exclamation-circle'> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}</i>")
+ job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index aec2e836c9f..6e993e58f0d 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -10,13 +10,19 @@
%hr
%p
- if @incompatible_repos.any?
- = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all compatible projects
+ = icon("spinner spin", class: "loading-icon")
- else
- = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
-
-.table-holder
+.table-responsive
%table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
%thead
%tr
%th From Bitbucket
@@ -28,7 +34,7 @@
%td
= link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
%td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
@@ -47,7 +53,9 @@
%td.import-target
= "#{repo["owner"]}/#{repo["slug"]}"
%td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
- @incompatible_repos.each do |repo|
%tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
%td
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index 6ee16c8be4b..d3d3c595c17 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -13,10 +13,15 @@
how FogBugz email addresses and usernames are imported into GitLab.
%hr
%p
- = button_tag 'Import all projects', class: 'btn btn-success js-import-all'
+ = button_tag class: 'btn btn-import btn-success js-import-all' do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
-.table-holder
+.table-responsive
%table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
%thead
%tr
%th From FogBugz
@@ -28,7 +33,7 @@
%td
= project.import_source
%td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
@@ -47,7 +52,9 @@
%td.import-target
= "#{current_user.username}/#{repo.name}"
%td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}");
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 1416ee5bd5a..9639da4cb58 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -8,10 +8,15 @@
Select projects you want to import.
%hr
%p
- = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
-.table-holder
+.table-responsive
%table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
%thead
%tr
%th From GitHub
@@ -23,7 +28,7 @@
%td
= link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
%td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
@@ -42,7 +47,9 @@
%td.import-target
= repo.full_name
%td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}");
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 911a55eb85d..e3a356b5379 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -8,10 +8,15 @@
Select projects you want to import.
%hr
%p
- = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
-.table-holder
+.table-responsive
%table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
%thead
%tr
%th From GitLab.com
@@ -23,7 +28,7 @@
%td
= link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
%td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
@@ -42,7 +47,9 @@
%td.import-target
= repo["path_with_namespace"]
%td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ = button_tag class: "btn js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}");
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index 6b0fa1edf8c..267eee4f262 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -8,10 +8,15 @@
Select projects you want to import.
%hr
%p
- = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
-.table-holder
+.table-responsive
%table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
%thead
%tr
%th From Gitorious.org
@@ -23,7 +28,7 @@
%td
= link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
%td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
@@ -42,7 +47,9 @@
%td.import-target
= repo.full_name
%td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}");
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 175ef6921cd..5ada6b174eb 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -14,12 +14,19 @@
%hr
%p
- if @incompatible_repos.any?
- = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all compatible projects
+ = icon("spinner spin", class: "loading-icon")
- else
- = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ Import all projects
+ = icon("spinner spin", class: "loading-icon")
-.table-holder
+.table-responsive
%table.table.import-jobs
+ %colgroup.import-jobs-from-col
+ %colgroup.import-jobs-to-col
+ %colgroup.import-jobs-status-col
%thead
%tr
%th From Google Code
@@ -31,7 +38,7 @@
%td
= link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
%td
- %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
@@ -50,7 +57,9 @@
%td.import-target
= "#{current_user.username}/#{repo.name}"
%td.import-actions.job-status
- = button_tag "Import", class: "btn js-add-to-import"
+ = button_tag class: "btn btn-import js-add-to-import" do
+ Import
+ = icon("spinner spin", class: "loading-icon")
- @incompatible_repos.each do |repo|
%tr{id: "repo_#{repo.id}"}
%td
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
index 9ae6964aaac..b6074373e2b 100644
--- a/app/views/projects/_builds_settings.html.haml
+++ b/app/views/projects/_builds_settings.html.haml
@@ -52,6 +52,9 @@
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
+ %li
+ gcovr (C/C++) -
+ %code ^TOTAL.*\s+(\d+\%)$
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 7a78d61a611..8de44a6c914 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,6 +1,6 @@
.md-area
.md-header
- %ul.nav-links
+ %ul.nav-links.clearfix
%li.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index abcfca4cd11..38e62c81fed 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -1,20 +1,20 @@
- if @lines.present?
- if @form.unfold? && @form.since != 1 && !@form.bottom?
%tr.line_holder{ id: @form.since }
- = render "projects/diffs/match_line", {line: @match_line,
- line_old: @form.since, line_new: @form.since, bottom: false, new_file: false}
+ = render "projects/diffs/match_line", { line: @match_line,
+ line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
- @lines.each_with_index do |line, index|
- line_new = index + @form.since
- line_old = line_new - @form.offset
%tr.line_holder
- %td.old_line.diff-line-num{data: {linenumber: line_old}}
+ %td.old_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_old), "#"
- %td.new_line.diff-line-num
+ %td.new_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_new) , "#"
%td.line_content.noteable_line==#{' ' * @form.indent}#{line}
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
%tr.line_holder{ id: @form.to }
- = render "projects/diffs/match_line", {line: @match_line,
- line_old: @form.to, line_new: @form.to, bottom: true, new_file: false}
+ = render "projects/diffs/match_line", { line: @match_line,
+ line_old: @form.to, line_new: @form.to, bottom: true, new_file: false }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 7da89231243..34f27f1e793 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -11,7 +11,7 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
- %span.item-title.str-truncated
+ %span.item-title
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 9464c8dc996..107097ad963 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,26 +1,26 @@
- type = line.type
-%tr.line_holder{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,
- line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file}
+ = render "projects/diffs/match_line", { line: line.text,
+ line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file }
- when 'nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num{class: type}
- - link_text = raw(type == "new" ? "&nbsp;" : line.old_pos)
+ %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
+ - link_text = type == "new" ? "&nbsp;".html_safe : line.old_pos
- if defined?(plain) && plain
= link_text
- else
- = link_to link_text, "##{line_code}", id: line_code
+ = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
- if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(line_code)
- %td.new_line.diff-line-num{class: type, data: {linenumber: line.new_pos}}
- - link_text = raw(type == "old" ? "&nbsp;" : line.new_pos)
+ %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
+ - link_text = type == "old" ? "&nbsp;".html_safe : line.new_pos
- if defined?(plain) && plain
= link_text
- else
- = link_to link_text, "##{line_code}", id: line_code
- %td.line_content{class: "noteable_line #{type} #{line_code}", data: { line_code: line_code }}= diff_line_content(line.text)
+ = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
+ %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type)
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 6d872cd0b21..76a4f41193c 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -210,6 +210,7 @@
%li Be careful. Changing the project's namespace can have unintended side effects.
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location.
+ %li Project visibility level will be changed to match namespace rules when transfering to a group.
.form-actions
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- else
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 4aa92d0b39e..7a8009f6da4 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -27,7 +27,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = issue.notes.user.count
+ - note_count = issue.notes.user.nonawards.count
- if note_count > 0
%li
= link_to issue_path(issue) + "#notes" do
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 5fe5ddc0819..bde80bbb54b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,82 +1,79 @@
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
+- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
-= render "header_title"
+.clearfix.detail-page-header
+ .issuable-header
+ .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
+ = icon('check', class: "hidden-sm hidden-md hidden-lg")
+ %span.hidden-xs
+ Closed
+ .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
+ = icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
+ %span.hidden-xs Open
-.issue
- .detail-page-header.issuable-header
- .pull-left
- .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"}
- %span.hidden-xs
- Closed
- %span.hidden-sm.hidden-md.hidden-lg
- = icon('check')
- .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"}
- %span.hidden-xs
- Open
- %span.hidden-sm.hidden-md.hidden-lg
- = icon('circle-o')
-
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
- .issue-meta
+ .issuable-meta
= confidential_icon(@issue)
- %strong.identifier
- Issue ##{@issue.iid}
- %span.creator
- opened
- .editor-details
- .editor-details
- = time_ago_with_tooltip(@issue.created_at)
- by
- %strong
- = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
- %strong
- = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
- by_username: true, avatar: false)
+ = issuable_meta(@issue, @project, "Issue")
- .pull-right.issue-btn-group
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
- = icon('plus')
- New issue
- - if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
- = icon('pencil-square-o')
- Edit
+ - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
+ %span.caret
+ Options
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %ul
+ - if can?(current_user, :create_issue, @project)
+ %li
+ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
+ - if can?(current_user, :update_issue, @issue)
+ %li
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ %li
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ %li
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
+ - if can?(current_user, :create_issue, @project)
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
+ = icon('plus')
+ New issue
+ - if can?(current_user, :update_issue, @issue)
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do
+ = icon('pencil-square-o')
+ Edit
- .issue-details.issuable-details
- .detail-page-description.content-block
- %h2.title
- = markdown escape_once(@issue.title), pipeline: :single_line
- %div
- - if @issue.description.present?
- .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
- .wiki
- = preserve do
- = markdown(@issue.description, cache_key: [@issue, "description"])
- %textarea.hidden.js-task-list-field
- = @issue.description
- = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
+.issue-details.issuable-details
+ .detail-page-description.content-block
+ %h2.title
+ = markdown escape_once(@issue.title), pipeline: :single_line
+ - if @issue.description.present?
+ .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
+ .wiki
+ = preserve do
+ = markdown(@issue.description, cache_key: [@issue, "description"])
+ %textarea.hidden.js-task-list-field
+ = @issue.description
+ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
- #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)}
- // This element is filled in using JavaScript.
+ #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
+ // This element is filled in using JavaScript.
- #related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)}
- // This element is filled in using JavaScript.
+ #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
+ // This element is filled in using JavaScript.
- .content-block.content-block-small
- = render 'new_branch'
- = render 'votes/votes_block', votable: @issue
+ .content-block.content-block-small
+ = render 'new_branch'
+ = render 'votes/votes_block', votable: @issue
- .row
- %section.col-md-12
- .issuable-discussion
- = render 'projects/issues/discussion'
+ %section.issuable-discussion
+ = render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml
index 986d8c220db..e69de29bb2d 100644
--- a/app/views/projects/issues/update.js.haml
+++ b/app/views/projects/issues/update.js.haml
@@ -1,3 +0,0 @@
-$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}";
-$('aside.right-sidebar').effect('highlight');
-new IssuableContext();
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 097a65969a6..8bf544b8371 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -14,7 +14,7 @@
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
.subscription-status{data: {status: label_subscription_status(label)}}
- %a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}}
+ %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } }
%span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 391193eed6c..e740fe8c84d 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -35,7 +35,7 @@
= icon('thumbs-down')
= downvotes
- - note_count = merge_request.mr_and_commit_notes.user.count
+ - note_count = merge_request.mr_and_commit_notes.user.nonawards.count
- if note_count > 0
%li
= link_to merge_request_path(merge_request) + "#notes" do
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 7d7c487e970..b08524574e4 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -16,11 +16,9 @@
= dropdown_title("Select source project")
= dropdown_filter("Search projects")
= dropdown_content do
- - is_active = f.object.source_project_id == @merge_request.source_project.id
- %ul
- %li
- %a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } }
- = @merge_request.source_project_path
+ = render 'projects/merge_requests/dropdowns/project',
+ projects: [@merge_request.source_project],
+ selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
@@ -28,11 +26,9 @@
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
= dropdown_content do
- %ul
- - @merge_request.source_branches.each do |branch|
- %li
- %a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } }
- = branch
+ = render 'projects/merge_requests/dropdowns/branch',
+ branches: @merge_request.source_branches,
+ selected: f.object.source_branch
.panel-footer
= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
@@ -50,11 +46,9 @@
= dropdown_title("Select target project")
= dropdown_filter("Search projects")
= dropdown_content do
- %ul
- - projects.each do |project|
- %li
- %a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } }
- = project.path_with_namespace
+ = render 'projects/merge_requests/dropdowns/project',
+ projects: projects,
+ selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" }
@@ -62,11 +56,9 @@
= dropdown_title("Select target branch")
= dropdown_filter("Search branches")
= dropdown_content do
- %ul
- - @merge_request.target_branches.each do |branch|
- %li
- %a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } }
- = branch
+ = render 'projects/merge_requests/dropdowns/branch',
+ branches: @merge_request.target_branches,
+ selected: f.object.target_branch
.panel-footer
= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 1dd8f721f7e..285ad26316c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,8 +1,7 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
-
-= render "header_title"
+- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
- if params[:view] == 'parallel'
- fluid_layout true
@@ -32,8 +31,7 @@
%span Request to merge
%span.label-branch= source_branch_with_namespace(@merge_request)
%span into
- = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
- = @merge_request.target_branch
+ = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
@@ -51,7 +49,7 @@
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
+ %span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml
new file mode 100644
index 00000000000..ba8d9a5835c
--- /dev/null
+++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml
@@ -0,0 +1,5 @@
+%ul
+ - branches.each do |branch|
+ %li
+ %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } }
+ = branch
diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml
new file mode 100644
index 00000000000..25d5dc92f8a
--- /dev/null
+++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml
@@ -0,0 +1,5 @@
+%ul
+ - projects.each do |project|
+ %li
+ %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } }
+ = project.path_with_namespace
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index ab4b1f14be5..0a99e8c9591 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,35 +1,32 @@
-.detail-page-header
- .status-box{ class: status_box_class(@merge_request) }
- %span.hidden-xs
- = @merge_request.state_human_name
- %span.hidden-sm.hidden-md.hidden-lg
- = icon(@merge_request.state_icon_name)
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
- = icon('angle-double-left')
- .issue-meta
- %strong.identifier
- %span.hidden-sm.hidden-md.hidden-lg
- MR
+.clearfix.detail-page-header
+ .issuable-header
+ .issuable-status-box.status-box{ class: status_box_class(@merge_request) }
+ = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
- Merge Request
- !#{@merge_request.iid}
- %span.creator
- opened
- .editor-details
- = time_ago_with_tooltip(@merge_request.created_at)
- by
- %strong
- = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
- %strong
- = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
- by_username: true, avatar: false)
+ = @merge_request.state_human_name
- .issue-btn-group.pull-right
- - if can?(current_user, :update_merge_request, @merge_request)
- - if @merge_request.open?
- = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
- = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
+ .issuable-meta
+ = issuable_meta(@merge_request, @project, "Merge Request")
+
+ - if can?(current_user, :update_merge_request, @merge_request)
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
+ %span.caret
+ Options
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %ul
+ %li{ class: issue_button_visibility(@merge_request, true) }
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
+ %li{ class: issue_button_visibility(@merge_request, false) }
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
+ %li
+ = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request'
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
+ = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do
= icon('pencil-square-o')
Edit
- - if @merge_request.closed?
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml
index 9cce5660e1c..e69de29bb2d 100644
--- a/app/views/projects/merge_requests/update.js.haml
+++ b/app/views/projects/merge_requests/update.js.haml
@@ -1,3 +0,0 @@
-$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}";
-$('aside.right-sidebar').effect('highlight');
-new IssuableContext();
diff --git a/app/views/projects/merge_requests/update_branches.html.haml b/app/views/projects/merge_requests/update_branches.html.haml
index 1b93188a10c..64482973a89 100644
--- a/app/views/projects/merge_requests/update_branches.html.haml
+++ b/app/views/projects/merge_requests/update_branches.html.haml
@@ -1,5 +1,3 @@
-%ul
- - @target_branches.each do |branch|
- %li
- %a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } }
- = branch
+= render 'projects/merge_requests/dropdowns/branch',
+branches: @target_branches,
+selected: nil
diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml
index b8068835b3a..572b00a38c7 100644
--- a/app/views/projects/notes/_discussion.html.haml
+++ b/app/views/projects/notes/_discussion.html.haml
@@ -1,5 +1,5 @@
- note = discussion_notes.first
-.timeline-entry
+%li.note.note-discussion.timeline-entry
.timeline-entry-inner
.timeline-icon
= link_to user_path(note.author) do
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 03a44ca99c0..6e9ecdf7649 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,4 +1,5 @@
-%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] }
+- note_editable = note_editable?(note)
+%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
.timeline-entry-inner
.timeline-icon
%a{href: user_path(note.author)}
@@ -15,16 +16,16 @@
- if access
%span.note-role
= access
- - if note_editable?(note)
+ - if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o')
- .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
+ .note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- - if note_editable?(note)
+ - if note_editable
= render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index cc42aab5c52..1c39ce897a3 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -1,6 +1,6 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
-%ul.notes.timeline
+%ul.notes.notes-form.timeline
%li.timeline-entry
- if can? current_user, :create_note, @project
.timeline-icon.hidden-xs.hidden-sm
diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml
index cd8a5f0bd02..0ea8862a684 100644
--- a/app/views/projects/notes/discussions/_active.html.haml
+++ b/app/views/projects/notes/discussions/_active.html.haml
@@ -6,15 +6,11 @@
= "#{note.author.to_reference} started a discussion"
= link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
on the diff
+ = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
.discussion-actions
= link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
- .last-update.hide.js-toggle-content
- - last_note = discussion_notes.last
- last updated by
- = link_to_member(@project, last_note.author, avatar: false)
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
index 46f2ba4bbcf..2a2ead58eeb 100644
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ b/app/views/projects/notes/discussions/_commit.html.haml
@@ -8,21 +8,18 @@
= "#{note.author.to_reference} started a discussion on #{commit_description}"
- if commit
= link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
+ = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
.discussion-actions
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
- .last-update.hide.js-toggle-content
- - last_note = discussion_notes.last
- last updated by
- = link_to_member(@project, last_note.author, avatar: false)
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
- if note.for_diff_line?
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
- else
.panel.panel-default
.notes{ data: { discussion_id: discussion_notes.first.discussion_id } }
- = render discussion_notes
+ %ul.notes.timeline
+ = render discussion_notes
.discussion-reply-holder
= link_to_reply_diff(discussion_notes.first)
diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml
index 820e31ccd61..d46aab000c3 100644
--- a/app/views/projects/notes/discussions/_diff.html.haml
+++ b/app/views/projects/notes/discussions/_diff.html.haml
@@ -20,11 +20,9 @@
%td.new_line.diff-line-num= "..."
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num
- = raw(type == "new" ? "&nbsp;" : line.old_pos)
- %td.new_line.diff-line-num
- = raw(type == "old" ? "&nbsp;" : line.new_pos)
- %td.line_content{class: "noteable_line #{type} #{line_code}", line_code: line_code}= diff_line_content(line.text)
+ %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)
- if line_code == note.line_code
= render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml
index f8e000b424f..45141bcd1df 100644
--- a/app/views/projects/notes/discussions/_outdated.html.haml
+++ b/app/views/projects/notes/discussions/_outdated.html.haml
@@ -5,14 +5,10 @@
.inline.discussion-headline-light
= "#{note.author.to_reference} started a discussion"
on the outdated diff
+ = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
.discussion-actions
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-down
Show/hide discussion
- .last-update.hide.js-toggle-content
- - last_note = discussion_notes.last
- last updated by
- = link_to_member(@project, last_note.author, avatar: false)
- #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content.hide
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
new file mode 100644
index 00000000000..df16f503570
--- /dev/null
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{@message}.
+
+%p
+ = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
new file mode 100644
index 00000000000..02f3f80288a
--- /dev/null
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -0,0 +1,3 @@
+#{@message}.
+\
+View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index aed2622a6da..bae15b7f844 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -4,7 +4,7 @@
= f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad js-gfm-input', required: true
+ class: 'form-control pad', required: true
- if issuable.is_a?(MergeRequest)
%p.help-block
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 94affa4b59a..08bfd93f4e6 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -49,7 +49,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
- = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
+ = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone
.sidebar-collapsed-icon
@@ -128,7 +128,7 @@
.title.hide-collapsed
Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
+ %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )}
@@ -152,4 +152,4 @@
new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription')
- new Sidebar(); \ No newline at end of file
+ new Sidebar();
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
new file mode 100644
index 00000000000..667fff031dd
--- /dev/null
+++ b/app/workers/admin_email_worker.rb
@@ -0,0 +1,12 @@
+class AdminEmailWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false # this job auto-repeats via sidekiq-cron
+
+ def perform
+ repository_check_failed_count = Project.where(last_repository_check_failed: true).count
+ return if repository_check_failed_count.zero?
+
+ RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now
+ end
+end
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
new file mode 100644
index 00000000000..44b3145d50f
--- /dev/null
+++ b/app/workers/repository_check/batch_worker.rb
@@ -0,0 +1,63 @@
+module RepositoryCheck
+ class BatchWorker
+ include Sidekiq::Worker
+
+ RUN_TIME = 3600
+
+ sidekiq_options retry: false
+
+ def perform
+ start = Time.now
+
+ # This loop will break after a little more than one hour ('a little
+ # more' because `git fsck` may take a few minutes), or if it runs out of
+ # projects to check. By default sidekiq-cron will start a new
+ # RepositoryCheckWorker each hour so that as long as there are repositories to
+ # check, only one (or two) will be checked at a time.
+ project_ids.each do |project_id|
+ break if Time.now - start >= RUN_TIME
+ break unless current_settings.repository_checks_enabled
+
+ next unless try_obtain_lease(project_id)
+
+ SingleRepositoryWorker.new.perform(project_id)
+ end
+ end
+
+ private
+
+ # Project.find_each does not support WHERE clauses and
+ # Project.find_in_batches does not support ordering. So we just build an
+ # array of ID's. This is OK because we do it only once an hour, because
+ # getting ID's from Postgres is not terribly slow, and because no user
+ # has to sit and wait for this query to finish.
+ def project_ids
+ limit = 10_000
+ never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit).
+ pluck(:id)
+ old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago).
+ reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
+ never_checked_projects + old_check_projects
+ end
+
+ def try_obtain_lease(id)
+ # Use a 24-hour timeout because on servers/projects where 'git fsck' is
+ # super slow we definitely do not want to run it twice in parallel.
+ Gitlab::ExclusiveLease.new(
+ "project_repository_check:#{id}",
+ timeout: 24.hours
+ ).try_obtain
+ end
+
+ def current_settings
+ # No caching of the settings! If we cache them and an admin disables
+ # this feature, an active RepositoryCheckWorker would keep going for up
+ # to 1 hour after the feature was disabled.
+ if Rails.env.test?
+ Gitlab::CurrentSettings.fake_application_settings
+ else
+ ApplicationSetting.current
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
new file mode 100644
index 00000000000..b7202ddff34
--- /dev/null
+++ b/app/workers/repository_check/clear_worker.rb
@@ -0,0 +1,17 @@
+module RepositoryCheck
+ class ClearWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false
+
+ def perform
+ # Do small batched updates because these updates will be slow and locking
+ Project.select(:id).find_in_batches(batch_size: 100) do |batch|
+ Project.where(id: batch.map(&:id)).update_all(
+ last_repository_check_failed: nil,
+ last_repository_check_at: nil,
+ )
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
new file mode 100644
index 00000000000..e54ae86d06c
--- /dev/null
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -0,0 +1,36 @@
+module RepositoryCheck
+ class SingleRepositoryWorker
+ include Sidekiq::Worker
+
+ sidekiq_options retry: false
+
+ def perform(project_id)
+ project = Project.find(project_id)
+ project.update_columns(
+ last_repository_check_failed: !check(project),
+ last_repository_check_at: Time.now,
+ )
+ end
+
+ private
+
+ def check(project)
+ # Use 'map do', not 'all? do', to prevent short-circuiting
+ [project.repository, project.wiki.repository].map do |repository|
+ git_fsck(repository.path_to_repo)
+ end.all?
+ end
+
+ def git_fsck(path)
+ cmd = %W(nice git --git-dir=#{path} fsck)
+ output, status = Gitlab::Popen.popen(cmd)
+
+ if status.zero?
+ true
+ else
+ Gitlab::RepositoryCheckLogger.error("command failed: #{cmd.join(' ')}\n#{output}")
+ false
+ end
+ end
+ end
+end