diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-04-26 16:36:17 +0200 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-04-26 16:36:17 +0200 |
commit | cb90368a692149b4e01b50d7c6682c48cffa438b (patch) | |
tree | 6236d6f4fd56acad1015950d483db0ba8fa3ada9 /app | |
parent | c6f19aed51736e5945283a611eae09f32a9b5aeb (diff) | |
parent | f127edd012bd8b6f76ac67d69aadbd7d4837258f (diff) | |
download | gitlab-ce-cb90368a692149b4e01b50d7c6682c48cffa438b.tar.gz |
Merge remote-tracking branch 'origin/master' into with-pipeline-view
Diffstat (limited to 'app')
285 files changed, 3141 insertions, 1449 deletions
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index f3ed9a66715..dd1bbb37551 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,6 +5,7 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" + license_path: "/api/:version/licenses/:key" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -92,6 +93,16 @@ ).done (projects) -> callback(projects) + # Return text for a specific license + licenseText: (key, data, callback) -> + url = Api.buildUrl(Api.license_path).replace(':key', key) + + $.ajax( + url: url + data: data + ).done (license) -> + callback(license) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b05138ac1ac..5bac8eef1cb 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -22,7 +22,17 @@ #= require cal-heatmap #= require turbolinks #= require autosave -#= require bootstrap +#= require bootstrap/affix +#= require bootstrap/alert +#= require bootstrap/button +#= require bootstrap/collapse +#= require bootstrap/dropdown +#= require bootstrap/modal +#= require bootstrap/scrollspy +#= require bootstrap/tab +#= require bootstrap/transition +#= require bootstrap/tooltip +#= require bootstrap/popover #= require select2 #= require raphael #= require g.raphael @@ -164,7 +174,7 @@ $ -> $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false) + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true) # Flash if (flash = $(".flash-container")).length > 0 diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index af4462ece38..fcba9818726 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,5 +1,5 @@ class @AwardsHandler - constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) -> + constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @unicodes) -> $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() @@ -31,6 +31,8 @@ class @AwardsHandler awards_handler.addAward emoji + $(this).trigger 'blur' + didUserClickEmoji: (that, emoji) -> if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 @@ -55,7 +57,6 @@ class @AwardsHandler , 200 addAward: (emoji) -> - emoji = @normilizeEmojiName(emoji) @postEmoji emoji, => @addAwardToEmojiBar(emoji) @@ -64,7 +65,6 @@ class @AwardsHandler addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) - emoji = @normilizeEmojiName(emoji) if @exist(emoji) if @isActive(emoji) @decrementCounter(emoji) @@ -146,15 +146,7 @@ class @AwardsHandler $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> - emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") - - if emoji_icon.length > 0 - unicodeName = emoji_icon.data("unicode-name") - else - # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") - - "emoji-#{unicodeName}" + "emoji-#{@unicodes[emoji]}" postEmoji: (emoji, callback) -> $.post @post_emoji_url, { note: { @@ -173,9 +165,6 @@ class @AwardsHandler scrollTop: $('.awards').offset().top - 80 }, 200) - normilizeEmojiName: (emoji) -> - @aliases[emoji] || emoji - addEmojiToFrequentlyUsedList: (emoji) -> frequently_used_emojis = @getFrequentlyUsedEmojis() frequently_used_emojis.push(emoji) diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee index 6e29d374267..3cb96bacaa7 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js.coffee +++ b/app/assets/javascripts/behaviors/quick_submit.js.coffee @@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> e.preventDefault() $form = $(e.target).closest('form') - $form.find('input[type=submit], button[type=submit]').disable() + $submit_button = $form.find('input[type=submit], button[type=submit]') + + return if $submit_button.attr('disabled') + + $submit_button.disable() $form.submit() # If the user tabs to a submit button on a `js-quick-submit` form, display a diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee new file mode 100644 index 00000000000..e17eaa75dc1 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee @@ -0,0 +1,30 @@ +class @BlobLicenseSelector + licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i + + constructor: (editor) -> + @$licenseSelector = $('.js-license-selector') + $fileNameInput = $('#file_name') + + initialFileNameValue = if $fileNameInput.length + $fileNameInput.val() + else if $('.editor-file-name').length + $('.editor-file-name').text().trim() + + @toggleLicenseSelector(initialFileNameValue) + + if $fileNameInput + $fileNameInput.on 'keyup blur', (e) => + @toggleLicenseSelector($(e.target).val()) + + $('select.license-select').on 'change', (e) -> + data = + project: $(this).data('project') + fullname: $(this).data('fullname') + Api.licenseText $(this).val(), data, (license) -> + editor.setValue(license.content, -1) + + toggleLicenseSelector: (fileName) => + if @licenseRegex.test(fileName) + @$licenseSelector.show() + else + @$licenseSelector.hide() diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 390e41ed8d4..eea9aa972ee 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -1,44 +1,39 @@ class @EditBlob - constructor: (assets_path, mode)-> - ace.config.set "modePath", assets_path + '/ace' + constructor: (assets_path, ace_mode = null) -> + ace.config.set "modePath", "#{assets_path}/ace" ace.config.loadModule "ace/ext/searchbox" - if mode - ace_mode = mode - editor = ace.edit("editor") - editor.focus() - @editor = editor - - if ace_mode - editor.getSession().setMode "ace/mode/" + ace_mode + @editor = ace.edit("editor") + @editor.focus() + @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode # Before a form submission, move the content from the Ace editor into the # submitted textarea - $('form').submit -> - $("#file-content").val(editor.getValue()) + $('form').submit => + $("#file-content").val(@editor.getValue()) + + @initModePanesAndLinks() + new BlobLicenseSelector(@editor) - editModePanes = $(".js-edit-mode-pane") - editModeLinks = $(".js-edit-mode a") - editModeLinks.click (event) -> - event.preventDefault() - currentLink = $(this) - paneId = currentLink.attr("href") - currentPane = editModePanes.filter(paneId) - editModeLinks.parent().removeClass "active hover" - currentLink.parent().addClass "active hover" - editModePanes.hide() - if paneId is "#preview" - currentPane.fadeIn 200 - $.post currentLink.data("preview-url"), - content: editor.getValue() - , (response) -> - currentPane.empty().append response - currentPane.syntaxHighlight() - return + initModePanesAndLinks: -> + @$editModePanes = $(".js-edit-mode-pane") + @$editModeLinks = $(".js-edit-mode a") + @$editModeLinks.click @editModeLinkClickHandler - else - currentPane.fadeIn 200 - editor.focus() - return + editModeLinkClickHandler: (event) => + event.preventDefault() + currentLink = $(event.target) + paneId = currentLink.attr("href") + currentPane = @$editModePanes.filter(paneId) + @$editModeLinks.parent().removeClass "active hover" + currentLink.parent().addClass "active hover" + @$editModePanes.hide() + currentPane.fadeIn 200 + if paneId is "#preview" + $.post currentLink.data("preview-url"), + content: @editor.getValue() + , (response) -> + currentPane.empty().append response + currentPane.syntaxHighlight() - editor: -> - return @editor + else + @editor.focus() diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee deleted file mode 100644 index 68c5e5195e3..00000000000 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @NewBlob - constructor: (assets_path, mode)-> - ace.config.set "modePath", assets_path + '/ace' - ace.config.loadModule "ace/ext/searchbox" - if mode - ace_mode = mode - editor = ace.edit("editor") - editor.focus() - @editor = editor - - if ace_mode - editor.getSession().setMode "ace/mode/" + ace_mode - - # Before a form submission, move the content from the Ace editor into the - # submitted textarea - $('form').submit -> - $("#file-content").val(editor.getValue()) - - editor: -> - return @editor diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee index ffd3627b1b0..0acb4c1955e 100644 --- a/app/assets/javascripts/commits.js.coffee +++ b/app/assets/javascripts/commits.js.coffee @@ -1,7 +1,7 @@ class @CommitsList @timer = null - @init: (ref, limit) -> + @init: (limit) -> $("body").on "click", ".day-commits-table li.commit", (event) -> if event.target.nodeName != "A" location.href = $(this).attr("url") diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 70fd6f50e9c..2fdb7562515 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -17,6 +17,7 @@ class Dispatcher switch page when 'projects:issues:index' Issues.init() + Issuable.init() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() @@ -28,26 +29,26 @@ class Dispatcher new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() - new DropzoneInput($('.milestone-form')) + new GLForm($('.milestone-form')) when 'groups:milestones:new' new ZenMode() when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.issue-form')) + new GLForm($('.issue-form')) new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' new Diff() shortcut_handler = new ShortcutsNavigation() - new DropzoneInput($('.merge-request-form')) + new GLForm($('.merge-request-form')) new IssuableForm($('.merge-request-form')) when 'projects:tags:new' new ZenMode() - new DropzoneInput($('.tag-form')) + new GLForm($('.tag-form')) when 'projects:releases:edit' new ZenMode() - new DropzoneInput($('.release-form')) + new GLForm($('.release-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssuable(true) @@ -57,7 +58,7 @@ class Dispatcher new ZenMode() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() - MergeRequests.init() + Issuable.init() when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' @@ -137,7 +138,7 @@ class Dispatcher new Wikis() shortcut_handler = new ShortcutsNavigation() new ZenMode() - new DropzoneInput($('.wiki-form')) + new GLForm($('.wiki-form')) when 'snippets' shortcut_handler = new ShortcutsNavigation() new ZenMode() if path[2] == 'show' diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index b502131a99d..e2194589b38 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -15,11 +15,13 @@ class @DropzoneInput project_uploads_path = window.project_uploads_path or null max_file_size = gon.max_file_size or 10 - form_textarea = $(form).find("textarea.markdown-area") + form_textarea = $(form).find(".js-gfm-input") form_textarea.wrap "<div class=\"div-dropzone\"></div>" form_textarea.on 'paste', (event) => handlePaste(event) + $mdArea = $(form_textarea).closest('.md-area') + $(form).setupMarkdownPreview() form_dropzone = $(form).find('.div-dropzone') @@ -49,17 +51,17 @@ class @DropzoneInput $(".div-dropzone-alert").alert "close" dragover: -> - form_textarea.addClass "div-dropzone-focus" + $mdArea.addClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0.7 return dragleave: -> - form_textarea.removeClass "div-dropzone-focus" + $mdArea.removeClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0 return drop: -> - form_textarea.removeClass "div-dropzone-focus" + $mdArea.removeClass 'is-dropzone-hover' form.find(".div-dropzone-hover").css "opacity", 0 form_textarea.focus() return diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee new file mode 100644 index 00000000000..a4304786cbb --- /dev/null +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -0,0 +1,64 @@ +class @DueDateSelect + constructor: -> + $loading = $('.js-issuable-update .due_date') + .find('.block-loading') + .hide() + + $('.js-due-date-select').each (i, dropdown) -> + $dropdown = $(dropdown) + $dropdownParent = $dropdown.closest('.dropdown') + $datePicker = $dropdownParent.find('.js-due-date-calendar') + $block = $dropdown.closest('.block') + $selectbox = $dropdown.closest('.selectbox') + $value = $block.find('.value') + $sidebarValue = $('.js-due-date-sidebar-value', $block) + + fieldName = $dropdown.data('field-name') + abilityName = $dropdown.data('ability-name') + issueUpdateURL = $dropdown.data('issue-update') + + $dropdown.glDropdown( + hidden: -> + $selectbox.hide() + $value.removeAttr('style') + ) + + addDueDate = -> + # Create the post date + value = $("input[name='#{fieldName}']").val() + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + + data = {} + data[abilityName] = {} + data[abilityName].due_date = value + + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + beforeSend: -> + $loading.fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() + $value.removeAttr('style') + + $value.html(mediumDate) + $sidebarValue.html(mediumDate) + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') + $loading.fadeOut() + + $datePicker.datepicker( + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='#{fieldName}']").val() + altField: "input[name='#{fieldName}']" + onSelect: -> + addDueDate() + ) + + $(document) + .off 'click', '.ui-datepicker-header a' + .on 'click', '.ui-datepicker-header a', (e) -> + e.stopImmediatePropagation() 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/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 2dc37257e22..29466e9f2ed 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -32,10 +32,8 @@ class GitLabDropdownFilter else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS $inputContainer.removeClass HAS_VALUE_CLASS - if keyCode is 13 and @input.val() isnt "" - if @options.enterCallback - @options.enterCallback() - return + if keyCode is 13 + return false clearTimeout timeout timeout = setTimeout => @@ -132,7 +130,6 @@ class GitLabDropdown @filterInput = @getElement(FILTER_INPUT) @highlight = false @filterInputBlur = true - @enterCallback = true } = @options self = @ @@ -157,6 +154,9 @@ class GitLabDropdown @fullData = data @parseData @fullData + + if @options.filterable + @filterInput.trigger 'keyup' } # Init filterable @@ -178,9 +178,6 @@ class GitLabDropdown callback: (data) => currentIndex = -1 @parseData data - enterCallback: => - if @enterCallback - @selectRowAtIndex 0 # Event listeners @@ -224,6 +221,9 @@ class GitLabDropdown menu.toggleClass PAGE_TWO_CLASS + # Focus first visible input on active page + @dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus() + parseData: (data) -> @renderedData = data @@ -243,7 +243,8 @@ class GitLabDropdown shouldPropagate: (e) => if @options.multiSelect $target = $(e.target) - if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') + + if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link') e.stopPropagation() return false else @@ -378,7 +379,6 @@ class GitLabDropdown selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - if el.hasClass(ACTIVE_CLASS) el.removeClass(ACTIVE_CLASS) field.remove() @@ -389,13 +389,13 @@ class GitLabDropdown else selectedObject else - if !value? - field.remove() - - if not @options.multiSelect + if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.parent().find("input[name='#{fieldName}']").remove() + if !value? + field.remove() + # Toggle active class for the tick mark el.addClass ACTIVE_CLASS diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee new file mode 100644 index 00000000000..d540cc4dc46 --- /dev/null +++ b/app/assets/javascripts/gl_form.js.coffee @@ -0,0 +1,51 @@ +class @GLForm + constructor: (@form) -> + @textarea = @form.find('textarea.js-gfm-input') + + # Before we start, we should clean up any previous data for this form + @destroy() + + # Setup the form + @setupForm() + + @form.data 'gl-form', @ + + destroy: -> + # Clean form listeners + @clearEventListeners() + @form.data 'gl-form', null + + setupForm: -> + isNewForm = @form.is(':not(.gfm-form)') + + @form.removeClass 'js-new-note-form' + + if isNewForm + @form.find('.div-dropzone').remove() + @form.addClass('gfm-form') + disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button') + + # remove notify commit author checkbox for non-commit notes + GitLab.GfmAutoComplete.setup() + new DropzoneInput(@form) + + autosize(@textarea) + + # form and textarea event listeners + @addEventListeners() + + # hide discard button + @form.find('.js-note-discard').hide() + + @form.show() + + clearEventListeners: -> + @textarea.off 'focus' + @textarea.off 'blur' + + addEventListeners: -> + @textarea.on 'focus', -> + $(@).closest('.md-area').addClass 'is-focused' + + @textarea.on 'blur', -> + $(@).closest('.md-area').removeClass 'is-focused' 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/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee new file mode 100644 index 00000000000..afffed63ac5 --- /dev/null +++ b/app/assets/javascripts/issuable.js.coffee @@ -0,0 +1,84 @@ +@Issuable = + init: -> + Issuable.initTemplates() + Issuable.initSearch() + + initTemplates: -> + Issuable.labelRow = _.template( + '<% _.each(labels, function(label){ %> + <span class="label-row"> + <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a> + </span> + <% }); %>' + ) + + initSearch: -> + @timer = null + $('#issue_search') + .off 'keyup' + .on 'keyup', -> + clearTimeout(@timer) + @timer = setTimeout( -> + Issuable.filterResults $('#issue_search_form') + , 500) + + toggleLabelFilters: -> + $filteredLabels = $('.filtered-labels') + if $filteredLabels.find('.label-row').length > 0 + $filteredLabels.removeClass('hidden') + else + $filteredLabels.addClass('hidden') + + filterResults: (form) => + formData = form.serialize() + + $('.issues-holder, .merge-requests-holder').css('opacity', '0.5') + formAction = form.attr('action') + issuesUrl = formAction + issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") + issuesUrl += formData + $.ajax + type: 'GET' + url: formAction + data: formData + complete: -> + $('.issues-holder, .merge-requests-holder').css('opacity', '1.0') + success: (data) -> + $('.issues-holder, .merge-requests-holder').html(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: issuesUrl}, document.title, issuesUrl + Issuable.reload() + Issuable.updateStateFilters() + $filteredLabels = $('.filtered-labels') + + if typeof Issuable.labelRow is 'function' + $filteredLabels.html(Issuable.labelRow(data)) + + Issuable.toggleLabelFilters() + + dataType: "json" + + reload: -> + if Issues.created + Issues.initChecks() + + $('#filter_issue_search').val($('#issue_search').val()) + + updateStateFilters: -> + stateFilters = $('.issues-state-filters') + newParams = {} + paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search'] + + for paramKey in paramKeys + newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' + + if stateFilters.length + stateFilters.find('a').each -> + initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]') + labelNameValues = gl.utils.getParameterValues('label_name[]') + if labelNameValues + labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&') + newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}" + else + newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) + $(this).attr 'href', newUrl diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 2f19513a831..3c491ebfc4c 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -9,21 +9,29 @@ class @IssuableContext $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() - $(document).off("click", ".edit-link").on "click",".edit-link", (e) -> - $block = $(@).parents('.block') - $selectbox = $block.find('.selectbox') - if $selectbox.is(':visible') - $selectbox.hide() - $block.find('.value').show() - else - $selectbox.show() - $block.find('.value').hide() - - if $selectbox.is(':visible') - setTimeout (-> - $block.find('.dropdown-menu-toggle').trigger 'click' - ), 0 - + $(document) + .off 'click', '.issuable-sidebar .dropdown-content a' + .on 'click', '.issuable-sidebar .dropdown-content a', (e) -> + e.preventDefault() + + $(document) + .off 'click', '.edit-link' + .on 'click', '.edit-link', (e) -> + e.preventDefault() + + $block = $(@).parents('.block') + $selectbox = $block.find('.selectbox') + if $selectbox.is(':visible') + $selectbox.hide() + $block.find('.value').show() + else + $selectbox.show() + $block.find('.value').hide() + + if $selectbox.is(':visible') + setTimeout -> + $block.find('.dropdown-menu-toggle').trigger 'click' + , 0 $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 946d83b7bdd..c7d74a12f99 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -10,6 +10,9 @@ class @Issue @initTaskList() @initIssueBtnEventListeners() + @initMergeRequests() + @initRelatedBranches() + initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList @@ -69,3 +72,23 @@ class @Issue type: 'PATCH' url: $('form.js-issuable-update').attr('action') data: patchData + + initMergeRequests: -> + $container = $('#merge-requests') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load referenced merge requests', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) + + initRelatedBranches: -> + $container = $('#related-branches') + + $.getJSON($container.data('url')) + .error -> + new Flash('Failed to load related branches', 'alert') + .success (data) -> + if 'html' of data + $container.html(data.html) diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 0d9f2094c2a..3330e6c68ad 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -1,6 +1,6 @@ @Issues = init: -> - Issues.initSearch() + Issues.created = true Issues.initChecks() $("body").on "ajax:success", ".close_issue, .reopen_issue", -> @@ -15,10 +15,6 @@ else $(this).html totalIssues - 1 - reload: -> - Issues.initChecks() - $('#filter_issue_search').val($('#issue_search').val()) - initChecks: -> $(".check_all_issues").click -> $(".selected_issue").prop("checked", @checked) @@ -26,51 +22,6 @@ $(".selected_issue").bind "change", Issues.checkChanged - # Update state filters if present in page - updateStateFilters: -> - stateFilters = $('.issues-state-filters') - newParams = {} - paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] - - for paramKey in paramKeys - newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or '' - - if stateFilters.length - stateFilters.find('a').each -> - initialUrl = $(this).attr 'href' - $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl) - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout( -> - Issues.filterResults $("#issue_search_form") - , 500) - - filterResults: (form) => - $('.issues-holder, .merge-requests-holder').css("opacity", '0.5') - formAction = form.attr('action') - formData = form.serialize() - issuesUrl = formAction - issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}") - issuesUrl += formData - - $.ajax - type: "GET" - url: formAction - data: formData - complete: -> - $('.issues-holder, .merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.issues-holder, .merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issuesUrl}, document.title, issuesUrl - Issues.reload() - Issues.updateStateFilters() - dataType: "json" - checkChanged: -> checked_issues = $(".selected_issue:checked") if checked_issues.length > 0 diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 90385621879..85517b18c5a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,7 +6,7 @@ class @LabelsSelect labelUrl = $dropdown.data('labels') issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel? + if selectedLabel? and not $dropdown.hasClass 'js-multiselect' selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') @@ -16,33 +16,32 @@ class @LabelsSelect abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') + $form = $dropdown.closest('form') $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') $value = $block.find('.value') - $loading = $block.find('.block-loading').fadeOut() - - if newLabelField.length - $newLabelCreateButton = $('.js-new-label-btn') - $colorPreview = $('.js-dropdown-label-color-preview') - $newLabelError = $dropdown.parent().find('.js-label-error') - $newLabelError.hide() + $newLabelError = $('.js-label-error') + $colorPreview = $('.js-dropdown-label-color-preview') + $newLabelCreateButton = $('.js-new-label-btn') - # Suggested colors in the dropdown to chose from pre-chosen colors - $('.suggest-colors-dropdown a').on 'click', (e) -> + $newLabelError.hide() + $loading = $block.find('.block-loading').fadeOut() issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> - <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> - <span class="label color-label" style="background-color: <%= label.color %>;"> - <%= label.title %> + <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>"> + <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>; color: <%= label.text_color %>;"> + <%= _.escape(label.title) %> </span> </a> <% }); %>' - ); + ) labelNoneHTMLTemplate = _.template('<div class="light">None</div>') - if newLabelField.length and $dropdown.hasClass 'js-extra-options' + if newLabelField.length + + # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on "click", (e) -> e.preventDefault() e.stopPropagation() @@ -81,26 +80,25 @@ class @LabelsSelect enableLabelCreateButton = -> if newLabelField.val() isnt '' and newColorField.val() isnt '' $newLabelError.hide() - $('.js-new-label-btn').disable() - - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $('.js-new-label-btn').enable() - - if label.message? - $newLabelError - .text label.message - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - $newLabelCreateButton.enable() else $newLabelCreateButton.disable() + saveLabel = -> + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $newLabelCreateButton.enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + newLabelField.on 'keyup change', enableLabelCreateButton newColorField.on 'keyup change', enableLabelCreateButton @@ -111,24 +109,7 @@ class @LabelsSelect .on 'click', (e) -> e.preventDefault() e.stopPropagation() - - if newLabelField.val() isnt '' and newColorField.val() isnt '' - $newLabelError.hide() - $('.js-new-label-btn').disable() - - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $('.js-new-label-btn').enable() - - if label.message? - $newLabelError - .text label.message - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + saveLabel() saveLabelData = -> selected = $dropdown @@ -165,11 +146,13 @@ class @LabelsSelect .html(template) $sidebarCollapsedValue.text(labelCount) + $('.has-tooltip', $value).tooltip(container: 'body') + $value .find('a') .each((i) -> setTimeout(=> - glAnimate($(@), 'pulse') + gl.animate.animate($(@), 'pulse') ,200 * i ) ) @@ -198,18 +181,23 @@ class @LabelsSelect callback data renderRow: (label) -> - selectedClass = '' - if $selectbox.find("input[type='hidden']\ - [name='#{$dropdown.data('field-name')}']\ - [value='#{label.id}']").length - selectedClass = 'is-active' + removesAll = label.id is 0 or not label.id? + + selectedClass = [] + if $form.find("input[type='hidden']\ + [name='#{$dropdown.data('fieldName')}']\ + [value='#{this.id(label)}']").length + selectedClass.push 'is-active' + + if $dropdown.hasClass('js-multiselect') and removesAll + selectedClass.push 'dropdown-clear-active' color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" "<li> - <a href='#' class='#{selectedClass}'> + <a href='#' class='#{selectedClass.join(' ')}'> #{color} - #{label.title} + #{_.escape(label.title)} </a> </li>" filterable: true @@ -217,37 +205,56 @@ class @LabelsSelect fields: ['title'] selectable: true - toggleLabel: (selected) -> + toggleLabel: (selected, el) -> + selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') + if selected and selected.title? - selected.title + if selected_labels.length > 1 + "#{selected.title} +#{selected_labels.length - 1} more" + else + selected.title + else if not selected and selected_labels.length isnt 0 + if selected_labels.length > 1 + "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more" + else if selected_labels.length is 1 + $(selected_labels).text() else defaultLabel fieldName: $dropdown.data('field-name') id: (label) -> - if label.isAny? - '' - else if $dropdown.hasClass "js-filter-submit" - label.title + if $dropdown.hasClass("js-filter-submit") and not label.isAny? + _.escape label.title else label.id hidden: -> + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is 'projects:merge_requests:index' + $selectbox.hide() # display:block overrides the hide-collapse rule $value.removeAttr('style') if $dropdown.hasClass 'js-multiselect' - saveLabelData() + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='#{$dropdown.data('fieldName')}']") + Issuable.filterResults $dropdown.closest('form') + else if $dropdown.hasClass('js-filter-submit') + $dropdown.closest('form').submit() + else + saveLabelData() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - + isMRIndex = page is 'projects:merge_requests:index' if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedLabel = label.title - - Issues.filterResults $dropdown.closest('form') + if not $dropdown.hasClass 'js-multiselect' + selectedLabel = label.title + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee index 8f892b5a2b9..ec3b44d6126 100644 --- a/app/assets/javascripts/lib/animate.js.coffee +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -1,13 +1,39 @@ ((w) -> + if not w.gl? then w.gl = {} + if not gl.animate? then gl.animate = {} - w.glAnimate = ($el, animation, done) -> + gl.animate.animate = ($el, animation, options, done) -> + if options?.cssStart? + $el.css(options.cssStart) $el - .removeClass() + .removeClass(animation + ' animated') .addClass(animation + ' animated') .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass() + $(this).removeClass(animation + ' animated') + if done? + done() + if options?.cssEnd? + $el.css(options.cssEnd) return return - return + gl.animate.animateEach = ($els, animation, time, options, done) -> + dfd = $.Deferred() + if not $els.length + dfd.resolve() + $els.each((i) -> + setTimeout(=> + $this = $(@) + gl.animate.animate($this, animation, options, => + if i is $els.length - 1 + dfd.resolve() + if done? + done() + ) + ,time * i + ) + return + ) + return dfd.promise() + return ) window
\ No newline at end of file diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee index abd556e0b4e..6a00932c028 100644 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -3,16 +3,20 @@ w.gl ?= {} w.gl.utils ?= {} - w.gl.utils.getUrlParameter = (sParam) -> + # Returns an array containing the value(s) of the + # of the key passed as an argument + w.gl.utils.getParameterValues = (sParam) -> sPageURL = decodeURIComponent(window.location.search.substring(1)) sURLVariables = sPageURL.split('&') sParameterName = undefined + values = [] i = 0 while i < sURLVariables.length sParameterName = sURLVariables[i].split('=') if sParameterName[0] is sParam - return if sParameterName[1] is undefined then true else sParameterName[1] + values.push(sParameterName[1]) i++ + values # # # @param {Object} params - url keys and value to merge @@ -28,4 +32,12 @@ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" newUrl + # removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = (url, param) -> + url = decodeURIComponent(url) + urlVariables = url.split('&') + ( + variables for variables in urlVariables when variables.indexOf(param) is -1 + ).join('&') + ) window diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index ef0b534a709..372732d0aac 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -85,8 +85,10 @@ class @MergeRequestTabs scrollToElement: (container) -> if window.location.hash - $el = $("div#{container} #{window.location.hash}") - $('body').scrollTo($el.offset().top) if $el.length + navBarHeight = $('.navbar-gitlab').outerHeight() + + $el = $("#{container} #{window.location.hash}:not(.match)") + $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length # Activate a tab based on the current action activateTab: (action) -> @@ -152,12 +154,39 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.querySelector("div#diffs").innerHTML = data.html + $('#diffs').html data.html gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) - $('div#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @scrollToElement("#diffs") + @highlighSelectedLine() + + $(document) + .off 'click', '.diff-line-num a' + .on 'click', '.diff-line-num a', (e) => + e.preventDefault() + window.location.hash = $(e.currentTarget).attr 'href' + @highlighSelectedLine() + @scrollToElement("#diffs") + + highlighSelectedLine: -> + $('.hll').removeClass 'hll' + locationHash = window.location.hash + + if locationHash isnt '' + hashClassString = ".#{locationHash.replace('#', '')}" + $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) + + if not $diffLine.is 'tr' + $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") + else + $diffLine = $diffLine.find('td') + + 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/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee deleted file mode 100644 index b3c73ffce5d..00000000000 --- a/app/assets/javascripts/merge_requests.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -# -# * Filter merge requests -# -@MergeRequests = - init: -> - MergeRequests.initSearch() - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout(MergeRequests.filterResults, 500) - - filterResults: => - form = $("#issue_search_form") - search = $("#issue_search").val() - $('.merge-requests-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '?' + form.serialize() - - $.ajax - type: "GET" - url: form.attr('action') - data: form.serialize() - complete: -> - $('.merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issues_url}, document.title, issues_url - MergeRequests.reload() - dataType: "json" - - reload: -> - $('#filter_issue_search').val($('#issue_search').val()) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 6bd4e885a03..345a0e447af 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -24,7 +24,7 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( - '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>' + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>' ) milestoneLinkNoneTemplate = '<div class="light">None</div>' @@ -71,7 +71,7 @@ class @MilestoneSelect defaultLabel fieldName: $dropdown.data('field-name') text: (milestone) -> - milestone.title + _.escape(milestone.title) id: (milestone) -> if !useId milestone.name @@ -97,7 +97,7 @@ class @MilestoneSelect selectedMilestone = selected.name else selectedMilestone = '' - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index a67890200dd..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 => @@ -283,32 +295,10 @@ class @Notes show the form ### setupNoteForm: (form) -> - disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") - form.removeClass "js-new-note-form" - form.find('.div-dropzone').remove() - - # hide discard button - form.find('.js-note-discard').hide() - - # setup preview buttons - previewButton = form.find(".js-md-preview-button") + new GLForm form textarea = form.find(".js-note-text") - textarea.on "input", -> - if $(this).val().trim() isnt "" - previewButton.removeClass("turn-off").addClass "turn-on" - else - previewButton.removeClass("turn-on").addClass "turn-off" - - textarea.on 'focus', -> - $(this).closest('.md-area').addClass 'is-focused' - - textarea.on 'blur', -> - $(this).closest('.md-area').removeClass 'is-focused' - - autosize(textarea) - new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -317,11 +307,6 @@ class @Notes form.find("#note_noteable_id").val() ] - # remove notify commit author checkbox for non-commit notes - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) - form.show() - ### Called in response to the new note form being submitted @@ -370,39 +355,38 @@ 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" form = note.find(".note-edit-form") - isNewForm = form.is(':not(.gfm-form)') - if isNewForm - form.addClass('gfm-form') + form.addClass('current-note-edit-form') # Show the attachment delete link note.find(".js-note-attachment-delete").show() - # Setup markdown form - if isNewForm - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) - - textarea = form.find("textarea") - textarea.focus() - - if isNewForm - autosize(textarea) - - # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). - # The textarea has the correct value, Chrome just won't show it unless we - # modify it, so let's clear it and re-set it! - value = textarea.val() - textarea.val "" - textarea.val value - - if isNewForm - disableButtonIfEmptyField textarea, form.find(".js-comment-button") + done = ($noteText) -> + # Neat little trick to put the cursor at the end + noteTextVal = $noteText.val() + $noteText.val('').val(noteTextVal); + + 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 @@ -559,6 +543,9 @@ class @Notes removeDiscussionNoteForm: (form)-> row = form.closest("tr") + glForm = form.data 'gl-form' + glForm.destroy() + form.find(".js-note-text").data("autosave").reset() # show the reply button (will only work for replies) @@ -570,7 +557,6 @@ class @Notes # only remove the form form.remove() - cancelDiscussionForm: (e) => e.preventDefault() form = $(e.target).closest(".js-discussion-note-form") diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index f4a2562885d..26a12423521 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -45,9 +45,10 @@ class @Profile saveForm: -> self = @ - formData = new FormData(@form[0]) - formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png') + + avatarBlob = @avatarGlCrop.getBlob() + formData.append('user[avatar]', avatarBlob, 'avatar.png') if avatarBlob? $.ajax url: @form.attr('action') diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee index 67403554340..2d084b76cfe 100644 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -1,10 +1,12 @@ class @Sidebar constructor: (currentUser) -> + @sidebar = $('aside') + @addEventListeners() addEventListeners: -> - $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked) - $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden) + @sidebar.on('click', '.sidebar-collapsed-icon', @, @sidebarCollapseClicked) + $('.dropdown').on('hidden.gl.dropdown', @, @onSidebarDropdownHidden) $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) @@ -30,26 +32,56 @@ class @Sidebar else i.show() - sidebarCollapseClicked: (e) -> + sidebar = e.data e.preventDefault() $block = $(@).closest('.block') + sidebar.openDropdown($block); - $('aside') - .find('.gutter-toggle') - .trigger('click') - $editLink = $block.find('.edit-link') + openDropdown: (blockOrName) -> + $block = if _.isString(blockOrName) then @getBlock(blockOrName) else blockOrName + + $block.find('.edit-link').trigger('click') - if $editLink.length - $editLink.trigger('click') - $block.addClass('collapse-after-update') - $('.page-with-sidebar').addClass('with-overlay') + if not @isOpen() + @setCollapseAfterUpdate($block) + @toggleSidebar('open') - sidebarDropdownHidden: (e) -> + setCollapseAfterUpdate: ($block) -> + $block.addClass('collapse-after-update') + $('.page-with-sidebar').addClass('with-overlay') + + onSidebarDropdownHidden: (e) -> + sidebar = e.data + e.preventDefault() $block = $(@).closest('.block') + sidebar.sidebarDropdownHidden($block) + + sidebarDropdownHidden: ($block) -> if $block.hasClass('collapse-after-update') $block.removeClass('collapse-after-update') $('.page-with-sidebar').removeClass('with-overlay') - $('aside') - .find('.gutter-toggle') - .trigger('click')
\ No newline at end of file + @toggleSidebar('hide') + + triggerOpenSidebar: -> + @sidebar + .find('.js-sidebar-toggle') + .trigger('click') + + toggleSidebar: (action = 'toggle') -> + if action is 'toggle' + @triggerOpenSidebar() + + if action is 'open' + @triggerOpenSidebar() if not @isOpen() + + if action is 'hide' + @triggerOpenSidebar() is @isOpen() + + isOpen: -> + @sidebar.is('.right-sidebar-expanded') + + getBlock: (name) -> + @sidebar.find(".block.#{name}") + + diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index 100e3aac535..f3d66004138 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -2,34 +2,35 @@ class @Shortcuts constructor: -> @enabledHelp = [] Mousetrap.reset() - Mousetrap.bind('?', @selectiveHelp) + Mousetrap.bind('?', @onToggleHelp) Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? - selectiveHelp: (e) => - Shortcuts.showHelp(e, @enabledHelp) + onToggleHelp: (e) => + e.preventDefault() + @toggleHelp(@enabledHelp) toggleMarkdownPreview: (e) => $(document).triggerHandler('markdown-preview:toggle', [e]) - @showHelp: (e, location) -> - if $('#modal-shortcuts').length > 0 - $('#modal-shortcuts').modal('show') - else - url = '/help/shortcuts' - url = gon.relative_url_root + url if gon.relative_url_root? - $.ajax( - url: url, - dataType: 'script', - success: (e) -> - if location and location.length > 0 - $(l).show() for l in location - else - $('.hidden-shortcut').show() - $('.js-more-help-button').remove() - ) - e.preventDefault() + toggleHelp: (location) -> + $modal = $('#modal-shortcuts') + + if $modal.length + $modal.modal('toggle') + return + + $.ajax( + url: gon.shortcuts_path, + dataType: 'script', + success: (e) -> + if location and location.length > 0 + $(l).show() for l in location + else + $('.hidden-shortcut').show() + $('.js-more-help-button').remove() + ) @focusSearch: (e) -> $('#search').focus() diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index bbf02f1db24..ad9b3c1c6bf 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -4,18 +4,8 @@ class @ShortcutsIssuable extends ShortcutsNavigation constructor: (isMergeRequest) -> super() - Mousetrap.bind('a', -> - $('.block.assignee .edit-link').trigger('click') - return false - ) - Mousetrap.bind('m', -> - $('.block.milestone .edit-link').trigger('click') - return false - ) - Mousetrap.bind('r', => - @replyWithSelectedText() - return false - ) + Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) + Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) Mousetrap.bind('j', => @prevIssue() return false @@ -28,7 +18,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation @editIssue() return false ) - + Mousetrap.bind('l', @openSidebarDropdown.bind(@, 'labels')) if isMergeRequest @enabledHelp.push('.hidden-shortcut.merge_requests') @@ -71,3 +61,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation editIssue: -> $editBtn = $('.issuable-edit') Turbolinks.visit($editBtn.attr('href')) + + openSidebarDropdown: (name) -> + sidebar.openDropdown(name) + return false diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee index 8decaedd87b..f39504e0645 100644 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ b/app/assets/javascripts/shortcuts_navigation.coffee @@ -14,6 +14,7 @@ class @ShortcutsNavigation extends Shortcuts Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests')) Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki')) Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets')) + Mousetrap.bind('i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue')) @enabledHelp.push('.hidden-shortcut.project') @findAndFollowLink: (selector) -> 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/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index 00d2b641723..10bef96f43d 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -1,5 +1,11 @@ class @Todos - constructor: (@name) -> + constructor: (opts = {}) -> + { + @el = $('.js-todos-options') + } = opts + + @perPage = @el.data('perPage') + @clearListeners() @initBtnListeners() @@ -26,6 +32,7 @@ class @Todos dataType: 'json' data: '_method': 'delete' success: (data) => + @redirectIfNeeded data.count @clearDone $this.closest('li') @updateBadges data @@ -57,11 +64,46 @@ class @Todos $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + getTotalPages: -> + @el.data('totalPages') + + getCurrentPage: -> + @el.data('currentPage') + + getTodosPerPage: -> + @el.data('perPage') + + redirectIfNeeded: (total) -> + currPages = @getTotalPages() + currPage = @getCurrentPage() + + # Refresh if no remaining Todos + if not total + location.reload() + return + + # Do nothing if no pagination + return if not currPages + + newPages = Math.ceil(total / @getTodosPerPage()) + url = location.href # Includes query strings + + # If new total of pages is different than we have now + if newPages isnt currPages + # Redirect to previous page if there's one available + if currPages > 1 and currPage is currPages + pageParams = + page: currPages - 1 + url = gl.utils.mergeUrlParams(pageParams, url) + + Turbolinks.visit(url) + goToTodoUrl: (e)-> todoLink = $(this).data('url') return unless todoLink - if e.metaKey + # Allow Meta-Click or Mouse3-click to open in a new tab + if e.metaKey or e.which is 2 e.preventDefault() window.open(todoLink,'_blank') else diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index eee9b6e690e..b80b1b861cc 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') @@ -157,7 +158,7 @@ class @UsersSelect if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) selectedId = user.id - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else @@ -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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e8c0172680d..18a74fe21a0 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -144,6 +144,10 @@ } } +.btn-lg { + padding: 12px 20px; +} + .btn-transparent { color: $btn-transparent-color; background-color: transparent; diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 0b3af592d4a..11f39d583bd 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -54,6 +54,10 @@ fill: #254e77 !important; } + .future { + visibility: hidden; + } + .domain-background { fill: none; shape-rendering: crispedges; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ba6c7930cdc..239eaf15cc1 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -248,7 +248,7 @@ .dropdown-title { position: relative; - padding: 0 25px 15px; + padding: 0 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -278,7 +278,7 @@ right: 5px; width: 20px; height: 20px; - top: -1px; + top: -3px; } .dropdown-menu-back { @@ -320,7 +320,7 @@ } } -.dropdown-input-field { +.dropdown-input-field, .default-dropdown-input { width: 100%; padding: 0 7px; color: $dropdown-input-color; @@ -358,6 +358,13 @@ border-top: 1px solid $dropdown-divider-color; } +.dropdown-due-date-footer { + padding-top: 0; + margin-left: 10px; + margin-right: 10px; + border-top: 0; +} + .dropdown-footer-list { font-size: 14px; @@ -395,3 +402,122 @@ height: 15px; border-radius: $border-radius-base; } + +.dropdown-menu-due-date { + .dropdown-content { + max-height: 230px; + } + + .ui-widget { + table { + margin: 0; + } + + &.ui-datepicker-inline { + padding: 0 10px; + border: 0; + width: 100%; + } + + .ui-datepicker-header { + padding: 0 8px 10px; + border: 0; + + .ui-icon { + background: none; + font-size: 20px; + text-indent: 0; + + &:before { + display: block; + position: relative; + top: -2px; + color: $dropdown-title-btn-color; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + } + } + + .ui-state-active, + .ui-state-hover { + color: $md-link-color; + background-color: $calendar-hover-bg; + } + + .ui-datepicker-prev, + .ui-datepicker-next { + top: 0; + height: 15px; + cursor: pointer; + + &:hover { + background-color: transparent; + border: 0; + + .ui-icon:before { + color: $md-link-color; + } + } + } + + .ui-datepicker-prev { + left: 0; + + .ui-icon:before { + content: '\f104'; + text-align: left; + } + } + + .ui-datepicker-next { + right: 0; + + .ui-icon:before { + content: '\f105'; + text-align: right; + } + } + + td { + padding: 0; + border: 1px solid $calendar-border-color; + + &:first-child { + border-left: 0; + } + + &:last-child { + border-right: 0; + } + + a { + line-height: 17px; + border: 0; + border-radius: 0; + } + } + + .ui-datepicker-title { + color: $gl-gray; + font-size: 15px; + line-height: 1; + font-weight: normal; + } + } + + th { + padding: 2px 0; + color: $calendar-header-color; + font-weight: normal; + text-transform: lowercase; + border-top: 1px solid $calendar-border-color; + } + + .ui-datepicker-unselectable { + background-color: $calendar-unselectable-bg; + } +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 789df42fb66..61d9954c6c8 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -38,12 +38,14 @@ .filename { &.old { + display: inline-block; span.idiff { background-color: #f8cbcb; } } &.new { + display: inline-block; span.idiff { background-color: #a6f3a6; } @@ -82,10 +84,6 @@ } } - &.blob_file { - - } - &.blob-no-preview { background: #eee; text-shadow: 0 1px 2px #fff; @@ -129,6 +127,11 @@ td.line-numbers { float: none; border-left: 1px solid #ddd; + + i { + float: none; + margin-right: 0; + } } td.lines { padding: 0; diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 5ae0520fd7b..f4d35c4b4b1 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -1,24 +1,6 @@ /** * Styles that apply to all GFM related forms. */ -.issue-form, .merge-request-form, .wiki-form { - .description { - height: 16em; - border-top-left-radius: 0; - } -} - -.wiki-form { - .description { - height: 26em; - } -} - -.milestone-form { - .description { - height: 14em; - } -} .gfm-commit, .gfm-commit_range { font-family: $monospace_font; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3f015427d07..c303380764b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -71,7 +71,7 @@ header { .header-content { position: relative; height: $header-height; - padding-right: 20px; + padding-right: 40px; @media (min-width: $screen-sm-min) { padding-right: 0; @@ -122,6 +122,10 @@ header { } } + .project-item-select-holder { + display: inline; + } + .impersonation i { color: $red-normal; } 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/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index c8f86d60e3b..fd885b38680 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,22 +1,15 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - margin-bottom: -5px; - - .div-dropzone-focus { - border-color: #66afe9 !important; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important; - outline: 0 !important; - } .div-dropzone-hover { position: absolute; top: 50%; left: 50%; - margin-top: -0.5em; - margin-left: -0.6em; + margin-top: -11.5px; + margin-left: -15px; opacity: 0; - font-size: 50px; + font-size: 30px; transition: opacity 200ms ease-in-out; pointer-events: none; } @@ -97,3 +90,12 @@ box-shadow: none; width: 100%; } + +.md { + &.md-preview-holder { + code { + white-space: pre-wrap; + word-break: keep-all; + } + } +} 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/selects.scss b/app/assets/stylesheets/framework/selects.scss index b2fab387e17..eae5f062dda 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -121,9 +121,6 @@ } } -.select2-container-multi .select2-choices .select2-search-choice { -} - .select2-drop-active { margin-top: 6px; font-size: 14px; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index b91f2f6f898..29501069d27 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -11,7 +11,7 @@ border-bottom: 1px solid $border-white-light; &:target { - background: $row-hover; + background: $line-target-blue; } .avatar { @@ -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/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index dd42db1840f..96bab7880c2 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -43,7 +43,6 @@ @import "bootstrap/modals"; @import "bootstrap/tooltip"; @import "bootstrap/popovers"; -@import "bootstrap/carousel"; // Utility classes .clearfix { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 7b2aada5a0d..0a5b4b8834c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -250,14 +250,6 @@ a > code { * Textareas intended for GFM * */ -.js-gfm-input { - font-family: $monospace_font; - color: $gl-text-color; -} - -.md-preview { -} - .strikethrough { text-decoration: line-through; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0b6be86ce6a..ef37ade3b7b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -150,6 +150,7 @@ $light-grey-header: #faf9f9; */ $gl-primary: $blue-normal; $gl-success: $green-normal; +$gl-success-focus: rgba($gl-success, .4); $gl-info: $blue-normal; $gl-warning: $orange-normal; $gl-danger: $red-normal; @@ -167,8 +168,12 @@ $line-removed: #fbe9eb; $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; +$line-number-select: #fbf2da; $match-line: #fafafa; $table-border-gray: #f0f0f0; +$line-target-blue: #eaf3fc; +$line-select-yellow: #fcf8e7; +$line-select-yellow-dark: #f0e2bd; /* * Fonts */ @@ -240,3 +245,8 @@ $note-form-border-color: #e5e5e5; $note-toolbar-color: #959494; $zen-control-hover-color: #111; + +$calendar-header-color: #b8b8b8; +$calendar-hover-bg: #ecf3fe; +$calendar-border-color: rgba(#000, .1); +$calendar-unselectable-bg: #faf9f9; diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 47673944896..77a73dc379b 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #557; + border-color: darken(#557, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 806401c21ae..80a509a7c1a 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #49483e; + border-color: darken(#49483e, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); } @@ -105,8 +111,6 @@ .vg { color: #f8f8f2 } /* Name.Variable.Global */ .vi { color: #f8f8f2 } /* Name.Variable.Instance */ .il { color: #ae81ff } /* Literal.Number.Integer.Long */ - - .gh { } /* Generic Heading & Diff Header */ .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 6a809d4dfd2..c62bd021aef 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #174652; + border-color: darken(#174652, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index c482a1258f7..524cfaf90c3 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -21,6 +21,12 @@ // Diff line .line_holder { + td.diff-line-num.hll:not(.empty-cell), + td.line_content.hll:not(.empty-cell) { + background-color: #ddd8c5; + border-color: darken(#ddd8c5, 15%); + } + .diff-line-num.new, .line_content.new { @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 28331f59754..31a4e3deaac 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -21,6 +21,7 @@ // Diff line .line_holder { + .diff-line-num { &.old { background-color: $line-number-old; @@ -31,11 +32,16 @@ background-color: $line-number-new; border-color: $line-added-dark; } + + &.hll:not(.empty-cell) { + background-color: $line-number-select; + border-color: $line-select-yellow-dark; + } } .line_content { &.old { - background: $line-removed; + background-color: $line-removed; span.idiff { background-color: $line-removed-dark; @@ -52,7 +58,11 @@ &.match { color: $black-transparent; - background: $match-line; + background-color: $match-line; + } + + &.hll:not(.empty-cell) { + background-color: $line-select-yellow; } } } 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/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss new file mode 100644 index 00000000000..125f495d6d4 --- /dev/null +++ b/app/assets/stylesheets/pages/confirmation.scss @@ -0,0 +1,18 @@ +.well-confirmation { + margin-bottom: 20px; + border-bottom: 1px solid #eee; + + > h1 { + font-weight: 400; + } + + .lead { + margin-bottom: 20px; + } +} + +.confirmation-content { + a { + color: $md-link-color; + } +} diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5917f089720..3438dbe4958 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 { @@ -41,4 +36,11 @@ } } } + + .wiki { + code { + white-space: pre-wrap; + word-break: keep-all; + } + } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d0855f66911..e7c8198ba45 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -34,6 +34,7 @@ background: #fff; color: #333; border-radius: 0 0 3px 3px; + -webkit-overflow-scrolling: auto; .unfold { cursor: pointer; @@ -67,8 +68,26 @@ 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; + white-space: pre-wrap; } } } @@ -317,7 +336,7 @@ } .diff-file .line_content { - white-space: pre; + white-space: pre-wrap; } .diff-wrap-lines .line_content { @@ -391,3 +410,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/editor.scss b/app/assets/stylesheets/pages/editor.scss index 0f0592a0ab8..8981f070a20 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -26,6 +26,10 @@ line-height: 42px; padding-top: 7px; padding-bottom: 7px; + + .pull-right { + height: 20px; + } } .editor-ref { @@ -53,4 +57,9 @@ .select2 { float: right; } + + .encoding-selector, + .license-selector { + display: inline-block; + } } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 4e5c4ed84b6..f7f9a9bb770 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -18,9 +18,6 @@ } .graphs { - .graph-author-commits-count { - } - .graph-author-email { float: right; color: #777; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 604f1700cf8..ee95bdf488e 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,8 +59,10 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { margin: -$gl-padding+1; + margin-top: 15px; } } 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 8b6f37f21b5..1cf3023ecc9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -128,6 +128,7 @@ top: 58px; bottom: 0; right: 0; + z-index: 10; transition: width .3s; background: $gray-light; padding: 10px 20px; @@ -173,12 +174,6 @@ } } - .subscribe-button { - span { - margin-top: 0; - } - } - &.right-sidebar-collapsed { /* Extra small devices (phones, less than 768px) */ display: none; @@ -247,16 +242,20 @@ } } - .btn { + .issuable-pager { background: $gray-normal; border: 1px solid $border-gray-normal; &:hover { background: $gray-dark; border: 1px solid $border-gray-dark; } + + &.btn-primary { + @extend .btn-primary + } } - a:not(.btn) { + a:not(.issuable-pager) { &:hover { color: $md-link-color; text-decoration: none; @@ -279,10 +278,6 @@ } } -.btn-default.gutter-toggle { - margin-top: 4px; -} - .detail-page-description { small { color: $gray-darkest; @@ -322,3 +317,56 @@ color: #8c8c8c; } } + +.issuable-form-padding-top { + @media (min-width: $screen-sm-min) { + 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 3e0a3140be7..e179bdf0048 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -79,19 +79,30 @@ color: $white-light; } +@mixin labels-mobile { + @media (max-width: $screen-xs-min) { + display: block; + width: 100%; + margin-left: 0; + padding: 10px 0; + } +} + + .manage-labels-list { - .prepend-left-10 { + .prepend-left-10, .prepend-description-left { display: inline-block; width: 40%; vertical-align: middle; - @media (max-width: $screen-xs-min) { - display: block; - width: 100%; - margin-left: 0; - padding: 10px 0; - } + @include labels-mobile; + } + + .prepend-description-left { + width: 57%; + + @include labels-mobile; } .pull-info-right { @@ -106,7 +117,7 @@ padding: 6px; color: $gl-text-color; - &.subscribe-button { + &.label-subscribe-button { padding-left: 0; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index f4da17fadaa..7fa13e66b43 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -40,7 +40,9 @@ } .note-textarea { + display: block; padding: 10px 0; + color: $gl-gray; font-family: $regular_font; border: 0; @@ -60,11 +62,11 @@ padding: $gl-padding-top $gl-padding; border: 1px solid $note-form-border-color; border-radius: $border-radius-base; + transition: border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; &.is-focused { - border-color: $focus-border-color; - box-shadow: 0 0 2px rgba(#000, .2), - 0 0 4px rgba($focus-border-color, .4); + @extend .form-control:focus; .comment-toolbar, .nav-links { @@ -72,15 +74,14 @@ } } - p { - code { - white-space: normal; - } + &.is-dropzone-hover { + border-color: $gl-success; + box-shadow: 0 0 2px $black-transparent, + 0 0 4px $gl-success-focus; - pre { - code { - white-space: pre; - } + .comment-toolbar, + .nav-links { + border-color: $gl-success; } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e421a31549a..55b1ad97eb0 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -81,16 +81,8 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - p { - code { - white-space: normal; - } - - pre { - code { - white-space: pre; - } - } + code { + white-space: pre-wrap; } // Reset ul style types since we're nested inside a ul already @@ -117,6 +109,10 @@ ul.notes { border-color: darken(#f5f5f5, 8%); margin: 10px 0; } + + code { + word-break: keep-all; + } } a { @@ -137,7 +133,7 @@ ul.notes { margin-right: 10px; } .line_content { - white-space: pre; + white-space: pre-wrap; } } @@ -191,6 +187,9 @@ ul.notes { } } + .author_link { + color: $gl-gray; + } } .note-headline-light, @@ -198,6 +197,12 @@ ul.notes { color: $notes-light-color; } +.discussion-headline-light { + a { + color: $gl-link-color; + } +} + /** * Actions for Discussions/Notes */ @@ -209,6 +214,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 +292,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 { @@ -287,17 +302,12 @@ ul.notes { padding: 4px; font-size: 16px; color: $gl-link-color; - margin-left: -60px; + margin-left: -56px; 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..ec22548ddeb 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 @@ -66,6 +75,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :admin_notification_email, :user_oauth_applications, :shared_runners_enabled, + :shared_runners_text, :max_artifacts_size, :metrics_enabled, :metrics_host, @@ -82,6 +92,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_enabled, :akismet_api_key, :email_author_in_body, + :repository_checks_enabled, + :metrics_packet_size, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 0bd19c49d8f..93c4894ea0f 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController end def hook_params - params.require(:hook).permit(:url, :enable_ssl_verification) + params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events) end end 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/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb index f63b703d101..586f97c5eb4 100644 --- a/app/controllers/concerns/filter_projects.rb +++ b/app/controllers/concerns/filter_projects.rb @@ -10,6 +10,8 @@ module FilterProjects def filter_projects(projects) projects = projects.search(params[:filter_projects]) if params[:filter_projects].present? projects = projects.non_archived if params[:archived].blank? + projects = projects.personal(current_user) if params[:personal].present? && current_user + projects end end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index af1faca93f6..7b66ad3f92c 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -1,7 +1,16 @@ class ConfirmationsController < Devise::ConfirmationsController + def almost_there + flash[:notice] = nil + render layout: "devise_empty" + end + protected + def after_resending_confirmation_instructions_path_for(resource) + users_almost_there_path + end + def after_confirmation_path_for(resource_name, resource) if signed_in?(resource_name) after_sign_in_path_for(resource) 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/application_controller.rb b/app/controllers/projects/application_controller.rb index 74150ad606b..be872a93fee 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -83,8 +83,7 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - view = params[:view] || cookies[:diff_view] - cookies.permanent[:diff_view] = params[:view] = view if view + cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? end def builds_enabled diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index f159e169f6d..b8b9e78427d 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,7 +1,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] - before_action :authorize_update_build!, except: [:index, :show, :status] + before_action :authorize_update_build!, except: [:index, :show, :status, :raw] layout 'project' def index @@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController notice: "Build has been sucessfully erased!" end + def raw + if @build.has_trace? + send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline' + else + render_404 + end + end + private def build diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 72078c3cc68..a202cb38692 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_read_commit_status!, only: [:builds] before_action :commit before_action :define_show_vars, only: [:show, :builds] - before_action :authorize_edit_tree!, only: [:revert] + before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] def show apply_diff_view_cookie! @@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController end def revert - assign_revert_commit_vars + assign_change_commit_vars(@commit.revert_branch_name) return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.", - success_path: successful_revert_path, failure_path: failed_revert_path) + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + success_path: successful_change_path, failure_path: failed_change_path) end + + def cherry_pick + assign_change_commit_vars(@commit.cherry_pick_branch_name) + + return render_404 if @target_branch.blank? - private - - def revert_type_title - @commit.merged_merge_request ? 'merge request' : 'commit' + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + success_path: successful_change_path, failure_path: failed_change_path) end - def successful_revert_path + private + + def successful_change_path return referenced_merge_request_url if @commit.merged_merge_request namespace_project_commits_url(@project.namespace, @project, @target_branch) end - def failed_revert_path + def failed_change_path return referenced_merge_request_url if @commit.merged_merge_request namespace_project_commit_url(@project.namespace, @project, params[:id]) @@ -116,14 +121,13 @@ class Projects::CommitController < Projects::ApplicationController @builds = Ci::Build.where(commit: ci_commits) end - def assign_revert_commit_vars + def assign_change_commit_vars(mr_source_branch) @commit = project.commit(params[:id]) @target_branch = params[:target_branch] - @mr_source_branch = @commit.revert_branch_name + @mr_source_branch = mr_source_branch @mr_target_branch = @target_branch @commit_params = { commit: @commit, - revert_type_title: revert_type_title, create_merge_request: params[:create_merge_request].present? || different_project? } end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 4159e53bfa9..606552fa853 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -7,10 +7,12 @@ class Projects::GroupLinksController < Projects::ApplicationController end def create - link = project.project_group_links.new - link.group_id = params[:link_group_id] - link.group_access = params[:link_group_access] - link.save + group = Group.find(params[:link_group_id]) + return render_404 unless can?(current_user, :read_group, group) + + project.project_group_links.create( + group: group, group_access: params[:link_group_access] + ) redirect_to namespace_project_group_links_path(project.namespace, project) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6d649e72f84..7d4fc361ce2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions before_action :module_enabled - before_action :issue, only: [:edit, :update, :show] + before_action :issue, + only: [:edit, :update, :show, :referenced_merge_requests, :related_branches] # Allow read any issue before_action :authorize_read_issue!, only: [:show] @@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController # Allow issues bulk update before_action :authorize_admin_issues!, only: [:bulk_update] - # Cross-reference merge requests - before_action :closed_by_merge_requests, only: [:show] - respond_to :html def index @@ -35,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController end @issues = @issues.page(params[:page]) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.atom { render layout: false } format.json do render json: { - html: view_to_html_string("projects/issues/_issues") + html: view_to_html_string("projects/issues/_issues"), + labels: @labels.as_json(methods: :text_color) } end end @@ -62,11 +61,9 @@ 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 - @merge_requests = @issue.referenced_merge_requests(current_user) - @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) respond_to do |format| format.html @@ -104,7 +101,6 @@ class Projects::IssuesController < Projects::ApplicationController end respond_to do |format| - format.js format.html do if @issue.valid? redirect_to issue_path(@issue) @@ -113,7 +109,32 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) + render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) + end + end + end + + def referenced_merge_requests + @merge_requests = @issue.referenced_merge_requests(current_user) + @closed_by_merge_requests = @issue.closed_by_merge_requests(current_user) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_merge_requests') + } + end + end + end + + def related_branches + @related_branches = @issue.related_branches(current_user) + + respond_to do |format| + format.json do + render json: { + html: view_to_html_string('projects/issues/_related_branches') + } end end end @@ -123,10 +144,6 @@ class Projects::IssuesController < Projects::ApplicationController redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) end - def closed_by_merge_requests - @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) - end - protected def issue @@ -174,7 +191,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue_params params.require(:issue).permit( :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :state_event, :task_num, label_ids: [] + :milestone_id, :due_date, :state_event, :task_num, label_ids: [] ) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4b8fc049047..9c147b3689e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.json do render json: { - html: view_to_html_string("projects/merge_requests/_merge_requests") + html: view_to_html_string("projects/merge_requests/_merge_requests"), + labels: @labels.as_json(methods: :text_color) } end end @@ -148,13 +149,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController if @merge_request.valid? respond_to do |format| - format.js format.html do redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) end format.json do - render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) + render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }) end end else diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e457db2f0b7..33b2625c0ac 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,6 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize - before_action :authorize_admin_project_member!, except: :leave + before_action :authorize_admin_project_member!, except: [:leave, :index] def index @project_members = @project.project_members diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 8b2577aebe1..739681f4085 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :build_events, + :note_events, :build_events, :wiki_page_events, :notify_only_broken_builds, :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 9f3a4a69721..c02bc28acef 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :create_wiki, @project) - if @page.update(content, format, message) + if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' @@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController end def create - @page = WikiPage.new(@project_wiki) + @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute - if @page.create(wiki_params) + if @page.persisted? redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' @@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController params[:wiki].slice(:title, :content, :format, :message) end - def content - params[:wiki][:content] - end - - def format - params[:wiki][:format] - end - - def message - params[:wiki][:message] - end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c48175a4c5a..059b88e2253 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -31,11 +31,11 @@ class RegistrationsController < Devise::RegistrationsController end def after_sign_up_path_for(_resource) - new_user_session_path + users_almost_there_path end def after_inactive_sign_up_path_for(_resource) - new_user_session_path + users_almost_there_path end private 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/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f1df6832bf6..f00f3f709e9 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -39,6 +39,7 @@ class IssuableFinder items = by_assignee(items) items = by_author(items) items = by_label(items) + items = by_due_date(items) sort(items) end @@ -117,7 +118,7 @@ class IssuableFinder end def filter_by_no_label? - labels? && params[:label_name] == Label::None.title + labels? && params[:label_name].include?(Label::None.title) end def labels @@ -271,7 +272,6 @@ class IssuableFinder items = items.without_label else items = items.with_label(label_names) - if projects items = items.where(labels: { project_id: projects }) end @@ -281,8 +281,44 @@ class IssuableFinder items end + def by_due_date(items) + if due_date? + if filter_by_no_due_date? + items = items.without_due_date + elsif filter_by_overdue? + items = items.due_before(Date.today) + elsif filter_by_due_this_week? + items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) + elsif filter_by_due_this_month? + items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) + end + end + + items + end + + def filter_by_no_due_date? + due_date? && params[:due_date] == Issue::NoDueDate.name + end + + def filter_by_overdue? + due_date? && params[:due_date] == Issue::Overdue.name + end + + def filter_by_due_this_week? + due_date? && params[:due_date] == Issue::DueThisWeek.name + end + + def filter_by_due_this_month? + due_date? && params[:due_date] == Issue::DueThisMonth.name + end + + def due_date? + params[:due_date].present? && klass.column_names.include?('due_date') + end + def label_names - params[:label_name].split(',') + params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] end def current_user_related? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 16e5b8ac223..3e0074da394 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -254,11 +254,11 @@ module ApplicationHelper def page_filter_path(options = {}) without = options.delete(:without) + add_label = options.delete(:label) exist_opts = { state: params[:state], scope: params[:scope], - label_name: params[:label_name], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], @@ -275,6 +275,13 @@ module ApplicationHelper path = request.path path << "?#{options.to_param}" + if add_label + if params[:label_name].present? and params[:label_name].respond_to?('any?') + params[:label_name].each do |label| + path << "&label_name[]=#{label}" + end + end + end path end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 60a0ff32c9c..914b0ef6042 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -15,6 +15,10 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def shared_runners_text + current_application_settings.shared_runners_text + end + def user_oauth_applications? current_application_settings.user_oauth_applications end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9e59a295fc4..a4d7c425d0f 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -173,4 +173,15 @@ module BlobHelper response.etag = @blob.id !stale end + + def licenses_for_select + return @licenses_for_select if defined?(@licenses_for_select) + + licenses = Licensee::License.all + + @licenses_for_select = { + Popular: licenses.select(&:featured).map { |license| [license.name, license.key] }, + Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } + } + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 35ba543cef1..b59c3982edd 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -126,12 +126,10 @@ module CommitsHelper def revert_commit_link(commit, continue_to_path, btn_class: nil) return unless current_user - tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request" + tooltip = "Revert this #{commit.change_type_title} in a new merge request" if can_collaborate_with_project? - content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}" - end + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -146,11 +144,24 @@ module CommitsHelper end end - def revert_commit_type(commit) - if commit.merged_merge_request - 'merge request' - else - 'commit' + def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil) + return unless current_user + + tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" + + if can_collaborate_with_project? + link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip end end @@ -183,7 +194,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has-tooltip", - data: { 'original-title'.to_sym => sanitize(source_email) } + title: source_email } if user.nil? diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index ff32e834499..97466d532f4 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -9,7 +9,13 @@ module DiffHelper end def diff_view - params[:view] == 'parallel' ? 'parallel' : 'inline' + diff_views = %w(inline parallel) + + if diff_views.include?(cookies[:diff_view]) + cookies[:diff_view] + else + diff_views.first + end end def diff_hard_limit_enabled? @@ -40,10 +46,11 @@ module DiffHelper (unfold) ? 'unfold js-unfold' : '' end - def diff_line_content(line) + def diff_line_content(line, line_type = nil) if line.blank? " ".html_safe else + line[0] = ' ' if %w[new old].include?(line_type) line end end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb new file mode 100644 index 00000000000..109bc1a02d1 --- /dev/null +++ b/app/helpers/import_helper.rb @@ -0,0 +1,18 @@ +module ImportHelper + def github_project_link(path_with_namespace) + link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' + end + + private + + def github_project_url(path_with_namespace) + "#{github_root_url}/#{path_with_namespace}" + end + + def github_root_url + return @github_url if defined?(@github_url) + + provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } + @github_url = provider.fetch('url', 'https://github.com') if provider + end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index b14b8218d02..39474217286 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,25 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def multi_label_name(current_labels, default_label) + # current_labels may be a string from before + if current_labels.is_a?(Array) + if current_labels.count > 1 + "#{current_labels[0]} +#{current_labels.count - 1} more" + else + current_labels[0] + end + elsif current_labels.is_a?(String) + if current_labels.nil? || current_labels.empty? + default_label + else + current_labels + end + else + default_label + end + end + def issuable_json_path(issuable) project = issuable.project @@ -55,6 +74,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/issues_helper.rb b/app/helpers/issues_helper.rb index 4cb8adcebad..afe1e11a0da 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -131,7 +131,7 @@ module IssuesHelper class: "icon emoji-icon emoji-#{unicode}", title: name, data: data - else + else # Emoji icons displayed separately, used for the awards already given # to an issue or merge request. content_tag :img, "", @@ -172,6 +172,18 @@ module IssuesHelper end.to_h end + def due_date_options + options = [ + Issue::AnyDueDate, + Issue::NoDueDate, + Issue::DueThisWeek, + Issue::DueThisMonth, + Issue::Overdue + ] + + options_from_collection_for_select(options, 'name', 'title', params[:due_date]) + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 82f805fa444..e4e8b934bc8 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -84,6 +84,14 @@ module PageLayoutHelper end end + def nav(name = nil) + if name + @nav = name + else + @nav + end + end + def fluid_layout(enabled = false) if @fluid_layout.nil? @fluid_layout = (current_user && current_user.layout == "fluid") || enabled diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7e00aacceaa..3d5e61d2c18 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe end end @@ -123,6 +123,18 @@ module ProjectsHelper end end + def license_short_name(project) + no_license_key = project.repository.license_key.nil? || + # Back-compat if cache contains 'no-license', can be removed in a few weeks + project.repository.license_key == 'no-license' + + return 'LICENSE' if no_license_key + + license = Licensee::License.new(project.repository.license_key) + + license.nickname || license.name + end + private def get_project_nav_tabs(project, current_user) @@ -144,6 +156,10 @@ module ProjectsHelper nav_tabs << :settings end + if can?(current_user, :read_project_member, project) + nav_tabs << :team + end + if can?(current_user, :read_issue, project) nav_tabs << :issues end @@ -216,40 +232,14 @@ module ProjectsHelper end end - def add_contribution_guide_path(project) - if project && !project.repository.contribution_guide - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CONTRIBUTING.md", - commit_message: "Add contribution guide" - ) - end - end - - def add_changelog_path(project) - if project && !project.repository.changelog - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CHANGELOG", - commit_message: "Add changelog" - ) - end - end - - def add_license_path(project) - if project && !project.repository.license - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "LICENSE", - commit_message: "Add license" - ) - end + def add_special_file_path(project, file_name:, commit_message: nil) + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch || 'master', + file_name: file_name, + commit_message: commit_message || "Add #{file_name.downcase}" + ) end def contribution_guide_path(project) @@ -272,7 +262,7 @@ module ProjectsHelper end def license_path(project) - filename_path(project, :license) + filename_path(project, :license_blob) end def version_path(project) @@ -306,6 +296,13 @@ module ProjectsHelper namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') end + def new_license_path + ref = @repository.root_ref if @repository + ref ||= 'master' + + namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE') + end + def last_push_event if current_user current_user.recent_push(@project.id) @@ -335,8 +332,6 @@ module ProjectsHelper @ref || @repository.try(:root_ref) end - private - def filename_path(project, filename) if project && blob = project.repository.send(filename) namespace_project_blob_path( diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 05386d790ca..e951a87a212 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -2,30 +2,29 @@ module SelectsHelper def users_select_tag(id, opts = {}) css_class = "ajax-users-select " css_class << "multiselect " if opts[:multiple] + css_class << "skip_ldap " if opts[:skip_ldap] css_class << (opts[:class] || '') 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 html = { class: css_class, data: { - placeholder: placeholder, - null_user: null_user, - any_user: any_user, - email_user: email_user, + 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: first_user, - current_user: current_user + current_user: opts[:current_user] || false, + "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], + author_id: opts[:author_id] || '' } } unless opts[:scope] == :all + project = opts[:project] || @project + if project html['data-project-id'] = project.id elsif @group diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 2f2d2721d6d..630e10ea892 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -8,6 +8,8 @@ module SortingHelper sort_value_oldest_created => sort_title_oldest_created, sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_later => sort_title_milestone_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_due_date_later => sort_title_due_date_later, sort_value_largest_repo => sort_title_largest_repo, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, @@ -50,6 +52,14 @@ module SortingHelper 'Milestone due later' end + def sort_title_due_date_soon + 'Due soon' + end + + def sort_title_due_date_later + 'Due later' + end + def sort_title_name 'Name' end @@ -98,6 +108,14 @@ module SortingHelper 'milestone_due_desc' end + def sort_value_due_date_soon + 'due_date_asc' + end + + def sort_value_due_date_later + 'due_date_desc' + end + def sort_value_name 'name_asc' end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 04e53fe7c61..96a83671009 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -110,4 +110,12 @@ module TabHelper 'active' end end + + def profile_tab_class + if controller.controller_path =~ /\Aprofiles/ + return 'active' + end + + 'active' if current_controller?('oauth/applications') + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4920ca5af6e..dbedf417fa5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -66,7 +66,7 @@ module TreeHelper ref else project = tree_edit_project(project) - project.repository.next_patch_branch + project.repository.next_branch('patch') end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 55bb4f65270..9dd11d20ea6 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -56,7 +56,7 @@ module Emails { from: sender(sender_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})") + subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})") } end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index f9650df9a74..cdc40b81ee1 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -38,7 +38,7 @@ module Emails { from: sender(@note.author_id), to: recipient(recipient_id), - subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})") + subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})") } end diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb new file mode 100644 index 00000000000..21db2fe04a0 --- /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: "GitLab Admin | #{@message}" + ) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index ec5ac54c277..d3e8a3fa79a 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 @@ -289,7 +297,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 @@ -461,6 +468,10 @@ class Ability rules end + def user_abilities + [:read_user] + end + def abilities @abilities ||= begin abilities = Six.new @@ -475,6 +486,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 c0b334d3600..553cd447971 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -224,12 +224,33 @@ module Ci end end + def trace_length + if raw_trace + raw_trace.length + else + 0 + end + end + def trace=(trace) + recreate_trace_dir + File.write(path_to_trace, trace) + end + + def recreate_trace_dir unless Dir.exists?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end + end + private :recreate_trace_dir - File.write(path_to_trace, trace) + def append_trace(trace_part, offset) + recreate_trace_dir + + File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) + File.open(path_to_trace, 'a') do |f| + f.write(trace_part) + end end def dir_to_trace @@ -359,11 +380,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/ci/commit.rb b/app/models/ci/commit.rb index 6abeddeeae6..fadf5c62984 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -19,14 +19,17 @@ module Ci class Commit < ActiveRecord::Base extend Ci::Model - include CiStatus + include Statuseable belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :statuses, class_name: 'CommitStatus' has_many :builds, class_name: 'Ci::Build' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + delegate :stages, to: :statuses + validates_presence_of :sha + validates_presence_of :status validate :valid_commit_sha # Invalidate object and save if when touched @@ -40,10 +43,6 @@ module Ci CommitStatus.where(commit: all).stages end - def stages - statuses.stages - end - def project_id project.id end @@ -82,7 +81,7 @@ module Ci def retryable? builds.latest.any? do |build| - build.failed? || build.retryable? + build.failed? && build.retryable? end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 9ffdcc59128..562c3ed15b2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -15,8 +15,8 @@ class Commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] # Commits above this size will not be rendered in HTML - DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES) - DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) + DIFF_HARD_LIMIT_FILES = 1000 + DIFF_HARD_LIMIT_LINES = 50000 class << self def decorate(commits, project) @@ -154,7 +154,7 @@ class Commit id: id, message: safe_message, timestamp: committed_date.xmlschema, - url: commit_url, + url: Gitlab::UrlBuilder.build(self), author: { name: author_name, email: author_email @@ -168,10 +168,6 @@ class Commit data end - def commit_url - project.present? ? "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{id}" : "" - end - # Discover issues should be closed when this commit is pushed to a project's # default branch. def closes_issues(current_user = self.committer) @@ -223,6 +219,10 @@ class Commit def revert_branch_name "revert-#{short_id}" end + + def cherry_pick_branch_name + project.repository.next_branch("cherry-pick-#{short_id}", mild: true) + end def revert_description if merged_merge_request @@ -258,6 +258,10 @@ class Commit end.any? { |commit_ref| commit_ref.reverts_commit?(self) } end + def change_type_title + merged_merge_request ? 'merge request' : 'commit' + end + private def repo_changes diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 66eb5dcecf9..aa56314aa16 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -33,7 +33,7 @@ # class CommitStatus < ActiveRecord::Base - include CiStatus + include Statuseable self.table_name = 'ci_builds' @@ -81,7 +81,11 @@ class CommitStatus < ActiveRecord::Base end end - delegate :before_sha, :sha, :short_sha, to: :commit, prefix: false + delegate :sha, :short_sha, to: :commit + + def before_sha + commit.before_sha || Gitlab::Git::BLANK_SHA + end def self.stages order_by = 'max(stage_idx)' @@ -89,7 +93,10 @@ class CommitStatus < ActiveRecord::Base end def self.stages_status - Hash[group(:stage).pluck(:stage, self.status_sql)] + all.stages.inject({}) do |h, stage| + h[stage] = all.where(stage: stage).status + h + end end def ignored? @@ -97,11 +104,13 @@ class CommitStatus < ActiveRecord::Base end def duration - if started_at && finished_at - finished_at - started_at - elsif started_at - Time.now - started_at - end + duration = + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + duration end def stuck? diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 51288094ef1..5382dde6765 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -7,11 +7,13 @@ module InternalId end def set_iid - records = project.send(self.class.name.tableize) - records = records.with_deleted if self.paranoid? - max_iid = records.maximum(:iid) + if iid.blank? + records = project.send(self.class.name.tableize) + records = records.with_deleted if self.paranoid? + max_iid = records.maximum(:iid) - self.iid = max_iid.to_i + 1 + self.iid = max_iid.to_i + 1 + end end def to_param diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index afa2ca039ae..d5166e81474 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -37,7 +37,6 @@ module Issuable scope :closed, -> { with_state(:closed) } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } - scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } @@ -122,6 +121,14 @@ module Issuable joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") end + + def with_label(title) + if title.is_a?(Array) && title.count > 1 + joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}") + else + joins(:labels).where(labels: { title: title }) + end + end end def today? diff --git a/app/models/concerns/ci_status.rb b/app/models/concerns/statuseable.rb index 8190b2a20c6..8a293b7b76e 100644 --- a/app/models/concerns/ci_status.rb +++ b/app/models/concerns/statuseable.rb @@ -1,4 +1,4 @@ -module CiStatus +module Statuseable extend ActiveSupport::Concern AVAILABLE_STATUSES = %w(pending running success failed canceled skipped) @@ -7,7 +7,7 @@ module CiStatus def status_sql builds = all.select('count(*)').to_sql success = all.success.select('count(*)').to_sql - ignored = all.ignored.select('count(*)').to_sql if all.try(:ignored) + ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored) ignored ||= '0' pending = all.pending.select('count(*)').to_sql running = all.running.select('count(*)').to_sql @@ -33,7 +33,7 @@ module CiStatus def duration duration_array = all.map(&:duration).compact - duration_array.reduce(:+).to_i + duration_array.reduce(:+) end def started_at @@ -41,7 +41,7 @@ module CiStatus end def finished_at - all.minimum(:finished_at) + all.maximum(:finished_at) end end diff --git a/app/models/event.rb b/app/models/event.rb index 12183524b79..897518aadc7 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -345,7 +345,7 @@ class Event < ActiveRecord::Base end def reset_project_activity - if project + if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain project.update_column(:last_activity_at, self.created_at) end end 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/group.rb b/app/models/group.rb index 9a04ac70d35..1f8432e3320 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -15,7 +15,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index fe923fafbe0..bc6e0f98c3c 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -21,10 +21,9 @@ class ProjectHook < WebHook belongs_to :project - scope :push_hooks, -> { where(push_events: true) } - scope :tag_push_hooks, -> { where(tag_push_events: true) } scope :issue_hooks, -> { where(issues_events: true) } scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } + scope :wiki_page_hooks, -> { where(wiki_page_events: true) } end diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index c147d8762a9..15dddcc2447 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -19,4 +19,7 @@ # class SystemHook < WebHook + def async_execute(data, hook_name) + Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name) + end end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 7a13c3f0a39..3a2e4f546f7 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -30,6 +30,9 @@ class WebHook < ActiveRecord::Base default_value_for :build_events, false default_value_for :enable_ssl_verification, true + scope :push_hooks, -> { where(push_events: true) } + scope :tag_push_hooks, -> { where(tag_push_events: true) } + # HTTParty timeout default_timeout Gitlab.config.gitlab.webhook_timeout diff --git a/app/models/issue.rb b/app/models/issue.rb index e064b0f8b95..ea1bfb776ee 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -20,7 +20,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Issue < ActiveRecord::Base include InternalId @@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base include Sortable include Taskable + DueDateStruct = Struct.new(:title, :name).freeze + NoDueDate = DueDateStruct.new('No Due Date', '0').freeze + AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze + Overdue = DueDateStruct.new('Overdue', 'overdue').freeze + DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze + DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + ActsAsTaggableOn.strict_case_match = true belongs_to :project @@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base scope :open_for, ->(user) { opened.assigned_to(user) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) } + scope :without_due_date, -> { where(due_date: nil) } + scope :due_before, ->(date) { where('issues.due_date < ?', date) } + scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } + + scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } + scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end + def self.sort(method) + case method.to_s + when 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + else + super + end + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base end end - def related_branches - project.repository.branch_names.select do |branch| - branch.end_with?("-#{iid}") + # All branches containing the current issue's ID, except for + # those with a merge request open referencing the current issue. + def related_branches(current_user) + branches_with_iid = project.repository.branch_names.select do |branch| + branch =~ /\A#{iid}-(?!\d+-stable)/i end + + branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch) + + branches_with_iid - branches_with_merge_request end # Reset issue events cache @@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base end def to_branch_name - "#{title.parameterize}-#{iid}" + if self.confidential? + "#{iid}-confidential-issue" + else + "#{iid}-#{title.parameterize}" + end end def can_be_worked_on?(current_user) !self.closed? && !self.project.forked? && - self.related_branches.empty? && + self.related_branches(current_user).empty? && self.closed_by_merge_requests(current_user).empty? end + + def overdue? + due_date.try(:past?) || false + end end diff --git a/app/models/label.rb b/app/models/label.rb index 55c01cae762..60bdce32952 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -113,6 +113,10 @@ class Label < ActiveRecord::Base template end + def text_color + LabelsHelper::text_color_for_bg(self.color) + end + private def label_format_reference(format = :id) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6766e4d1afa..d00919c3b0c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -27,9 +27,6 @@ # merge_commit_sha :string # -require Rails.root.join("app/models/commit") -require Rails.root.join("lib/static_model") - class MergeRequest < ActiveRecord::Base include InternalId include Issuable @@ -605,4 +602,8 @@ class MergeRequest < ActiveRecord::Base def can_be_reverted?(current_user = nil) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end + + def can_be_cherry_picked? + merge_commit + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 33884118595..0580cafdd1b 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -11,8 +11,6 @@ # updated_at :datetime # -require Rails.root.join("app/models/commit") - class MergeRequestDiff < ActiveRecord::Base include Sortable diff --git a/app/models/note.rb b/app/models/note.rb index 87ced65c650..71b4293d57a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -20,7 +20,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Note < ActiveRecord::Base include Gitlab::CurrentSettings 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 e48830115bc..0420c6a61ae 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,7 +40,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Project < ActiveRecord::Base include Gitlab::ConfigHelper @@ -409,6 +408,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 @@ -792,18 +820,16 @@ class Project < ActiveRecord::Base wiki = Repository.new("#{old_path}.wiki", self) if repo.exists? - repo.expire_cache - repo.expire_emptiness_caches + repo.before_delete end if wiki.exists? - wiki.expire_cache - wiki.expire_emptiness_caches + wiki.before_delete end end - def hook_attrs - { + def hook_attrs(backward: true) + attrs = { name: name, description: description, web_url: web_url, @@ -814,12 +840,19 @@ class Project < ActiveRecord::Base visibility_level: visibility_level, path_with_namespace: path_with_namespace, default_branch: default_branch, - # Backward compatibility - homepage: web_url, - url: url_to_repo, - ssh_url: ssh_url_to_repo, - http_url: http_url_to_repo } + + # Backward compatibility + if backward + attrs.merge!({ + homepage: web_url, + url: url_to_repo, + ssh_url: ssh_url_to_repo, + http_url: http_url_to_repo + }) + end + + attrs end # Reset events cache related to this project @@ -865,7 +898,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 @@ -927,7 +962,7 @@ class Project < ActiveRecord::Base end def ci_commit(sha, ref) - ci_commits.find_by(sha: sha, ref: ref) + ci_commits.order(id: :desc).find_by(sha: sha, ref: ref) end def ensure_ci_commit(sha, ref) diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index cd3319f077e..2c0ae312f1b 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -8,12 +8,23 @@ # require 'carrierwave/orm/activerecord' -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/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 9e7f642180e..060062aaf7a 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -82,17 +82,17 @@ class BambooService < CiService end def build_info(sha) - url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") + url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s if username.blank? && password.blank? - @response = HTTParty.get(parsed_url.to_s, verify: false) + @response = HTTParty.get(url, verify: false) else - get_url = "#{url}&os_authType=basic" + url << '&os_authType=basic' auth = { - username: username, - password: password, + username: username, + password: password } - @response = HTTParty.get(get_url, verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end end @@ -101,11 +101,11 @@ class BambooService < CiService if @response.code != 200 || @response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. - "#{bamboo_url}/browse/#{build_key}" + URI.join(bamboo_url, "/browse/#{build_key}").to_s else # If actual build link is available, go to build result page. result_key = @response['results']['results']['result']['planResultKey']['key'] - "#{bamboo_url}/browse/#{result_key}" + URI.join(bamboo_url, "/browse/#{result_key}").to_s end end @@ -134,7 +134,7 @@ class BambooService < CiService return unless supported_events.include?(data[:object_kind]) # Bamboo requires a GET and does not take any data. - self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", - verify: false) + url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s + self.class.get(url, verify: false) end end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index f9f04838766..6ab6d7417b7 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -23,7 +23,7 @@ class BuildsEmailService < Service prop_accessor :recipients boolean_accessor :add_pusher boolean_accessor :notify_only_broken_builds - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? } def initialize_properties if properties.nil? @@ -87,10 +87,14 @@ class BuildsEmailService < Service end def all_recipients(data) - all_recipients = recipients.split(',').compact.reject(&:blank?) + all_recipients = [] + + unless recipients.blank? + all_recipients += recipients.split(',').compact.reject(&:blank?) + end if add_pusher? && data[:user][:email] - all_recipients << "#{data[:user][:email]}" + all_recipients << data[:user][:email] end all_recipients diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 0e3fa4a40fe..064ef8e8674 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -183,7 +183,7 @@ class HipchatService < Service title = obj_attr[:title] merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" - merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>" + merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: <b>#{title}</b>" @@ -224,7 +224,7 @@ class HipchatService < Service when "MergeRequest" subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) subject_id = subj_attr[:iid] - subject_desc = "##{subject_id}" + subject_desc = "!#{subject_id}" subject_type = "merge request" title = format_title(subj_attr[:title]) when "Snippet" diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index d89cf6d17b2..fd65027f084 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -60,7 +60,7 @@ class SlackService < Service end def supported_events - %w(push issue merge_request note tag_push build) + %w(push issue merge_request note tag_push build wiki_page) end def execute(data) @@ -90,6 +90,8 @@ class SlackService < Service NoteMessage.new(data) when "build" BuildMessage.new(data) if should_build_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) end opt = {} @@ -133,3 +135,4 @@ require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" require "slack_service/build_message" +require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb index e792c258f73..11fc691022b 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -50,7 +50,7 @@ class SlackService end def merge_request_link - "[merge request ##{merge_request_id}](#{merge_request_url})" + "[merge request !#{merge_request_id}](#{merge_request_url})" end def merge_request_url diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index b15d9a14677..89ba51cb662 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -58,7 +58,7 @@ class SlackService def create_merge_note(merge_request) commented_on_message( - "[merge request ##{merge_request[:iid]}](#{@note_url})", + "[merge request !#{merge_request[:iid]}](#{@note_url})", format_title(merge_request[:title])) end diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb new file mode 100644 index 00000000000..f336d9e7691 --- /dev/null +++ b/app/models/project_services/slack_service/wiki_page_message.rb @@ -0,0 +1,53 @@ +class SlackService + class WikiPageMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :wiki_page_url + attr_reader :action + attr_reader :description + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @wiki_page_url = obj_attr[:url] + @description = obj_attr[:content] + + @action = + case obj_attr[:action] + when "create" + "created" + when "update" + "edited" + end + end + + def attachments + description_message + end + + private + + def message + "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b8e9416131a..8dceee5e2c5 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -85,13 +85,15 @@ class TeamcityService < CiService end def build_info(sha) - url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ - "branch:unspecified:any,number:#{sha}") + url = URI.join( + teamcity_url, + "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}" + ).to_s auth = { username: username, - password: password, + password: password } - @response = HTTParty.get("#{url}", verify: false, basic_auth: auth) + @response = HTTParty.get(url, verify: false, basic_auth: auth) end def build_page(sha, ref) @@ -100,12 +102,14 @@ class TeamcityService < CiService if @response.code != 200 # If actual build link can't be determined, # send user to build summary page. - "#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}" + URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s else # If actual build link is available, go to build result page. built_id = @response['build']['id'] - "#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ - "&buildTypeId=#{build_type}" + URI.join( + teamcity_url, + "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" + ).to_s end end @@ -140,12 +144,13 @@ class TeamcityService < CiService branch = Gitlab::Git.ref_name(data[:ref]) - self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", - body: "<build branchName=\"#{branch}\">"\ - "<buildType id=\"#{build_type}\"/>"\ - '</build>', - headers: { 'Content-type' => 'application/xml' }, - basic_auth: auth - ) + self.class.post( + URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s, + body: "<build branchName=\"#{branch}\">"\ + "<buildType id=\"#{build_type}\"/>"\ + '</build>', + headers: { 'Content-type' => 'application/xml' }, + basic_auth: auth + ) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 0b2289cfa39..61c8dce6060 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 @@ -221,7 +228,8 @@ class Repository def cache_keys %i(size branch_names tag_names commit_count - readme version contribution_guide changelog license) + readme version contribution_guide changelog + license_blob license_key) end def build_cache @@ -454,27 +462,21 @@ class Repository end end - def license - cache.fetch(:license) do - licenses = tree(:head).blobs.find_all do |file| - file.name =~ /\A(copying|license|licence)/i - end - - preferences = [ - /\Alicen[sc]e\z/i, # LICENSE, LICENCE - /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt - /\Acopying\z/i, # COPYING - /\Acopying\.(?!lesser)/i, # COPYING.txt - /Acopying.lesser/i # COPYING.LESSER - ] + def license_blob + return nil if !exists? || empty? - license = nil - preferences.each do |r| - license = licenses.find { |l| l.name =~ r } - break if license + cache.fetch(:license_blob) do + tree(:head).blobs.find do |file| + file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i end + end + end - license + def license_key + return nil if !exists? || empty? + + cache.fetch(:license_key) do + Licensee.license(path).try(:key) end end @@ -542,15 +544,18 @@ class Repository commit(sha) end - def next_patch_branch - patch_branch_ids = self.branch_names.map do |n| - result = n.match(/\Apatch-([0-9]+)\z/) + def next_branch(name, opts={}) + branch_ids = self.branch_names.map do |n| + next 1 if n == name + result = n.match(/\A#{name}-([0-9]+)\z/) result[1].to_i if result end.compact - highest_patch_branch_id = patch_branch_ids.max || 0 + highest_branch_id = branch_ids.max || 0 - "patch-#{highest_patch_branch_id + 1}" + return name if opts[:mild] && 0 == highest_branch_id + + "#{name}-#{highest_branch_id + 1}" end # Remove archives older than 2 hours @@ -753,6 +758,28 @@ class Repository end end + def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) + source_sha = find_branch(base_branch).target + cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) + + return false unless cherry_pick_tree_id + + commit_with_hooks(user, base_branch) do |ref| + committer = user_to_committer(user) + source_sha = Rugged::Commit.create(rugged, + message: commit.message, + author: { + email: commit.author_email, + name: commit.author_name, + time: commit.authored_date + }, + committer: committer, + tree: cherry_pick_tree_id, + parents: [rugged.lookup(source_sha)], + update_ref: ref) + end + end + def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] @@ -767,6 +794,20 @@ class Repository tree_id end + def check_cherry_pick_content(commit, base_branch) + source_sha = find_branch(base_branch).target + args = [commit.id, source_sha] + args << 1 if commit.merge_commit? + + cherry_pick_index = rugged.cherrypick_commit(*args) + return false if cherry_pick_index.conflicts? + + tree_id = cherry_pick_index.write_tree(rugged) + return false unless diff_exists?(source_sha, tree_id) + + tree_id + end + def diff_exists?(sha1, sha2) rugged.diff(sha1, sha2).size > 0 end @@ -797,7 +838,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/models/service.rb b/app/models/service.rb index 721273250ea..2645b8321d7 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -32,6 +32,7 @@ class Service < ActiveRecord::Base default_value_for :tag_push_events, true default_value_for :note_events, true default_value_for :build_events, true + default_value_for :wiki_page_events, true after_initialize :initialize_properties @@ -53,6 +54,7 @@ class Service < ActiveRecord::Base scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } + scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } default_value_for :category, 'common' @@ -94,7 +96,7 @@ class Service < ActiveRecord::Base end def supported_events - %w(push tag_push issue merge_request) + %w(push tag_push issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user.rb b/app/models/user.rb index 031315debd7..ab48f8f1960 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,7 +63,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class User < ActiveRecord::Base extend Gitlab::ConfigHelper diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 526760779a4..3d5fd9d3ee9 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -29,6 +29,10 @@ class WikiPage # new Page values before writing to the Gollum repository. attr_accessor :attributes + def hook_attrs + attributes + end + def initialize(wiki, page = nil, persisted = false) @wiki = wiki @page = page diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb new file mode 100644 index 00000000000..6b69cb53b2c --- /dev/null +++ b/app/services/commits/change_service.rb @@ -0,0 +1,47 @@ +module Commits + class ChangeService < ::BaseService + class ValidationError < StandardError; end + class ChangeError < StandardError; end + + def execute + @source_project = params[:source_project] || @project + @target_branch = params[:target_branch] + @commit = params[:commit] + @create_merge_request = params[:create_merge_request].present? + + check_push_permissions unless @create_merge_request + commit + rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, + ValidationError, ChangeError => ex + error(ex.message) + end + + def commit + raise NotImplementedError + end + + private + + def check_push_permissions + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) + + unless allowed + raise ValidationError.new('You are not allowed to push into this branch') + end + + true + end + + def create_target_branch(new_branch) + # Temporary branch exists and contains the change commit + return success if repository.find_branch(new_branch) + + result = CreateBranchService.new(@project, current_user) + .execute(new_branch, @target_branch, source_project: @source_project) + + if result[:status] == :error + raise ChangeError, "There was an error creating the source branch: #{result[:message]}" + end + end + end +end diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb new file mode 100644 index 00000000000..f9a4efa7182 --- /dev/null +++ b/app/services/commits/cherry_pick_service.rb @@ -0,0 +1,19 @@ +module Commits + class CherryPickService < ChangeService + def commit + cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch + cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch) + + if cherry_pick_tree_id + create_target_branch(cherry_pick_into) if @create_merge_request + + repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id) + success + else + error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically. + It may have already been cherry-picked, or a more recent commit may have updated some of its content." + raise ChangeError, error_msg + end + end + end +end diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb index a3c950ede1f..c7de9f6f35e 100644 --- a/app/services/commits/revert_service.rb +++ b/app/services/commits/revert_service.rb @@ -1,21 +1,5 @@ module Commits - class RevertService < ::BaseService - class ValidationError < StandardError; end - class ReversionError < StandardError; end - - def execute - @source_project = params[:source_project] || @project - @target_branch = params[:target_branch] - @commit = params[:commit] - @create_merge_request = params[:create_merge_request].present? - - check_push_permissions unless @create_merge_request - commit - rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, - ValidationError, ReversionError => ex - error(ex.message) - end - + class RevertService < ChangeService def commit revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch revert_tree_id = repository.check_revert_content(@commit, @target_branch) @@ -26,34 +10,10 @@ module Commits repository.revert(current_user, @commit, revert_into, revert_tree_id) success else - error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically. + error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically. It may have already been reverted, or a more recent commit may have updated some of its content." - raise ReversionError, error_msg + raise ChangeError, error_msg end end - - private - - def create_target_branch(new_branch) - # Temporary branch exists and contains the revert commit - return success if repository.find_branch(new_branch) - - result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) - - if result[:status] == :error - raise ReversionError, "There was an error creating the source branch: #{result[:message]}" - end - end - - def check_push_permissions - allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) - - unless allowed - raise ValidationError.new('You are not allowed to push into this branch') - end - - true - end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index dc74c02760b..1e1be8cd04b 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -73,6 +73,7 @@ class GitPushService < BaseService @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) EventCreateService.new.push(@project, current_user, build_push_data) + SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) CreateCommitBuildsService.new.execute(@project, current_user, build_push_data) @@ -138,6 +139,11 @@ class GitPushService < BaseService build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) end + def build_push_data_system_hook + @push_data_system ||= Gitlab::PushDataBuilder. + build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], []) + end + def push_to_existing_branch? # Return if this is not a push to a branch (e.g. new commits) Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev]) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index c88c7672805..64271d8bc5c 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -1,16 +1,16 @@ -class GitTagPushService - attr_accessor :project, :user, :push_data +class GitTagPushService < BaseService + attr_accessor :push_data - def execute(project, user, oldrev, newrev, ref) + def execute project.repository.before_push_tag - @project, @user = project, user - @push_data = build_push_data(oldrev, newrev, ref) + @push_data = build_push_data - EventCreateService.new.push(project, user, @push_data) + EventCreateService.new.push(project, current_user, @push_data) + SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - CreateCommitBuildsService.new.execute(project, @user, @push_data) + CreateCommitBuildsService.new.execute(project, current_user, @push_data) ProjectCacheWorker.perform_async(project.id) true @@ -18,14 +18,14 @@ class GitTagPushService private - def build_push_data(oldrev, newrev, ref) + def build_push_data commits = [] message = nil - if !Gitlab::Git.blank_ref?(newrev) - tag_name = Gitlab::Git.ref_name(ref) + if !Gitlab::Git.blank_ref?(params[:newrev]) + tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) - if tag && tag.target == newrev + if tag && tag.target == params[:newrev] commit = project.commit(tag.target) commits = [commit].compact message = tag.message @@ -33,6 +33,11 @@ class GitTagPushService end Gitlab::PushDataBuilder. - build(project, user, oldrev, newrev, ref, commits, message) + build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message) + end + + def build_system_push_data + Gitlab::PushDataBuilder. + build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '') end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 18f76d3f650..2b16089df1b 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -37,8 +37,9 @@ class IssuableBaseService < BaseService end def filter_params(issuable_ability_name = :issue) - params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE - params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE + filter_assignee + filter_milestone + filter_labels ability = :"admin_#{issuable_ability_name}" @@ -49,6 +50,29 @@ class IssuableBaseService < BaseService end end + def filter_assignee + if params[:assignee_id] == IssuableFinder::NONE + params[:assignee_id] = '' + end + end + + def filter_milestone + milestone_id = params[:milestone_id] + return unless milestone_id + + if milestone_id == IssuableFinder::NONE || + project.milestones.find_by(id: milestone_id).nil? + params[:milestone_id] = '' + end + end + + def filter_labels + return if params[:label_ids].to_a.empty? + + params[:label_ids] = + project.labels.where(id: params[:label_ids]).pluck(:id) + end + def update(issuable) change_state(issuable) filter_params diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 770f32de944..772f5c5fffa 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -3,7 +3,7 @@ module Issues def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) - issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) + issue_url = Gitlab::UrlBuilder.build(issue) issue_data[:object_attributes].merge!(url: issue_url, action: action) issue_data end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index ac5b58db862..e6837a18696 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -20,8 +20,7 @@ module MergeRequests def hook_data(merge_request, action) hook_data = merge_request.to_hook_data(current_user) - merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action hook_data end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 6e9152e444e..fa34753c4fd 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -51,7 +51,7 @@ module MergeRequests # be interpreted as the use wants to close that issue on this project # Pattern example: 112-fix-mep-mep # Will lead to appending `Closes #112` to the description - if match = merge_request.source_branch.match(/-(\d+)\z/) + if match = merge_request.source_branch.match(/\A(\d+)-/) iid = match[1] closes_issue = "Closes ##{iid}" diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index a0973c5d260..3b7c36f0908 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -26,7 +26,9 @@ module Projects GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure - @project.update_column(:pushes_since_gc, 0) + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + @project.update_column(:pushes_since_gc, 0) + end end def needed? @@ -34,14 +36,18 @@ module Projects end def increment! - @project.increment!(:pushes_since_gc) + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + @project.increment!(:pushes_since_gc) + end end private def try_obtain_lease - lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) - lease.try_obtain + Gitlab::Metrics.measure(:obtain_housekeeping_lease) do + lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) + lease.try_obtain + end end end 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..111b3ec05ea 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,8 +34,11 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end - # Apply new namespace id + project.expire_caches_before_rename(old_path) + + # 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 +59,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/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index f0615ec7420..e43b5b51e5b 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -3,17 +3,13 @@ class SystemHooksService execute_hooks(build_event_data(model, event)) end - private - - def execute_hooks(data) - SystemHook.all.each do |sh| - async_execute_hook(sh, data, 'system_hooks') + def execute_hooks(data, hooks_scope = :all) + SystemHook.send(hooks_scope).each do |hook| + hook.async_execute(data, 'system_hooks') end end - def async_execute_hook(hook, data, hook_name) - Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name) - end + private def build_event_data(model, event) data = { diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 658b086496f..82a0e2fd1f5 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -222,7 +222,7 @@ class SystemNoteService # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `issue-branch-button-201`" + # "Started branch `201-issue-branch-button`" def self.new_issue_branch(issue, project, author, branch) h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb new file mode 100644 index 00000000000..9162f128602 --- /dev/null +++ b/app/services/wiki_pages/base_service.rb @@ -0,0 +1,27 @@ +module WikiPages + class BaseService < ::BaseService + + def hook_data(page, action) + hook_data = { + object_kind: page.class.name.underscore, + user: current_user.hook_attrs, + project: @project.hook_attrs, + object_attributes: page.hook_attrs, + # DEPRECATED + repository: @project.hook_attrs.slice(:name, :url, :description, :homepage) + } + + page_url = Gitlab::UrlBuilder.build(page) + hook_data[:object_attributes].merge!(url: page_url, action: action) + hook_data + end + + private + + def execute_hooks(page, action = 'create') + page_data = hook_data(page, action) + @project.execute_hooks(page_data, :wiki_page_hooks) + @project.execute_services(page_data, :wiki_page_hooks) + end + end +end diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb new file mode 100644 index 00000000000..988c663b9d0 --- /dev/null +++ b/app/services/wiki_pages/create_service.rb @@ -0,0 +1,13 @@ +module WikiPages + class CreateService < WikiPages::BaseService + def execute + page = WikiPage.new(@project.wiki) + + if page.create(@params) + execute_hooks(page, 'create') + end + + page + end + end +end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb new file mode 100644 index 00000000000..8f6a50da838 --- /dev/null +++ b/app/services/wiki_pages/update_service.rb @@ -0,0 +1,11 @@ +module WikiPages + class UpdateService < WikiPages::BaseService + def execute(page) + if page.update(@params[:content], @params[:format], @params[:message]) + execute_hooks(page, 'update') + end + + page + end + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a8cca1a81cb..e0d8d16a954 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 @@ -153,7 +155,11 @@ = f.label :shared_runners_enabled do = f.check_box :shared_runners_enabled Enable shared runners for new projects - + .form-group + = f.label :shared_runners_text, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :shared_runners_text, class: 'form-control', rows: 4 + .help-block Markdown enabled .form-group = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2' .col-sm-10 @@ -212,6 +218,13 @@ .help-block The sampling interval in seconds. Sampled data includes memory usage, retained Ruby objects, file descriptors and so on. + .form-group + = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_packet_size, class: 'form-control' + .help-block + The amount of points to store in a single UDP packet. More points + results in fewer but larger UDP packets being sent. %fieldset %legend Spam and Anti-bot Protection @@ -271,5 +284,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/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index ad952052f25..67d23c80233 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -17,6 +17,27 @@ .col-sm-10 = f.text_field :url, class: "form-control" .form-group + = f.label :url, "Trigger", class: 'control-label' + .col-sm-10.prepend-top-10 + %div + System hook will be triggered on set of events like creating project + or adding ssh key. But you can also enable extra triggers like Push events. + + %div.prepend-top-default + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + .form-group = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' .col-sm-10 .checkbox @@ -31,13 +52,16 @@ .panel.panel-default .panel-heading System hooks (#{@hooks.count}) - %ul.well-list + %ul.content-list - @hooks.each do |hook| %li - .list-item-name - %strong= hook.url - %p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} - - .pull-right + .controls = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm" = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + .monospace= hook.url + %div + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| + - if hook.send(trigger) + %span.label.label-gray= trigger.titleize + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + 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/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index b05fdbd5552..fe0b9d3a491 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -7,17 +7,17 @@ .form-group = f.label :name, class: 'control-label' .col-sm-10 - = f.text_field :name, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group = f.label :username, class: 'control-label' .col-sm-10 - = f.text_field :username, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group = f.label :email, class: 'control-label' .col-sm-10 - = f.text_field :email, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required - if @user.new_record? diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f9ec3a89158..49ab8aad1d5 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -45,6 +45,7 @@ .prepend-top-default - if @todos.any? + .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| .panel.panel-default.panel-small.js-todos-list - project = group[0] diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml new file mode 100644 index 00000000000..3c3830a3f10 --- /dev/null +++ b/app/views/devise/confirmations/almost_there.haml @@ -0,0 +1,10 @@ +.well-confirmation.text-center + %h1.prepend-top-0 + Almost there... + %p.lead + Please check your email to confirm your account +%p.confirmation-content.text-center + No confirmation email received? Please check your spam folder or +.append-bottom-20.prepend-top-20.text-center + %a.btn.btn-lg.btn-success{ href: new_user_confirmation_path } + Request new confirmation email 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..79df17ba612 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -1,5 +1,4 @@ - page_title "Applications" -- header_title page_title, applications_profile_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar @@ -68,7 +67,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/events/_event.html.haml b/app/views/events/_event.html.haml index 4d20dd5830e..5d622582088 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -4,7 +4,12 @@ #{time_ago_with_tooltip(event.created_at)} = cache [event, current_application_settings, "v2.2"] do - = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' + - if event.author + = link_to user_path(event.author.username) do + = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' + - else + = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' + - if event.created_project? = render "events/event/created_project", event: event - elsif event.push? diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 4290e0bf72e..7d9d27ae1fc 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -8,7 +8,7 @@ This will create milestone in every selected project %hr -= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| .row - if @milestone.errors.any? #error_explanation @@ -27,7 +27,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' .clearfix .error-alert .form-group diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index da3c3711cdd..70e88da7aae 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -21,7 +21,7 @@ %tr %td.shortcut .key ? - %td Show this dialog + %td Show/hide this dialog %tr %td.shortcut - if browser.mac? @@ -169,6 +169,10 @@ %td.shortcut .key t %td Go to finding file + %tr + %td.shortcut + .key i + %td New issue .col-lg-4 %table.shortcut-mappings %tbody{ class: 'hidden-shortcut network', style: 'display:none' } @@ -241,6 +245,10 @@ %td.shortcut .key e %td Edit issue + %tr + %td.shortcut + .key l + %td Change Label %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } %tr %th @@ -261,3 +269,7 @@ %td.shortcut .key e %td Edit merge request + %tr + %td.shortcut + .key l + %td Change Label 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..5b7f11440c1 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 @@ -21,9 +26,9 @@ - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} %td - = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" + = github_project_link(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 @@ -38,11 +43,13 @@ - @repos.each do |repo| %tr{id: "repo_#{repo.id}"} %td - = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" + = github_project_link(repo.full_name) %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/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588d..ca9c2a0bf2e 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -25,6 +25,10 @@ .content-wrapper = render "layouts/flash" = yield :flash_message + - if defined?(nav) && nav + .layout-nav + %div{ class: container_class } + = render "layouts/nav/#{nav}" %div{ class: (container_class unless @no_container) } .content .clearfix diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index babfb032236..e4d1c773d03 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,6 +6,6 @@ = yield :scripts_body_top = render "layouts/header/default", title: header_title - = render 'layouts/page', sidebar: sidebar + = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml new file mode 100644 index 00000000000..7c061dd531f --- /dev/null +++ b/app/views/layouts/devise_empty.html.haml @@ -0,0 +1,17 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head" + %body.ui_charcoal.login-page.application.navless + = render "layouts/header/empty" + = render "layouts/broadcast" + .container.navless-container + .content + = render "layouts/flash" + = yield + + %hr + .container + .footer-links + = link_to "Explore", explore_root_path + = link_to "Help", help_path + = link_to "About GitLab", "https://about.gitlab.com/" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 44339293095..3beb8ff7c0d 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -8,7 +8,7 @@ .navbar-collapse.collapse %ul.nav.navbar-nav %li.hidden-sm.hidden-xs - = render 'layouts/search' + = render 'layouts/search' unless current_controller?(:search) %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 5cef652da14..ca49c313ff7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -48,8 +48,7 @@ %span Help - %li.separate-item - = nav_link(controller: :profile) do + = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc5..d730840d63a 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,17 +1,9 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - +%ul.nav-links = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') %span - Profile Settings + Profile = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account' do = icon('gear fw') @@ -27,7 +19,6 @@ = icon('envelope-o fw') %span Emails - %span.count= number_with_delimiter(current_user.emails.count + 1) - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to edit_profile_password_path, title: 'Password' do @@ -45,7 +36,6 @@ = icon('key fw') %span SSH Keys - %span.count= number_with_delimiter(current_user.keys.count) = nav_link(controller: :preferences) do = link_to profile_preferences_path, title: 'Preferences' do -# TODO (rspeicher): Better icon? diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index f4797a85bb7..6c3f8efc74e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -84,7 +84,7 @@ Merge Requests %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :settings + - if project_nav_tab? :team = nav_link(controller: [:project_members, :teams]) do = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do = icon('users fw') @@ -131,3 +131,8 @@ %li.hidden = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do Network + + -# Shortcut to create a new issue + %li.hidden + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do + Create a new issue diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index dfa6cc5702e..b77d3402a2e 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,6 @@ - page_title "Profile Settings" - header_title "Profile Settings", profile_path unless header_title -- sidebar "profile" +- sidebar "dashboard" +- nav "profile" = render template: "layouts/application" diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 574e8bfef24..81c7c88fc96 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" + = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index 59db86b08bc..b435067d5a6 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" += "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index c9bf04f514e..41a320d6bd8 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" + = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index b96dd0fd8ab..7a5074a1dc3 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" += "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index 6762fae7f64..fbe506d4f4d 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was merged" + = "Merge Request #{@merge_request.to_reference} was merged" diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 34dbc60e19b..bfbae01094f 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was merged" += "Merge Request #{@merge_request.to_reference} was merged" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index bdcca6e4ab7..d4aad8d1862 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -1,4 +1,4 @@ -New Merge Request #<%= @merge_request.iid %> +New Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 1d1411992a6..8cdab63829e 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,4 +1,4 @@ -New comment for Merge Request <%= @merge_request.iid %> +New comment for Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 6efd119f260..afd3d79321f 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,5 +1,4 @@ - page_title "Account" -- header_title page_title, profile_account_path - if current_user.ldap_user? .alert.alert-info diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index f630c03e5f6..9c404b6935f 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,5 +1,4 @@ - page_title "Audit Log" -- header_title page_title, audit_log_profile_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 3f328f96cea..57527361eb6 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,5 +1,4 @@ - page_title "Emails" -- header_title page_title, profile_emails_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index e0f8c9a5733..6a067a03535 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,5 +1,4 @@ - page_title "SSH Keys" -- header_title page_title, profile_keys_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index a2a505c082b..7696f112bb3 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,5 +1,4 @@ - page_title "Notifications" -- header_title page_title, profile_notifications_path %div - if @user.errors.any? diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 5ac8a8b9d09..243428b690e 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Password" -- header_title page_title, edit_profile_password_path .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index f80211669fb..bfe53be6854 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,5 +1,4 @@ - page_title 'Preferences' -- header_title page_title, profile_preferences_path = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f| .col-lg-3.profile-settings-sidebar diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 961b61d2e76..48b0dd6b121 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -9,4 +9,7 @@ = spinner :javascript - new Activities(); + var activity = new Activities(); + $(document).on('page:restore', function (event) { + activity.reloadActivities() + }) diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 9ae6964aaac..0de019983ca 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -52,6 +52,12 @@ %li phpunit --coverage-text --colors=never (PHP) - %code ^\s*Lines:\s*\d+.\d+\% + %li + gcovr (C/C++) - + %code ^TOTAL.*\s+(\d+\%)$ + %li + tap --coverage-report=text-summary (Node.js) - + %code ^Statements\s*:\s*([^%]+) .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/_readme.html.haml b/app/views/projects/_readme.html.haml index d1191928d4f..a9908eaecca 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -9,7 +9,7 @@ - else .gray-content-block.second-block.center %h3.page-title - This project does not have README yet + This project does not have a README yet - if can?(current_user, :push_code, @project) %p A @@ -18,5 +18,5 @@ distributed with computer software, forming part of its documentation. %p We recommend you to - = link_to "add README", new_readme_path, class: 'underlined-link' + = link_to "add a README", new_readme_path, class: 'underlined-link' file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index bddff5cdcbc..e1e35013968 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,8 +1,8 @@ .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + = f.text_area attr, class: classes, placeholder: placeholder - else - = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f8b6fa253c4..fefa652a3da 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -13,7 +13,11 @@ required: true, class: 'form-control new-file-name' .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' + .license-selector.js-license-selector.hide + = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} + + .encoding-selector + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-content.code %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index abcfca4cd11..5926d181ba3 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} + %tr.line_holder + = 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}} - = link_to raw(line_old), "#" - %td.new_line.diff-line-num - = link_to raw(line_new) , "#" + %tr.line_holder{ id: line_old } + %td.old_line.diff-line-num{ data: { linenumber: line_old } } + = link_to raw(line_old), "##{line_old}" + %td.new_line.diff-line-num{ data: { linenumber: line_old } } + = link_to raw(line_new) , "##{line_old}" %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/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 1dd2b5c0af7..0459699432e 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -14,5 +14,5 @@ cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) :javascript - blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) + blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}") new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index aa85f495e39..0406fc21d77 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -58,6 +58,6 @@ %th Coverage %th - = render @builds, commit_sha: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? + = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index b50b2cf3764..99d72aa7935 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -10,7 +10,7 @@ - merge_request = @build.merge_request - if merge_request via - = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request) + = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) #up-build-trace - builds = @build.commit.builds.latest.to_a @@ -110,7 +110,7 @@ = icon('folder-open') Browse - .build-widget + .build-widget.build-controls %h4.title Build ##{@build.id} - if can?(current_user, :update_build, @project) @@ -127,6 +127,9 @@ data: { confirm: 'Are you sure you want to erase this build?' } do = icon('eraser') Erase + - if @build.has_trace? + = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), + class: 'btn btn-sm btn-success', target: '_blank' .clearfix - if @build.duration diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 7ded4828b2f..745ff461d76 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -21,7 +21,7 @@ %td = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" - - if !defined?(ref) || ref + - if defined?(ref) && ref %td - if build.ref = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref) diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_change.html.haml index 52ca3ed5b14..44ef1fdbbe3 100644 --- a/app/views/projects/commit/_revert.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,13 +1,21 @@ -#modal-revert-commit.modal +- case type.to_s +- when 'revert' + - label = 'Revert' + - target_label = 'Revert in branch' +- when 'cherry-pick' + - label = 'Cherry-pick' + - target_label = 'Pick into branch' + +.modal{id: "modal-#{type}-commit"} .modal-dialog .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== Revert this #{revert_commit_type(commit)} + %h3.page-title== #{label} this #{commit.change_type_title} .modal-body - = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do + = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do .form-group.branch - = label_tag 'target_branch', 'Revert in branch', class: 'control-label' + = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch" - if can?(current_user, :push_code, @project) @@ -20,7 +28,7 @@ - else = hidden_field_tag 'create_merge_request', 1 .form-actions - = submit_tag "Revert", class: 'btn btn-create' + = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) @@ -28,4 +36,4 @@ = commit_in_fork_help :javascript - new NewCommitForm($('.js-create-dir-form')) + new NewCommitForm($('.js-#{type}-form')) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 9cb14b6a90f..b21acac757d 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -18,6 +18,7 @@ Browse Files - unless @commit.has_been_reverted?(current_user) = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) + = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) %div %p diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 096f7058bd4..e5e3d696035 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -13,4 +13,5 @@ diff_refs: @diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - = render "projects/commit/revert", commit: @commit, title: @commit.title + - %w(revert cherry-pick).each do |type| + = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index b231b584eab..655cb0ac3cb 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -10,7 +10,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 ... @@ -30,5 +30,5 @@ by = commit_author_link(commit, avatar: true, size: 24) .committed_ago - #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + #{time_ago_with_tooltip(commit.committed_date)} = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index c52cf25d40a..bcdb09208aa 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -39,4 +39,4 @@ = spinner :javascript - CommitsList.init("#{@ref}", #{@limit}); + CommitsList.init(#{@limit}); 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" ? " " : line.old_pos) + %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "new" ? " ".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" ? " " : line.new_pos) + %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + - link_text = type == "old" ? " ".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/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index d7c49068745..81948513e43 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,11 +14,11 @@ %td.new_line.diff-line-num %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -27,11 +27,11 @@ - new_line_class = nil - new_line_code = left[:line_code] - %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if @comments_allowed && can?(current_user, :create_note, @project) = link_to_new_diff_note(right[:line_code], 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - if @reply_allowed - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) 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/empty.html.haml b/app/views/projects/empty.html.haml index 6ad7b05155a..52d093871b4 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -14,8 +14,10 @@ %p If you already have files you can push them using command line instructions below. %p - Otherwise you can start with - = link_to "adding README", new_readme_path, class: 'underlined-link' + Otherwise you can start with adding a + = link_to "README", new_readme_path, class: 'underlined-link' + or a + = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' file to this project. - if can?(current_user, :push_code, @project) diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index c15386b4883..f21c864e35c 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -15,12 +15,13 @@ - if defined?(commit_sha) && commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" - - %td - - if generic_commit_status.ref - = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) - - else - .light none + + - if defined?(ref) && ref + %td + - if generic_commit_status.ref + = link_to generic_commit_status.ref, namespace_project_commits_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.ref) + - else + .light none - if defined?(runner) && runner %td diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index e39224d86c6..aae3abcad4b 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -74,16 +74,15 @@ .panel.panel-default .panel-heading Webhooks (#{@hooks.count}) - %ul.well-list + %ul.content-list - @hooks.each do |hook| %li - .pull-right + .controls = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - .clearfix - %span.monospace= hook.url - %p + .monospace= hook.url + %div - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize - SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 33c48199ba5..7076f5db015 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue :javascript diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 4aa92d0b39e..9ad86ed71c9 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 @@ -48,6 +48,11 @@ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = icon('clock-o') = issue.milestone.title + - if issue.due_date + %span{class: "#{'cred' if issue.overdue?}"} + + = icon('calendar') + = issue.due_date.to_s(:medium) - if issue.labels.any? - issue.labels.each do |label| diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6fa059cbe68..bde80bbb54b 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,80 +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") + + - 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 - .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 +.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') - .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') + #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } } + // This element is filled in using JavaScript. - .merge-requests - = render 'merge_requests' - = render 'related_branches' + #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 deleted file mode 100644 index 986d8c220db..00000000000 --- a/app/views/projects/issues/update.js.haml +++ /dev/null @@ -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/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 1e6724fc92b..88525f4036a 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 7bfde8b1c57..330d750a681 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/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 9e59f7df71b..2f14a91e64f 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -10,7 +10,7 @@ %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request = f.hidden_field :source_project_id = f.hidden_field :source_branch diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1dd8f721f7e..290753d57c6 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,10 +1,9 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes +- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) -= render "header_title" - -- if params[:view] == 'parallel' +- if diff_view == 'parallel' - fluid_layout true .merge-request{'data-url' => merge_request_path(@merge_request)} @@ -32,8 +31,8 @@ %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 + %span.label-branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) @@ -51,7 +50,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 @@ -87,8 +86,10 @@ = spinner = render 'shared/issuable/sidebar', issuable: @merge_request -- if @merge_request.can_be_reverted? - = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_reverted?(current_user) + = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_cherry_picked? + = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript var merge_request; 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/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index fc62bb5bce9..b31ea5e5321 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,7 +1,7 @@ -- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" = render "header_title" %h3.page-title - Edit Merge Request ##{@merge_request.iid} + Edit Merge Request #{@merge_request.to_reference} %hr = render 'form' diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index fc03ee73a3d..f5bf16ef3ad 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" = render "header_title" .merge-request 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..36c275e8be1 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' + = 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" 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 deleted file mode 100644 index 9cce5660e1c..00000000000 --- a/app/views/projects/merge_requests/update.js.haml +++ /dev/null @@ -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/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 3abae9f0bf6..ec4beae9727 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -44,3 +44,8 @@ $('.remove_source_branch_in_progress').hide(); $('.remove_source_branch_widget.failed').show(); }); + - else + %p + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + = render 'projects/merge_requests/widget/merged_buttons' diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 85a3a6ba9e2..56167509af9 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -1,11 +1,14 @@ -- source_branch_exists = local_assigns.fetch(:source_branch_exists, false) -- mr_can_be_reverted = @merge_request.can_be_reverted? +- can_remove_source_branch = local_assigns.fetch(:source_branch_exists, false) && @merge_request.can_remove_source_branch?(current_user) +- mr_can_be_reverted = @merge_request.can_be_reverted?(current_user) +- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked? -- if source_branch_exists || mr_can_be_reverted +- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .btn-group - - if source_branch_exists + - if can_remove_source_branch = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + - if mr_can_be_cherry_picked + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index b2dae1c70ee..687222fa92f 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,6 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f| = form_errors(@milestone) - .row .col-md-6 .form-group @@ -11,7 +10,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' = render 'projects/notes/hints' .clearfix .error-alert 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/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 23e4f93eab5..c87a3fadf72 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index c446ecec2c3..d0ac380f216 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 03a44ca99c0..aeb7c1d5ee4 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)} @@ -7,7 +8,9 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference} commented" + = "#{note.author.to_reference}" + - if !note.system + commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') .note-actions @@ -15,16 +18,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" ? " " : line.old_pos) - %td.new_line.diff-line-num - = raw(type == "old" ? " " : 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" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".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/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml index 62888e41935..ae13f8428f0 100644 --- a/app/views/projects/project_members/_shared_group_members.html.haml +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -8,7 +8,7 @@ group, members with %strong #{group_links.human_access} role (#{shared_group_users_count}) - - if current_user.can?(:admin_group, shared_group) + - if can?(current_user, :admin_group, shared_group) .panel-head-actions = link_to group_group_members_path(shared_group), class: 'btn btn-sm' do %i.fa.fa-pencil-square-o diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index c4a3f06ee06..6f0b32aa165 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -9,11 +9,11 @@ %strong #{@tag.name} .prepend-top-default - = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f| + = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .error-alert - .form-actions.prepend-top-default - = f.submit 'Save changes', class: 'btn btn-save' - = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" + .error-alert + .form-actions.prepend-top-default + = f.submit 'Save changes', class: 'btn btn-save' + = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 6a37f444bb7..9fa4127c948 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,7 +1,10 @@ %h3 Shared runners -.bs-callout.bs-callout-warning - GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. +.bs-callout.bs-callout-warning.shared-runners-description + - if shared_runners_text.present? + = markdown(shared_runners_text, pipeline: 'plain_markdown') + - else + Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com). %hr - if @project.shared_runners_enabled? = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4310f038fc9..d854ac21725 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -36,9 +36,9 @@ %li = link_to 'Changelog', changelog_path(@project) - - if @repository.license + - if @repository.license_blob %li - = link_to 'License', license_path(@project) + = link_to license_short_name(@project), license_path(@project) - if @repository.contribution_guide %li @@ -47,15 +47,15 @@ - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing - = link_to add_changelog_path(@project) do + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do Add Changelog - - unless @repository.license + - unless @repository.license_blob %li.missing - = link_to add_license_path(@project) do + = link_to add_special_file_path(@project, file_name: 'LICENSE') do Add License - unless @repository.contribution_guide %li.missing - = link_to add_contribution_guide_path(@project) do + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do Add Contribution guide - if @repository.commit diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 77c7c4d23de..b40a6e5cb2d 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,7 +10,7 @@ New Tag %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 @@ -30,9 +30,9 @@ = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', attr: :release_description, classes: 'description form-control' + = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'projects/notes/hints' - .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. + .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 812876e2835..797a1a59e9f 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) = f.hidden_field :title, value: @page.title @@ -11,7 +11,7 @@ = f.label :content, class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do - = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'projects/notes/hints' .clearfix 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..a585147ddd1 --- /dev/null +++ b/app/views/repository_check_mailer/notify.html.haml @@ -0,0 +1,8 @@ +%p + #{@message}. + +%p + = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1) + +%p + You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}. 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..93db151329e --- /dev/null +++ b/app/views/repository_check_mailer/notify.text.haml @@ -0,0 +1,6 @@ +#{@message}. +\ +View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)} + +You are receiving this message because you are a GitLab administrator +for #{Gitlab.config.gitlab.url}. diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index faeb2b55c6f..333f6533213 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -2,7 +2,7 @@ %h4 = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - .pull-right ##{merge_request.iid} + .pull-right #{merge_request.to_reference} - if merge_request.description.present? .description.term = preserve do diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 9544e3d3e17..d9400b1d9fa 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,5 +1,5 @@ - project = note.project -- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- note_url = Gitlab::UrlBuilder.build(note) - noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index b38c5e18efb..9ce5562e667 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -2,4 +2,4 @@ %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line) + = markdown(label.description, pipeline: :single_line)
\ No newline at end of file diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml new file mode 100644 index 00000000000..dc89e36419c --- /dev/null +++ b/app/views/shared/_labels_row.html.haml @@ -0,0 +1,3 @@ +- labels.each do |label| + %span.label-row + = link_to_label(label, tooltip: false) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index fc935166bf6..4eaf7c2a025 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -62,6 +62,14 @@ %strong Build events %p.light This url will be triggered when a build status changes + - if @service.supported_events.include?("wiki_page") + %div + = form.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = form.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This url will be triggered when a wiki page is created/updated - @service.fields.each do |field| diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index e3a6a5a68b6..d327bd0a96f 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -20,6 +20,11 @@ = sort_title_milestone_soon = link_to page_filter_path(sort: sort_value_milestone_later) do = sort_title_milestone_later + - if controller.controller_name == 'issues' || controller.action_name == 'issues' + = link_to page_filter_path(sort: sort_value_due_date_soon) do + = sort_title_due_date_soon + = link_to page_filter_path(sort: sort_value_due_date_later) do + = sort_title_due_date_later = link_to page_filter_path(sort: sort_value_upvotes) do = sort_title_upvotes = link_to page_filter_path(sort: sort_value_downvotes) do diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 921eaefd79a..fc1101646fb 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -23,6 +23,7 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" + .pull-right = render 'shared/sort_dropdown' @@ -46,9 +47,10 @@ .filter-item.inline = button_tag "Update issues", class: "btn update_selected_issues btn-save" -- if @label - .gray-content-block.second-block - = render "shared/label_row", label: @label + - if !@labels.nil? + .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } + - if @labels.any? + = render "shared/labels_row", labels: @labels :javascript new UsersSelect(); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 757a3812deb..18b091df39b 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 @@ -29,7 +29,8 @@ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :description, - classes: 'description form-control' + classes: 'note-textarea', + placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .clearfix .error-alert @@ -70,13 +71,14 @@ - if can? current_user, :admin_milestone, issuable.project = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank .form-group + - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10 - - if issuable.project.labels.any? - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } + .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } + - if has_labels + .issuable-form-select-holder + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else - .prepend-top-10 %span.light No labels yet. - if can? current_user, :admin_label, issuable.project @@ -87,9 +89,10 @@ .form-group = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 - - projects = project_options(issuable, current_user, ability: :admin_issue) - = select_tag(:move_to_project_id, projects, include_blank: true, - class: 'select2', data: { placeholder: 'Select project' }) + .issuable-form-select-holder + - projects = project_options(issuable, current_user, ability: :admin_issue) + = select_tag(:move_to_project_id, projects, include_blank: true, + class: 'select2', data: { placeholder: 'Select project' }) %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } @@ -101,13 +104,15 @@ .form-group = f.label :source_branch, class: 'control-label' .col-sm-10 - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + .issuable-form-select-holder + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group = f.label :target_branch, class: 'control-label' .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) + .issuable-form-select-holder + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - if @merge_request.new_record? - %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) @@ -128,8 +133,6 @@ - else .pull-right - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project) - = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, - method: :delete, class: 'btn btn-grouped' do - = icon('trash-o') - Delete + = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" }, + method: :delete, class: 'btn btn-danger btn-grouped' = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel' diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index f722e61eeac..61fd1e9c335 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,44 +1,14 @@ - if params[:label_name].present? - = hidden_field_tag(:label_name, params[:label_name]) + - if params[:label_name].respond_to?('any?') + - params[:label_name].each do |label| + = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %span.dropdown-toggle-text - = h(params[:label_name].presence || "Label") + = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - = dropdown_title("Filter by label") - = dropdown_filter("Search labels") - = dropdown_content - - if @project - = dropdown_footer do - %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project - %li - %a.dropdown-toggle-page{href: "#"} - Create new - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - - if can? current_user, :admin_label, @project - Manage labels - - else - View labels + = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } - if can? current_user, :admin_label, @project and @project - .dropdown-page-two.dropdown-new-label - = dropdown_title("Create new label", back: true) - = dropdown_content do - .dropdown-labels-error.js-label-error - %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} - .suggest-colors.suggest-colors-dropdown - - suggested_colors.each do |color| - = link_to '#', style: "background-color: #{color}", data: { color: color } do -   - .dropdown-label-color-input - .dropdown-label-color-preview.js-dropdown-label-color-preview - %input#new_label_color.dropdown-input-field{ type: "text" } - .clearfix - %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"} - Create - %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"} - Cancel + = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml new file mode 100644 index 00000000000..3bc57d3d2ac --- /dev/null +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -0,0 +1,17 @@ +.dropdown-page-two.dropdown-new-label + = dropdown_title("Create new label", back: true) + = dropdown_content do + .dropdown-labels-error.js-label-error + %input#new_label_name.default-dropdown-input{ type: "text", placeholder: "Name new label" } + .suggest-colors.suggest-colors-dropdown + - suggested_colors.each do |color| + = link_to '#', style: "background-color: #{color}", data: { color: color } do +   + .dropdown-label-color-input + .dropdown-label-color-preview.js-dropdown-label-color-preview + %input#new_label_color.default-dropdown-input{ type: "text" } + .clearfix + %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" } + Create + %button.btn.btn-default.pull-right.js-cancel-label-btn{ type: "button" } + Cancel diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml new file mode 100644 index 00000000000..7f4867417f7 --- /dev/null +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -0,0 +1,20 @@ +- title = local_assigns.fetch(:title, 'Assign labels') +- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') +.dropdown-page-one + = dropdown_title(title) + = dropdown_filter(filter_placeholder) + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels + = dropdown_loading
\ No newline at end of file diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index a6970b7eebb..1d9b09a5ef1 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ - else - page_context_word = 'issues' %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do #{state_filters_text_for(:opened, @project)} - if defined?(type) && type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do #{state_filters_text_for(:merged, @project)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do #{state_filters_text_for(:closed, @project)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do #{state_filters_text_for(:closed, @project)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do #{state_filters_text_for(:all, @project)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 94affa4b59a..ed1b8a8da2a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -10,17 +10,17 @@ = sidebar_gutter_toggle_icon .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - if prev_issuable = prev_issuable_for(issuable) - = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn' + = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Prev - if next_issuable = next_issuable_for(issuable) - = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn' + = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Next - = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)} - if issuable.assignee @@ -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 @@ -58,7 +58,7 @@ - if issuable.milestone = issuable.milestone.title - else - No + None .title.hide-collapsed Milestone = icon('spinner spin', class: 'block-loading') @@ -75,6 +75,34 @@ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) + - if issuable.has_attribute?(:due_date) + .block.due_date + .sidebar-collapsed-icon + = icon('calendar') + %span.js-due-date-sidebar-value + = issuable.due_date.try(:to_s, :medium) || 'None' + .title.hide-collapsed + Due date + = icon('spinner spin', class: 'block-loading') + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.bold.hide-collapsed + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + .light None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .selectbox.hide-collapsed + = f.hidden_field :due_date, value: issuable.due_date + .dropdown + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %span.dropdown-toggle-text Due date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title('Due date') + = dropdown_content do + .js-due-date-calendar + - if issuable.project.labels.any? .block.labels .sidebar-collapsed-icon @@ -101,23 +129,9 @@ Label = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - = dropdown_title("Assign labels") - = dropdown_filter("Search labels") - = dropdown_content - - if @project - = dropdown_footer do - %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project - %li - %a.dropdown-toggle-page{href: "#"} - Create new - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - - if can? current_user, :admin_label, @project - Manage labels - - else - View labels + = render partial: "shared/issuable/label_page_default" + - if can? current_user, :admin_label, @project and @project + = render partial: "shared/issuable/label_page_create" = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -128,7 +142,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 )} @@ -150,6 +164,7 @@ :javascript new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); - new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); + new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') - new Sidebar();
\ No newline at end of file + new DueDateSelect(); + sidebar = new Sidebar(); diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index e1127b2311c..47b66d44e43 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -23,5 +23,5 @@ - if assignee = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), - class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do + class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 868b2357003..b15e8ea73fe 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -4,15 +4,16 @@ %li %span.label-row - = link_to milestones_label_path(options) do - - render_colored_label(label, tooltip: false) - %span.prepend-left-10 + %span.label-name + = link_to milestones_label_path(options) do + - render_colored_label(label, tooltip: false) + %span.prepend-description-left = markdown(label.description, pipeline: :single_line) - .pull-right - %strong.issues-count + .pull-info-right + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'opened')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - %strong.issues-count + %span.append-right-20 = link_to milestones_label_path(options.merge(state: 'closed')) do - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index e7e04621ff4..1169bed0382 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,4 +1,5 @@ - @sort ||= sort_value_recently_updated +- personal = params[:personal] - archived = params[:archived] .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} @@ -10,7 +11,7 @@ Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do = title %li.divider @@ -20,3 +21,11 @@ %li = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects + - if current_user + %li.divider + %li + = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do + Owned by anyone + %li + = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do + Owned by me diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0c4b6a5618b..3028491e5b6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -6,8 +6,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") -= render 'shared/show_aside' - .user-profile .cover-block .cover-controls diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index dc249155b92..59e12798691 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -19,12 +19,12 @@ var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; var noteable_type = "#{votable.class.name.underscore}"; var noteable_id = "#{votable.id}"; - var aliases = #{AwardEmoji.aliases.to_json}; + var unicodes = #{AwardEmoji.unicode.to_json}; window.awards_handler = new AwardsHandler( get_emojis_url, post_emoji_url, noteable_type, noteable_id, - aliases + unicodes ); 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/post_receive.rb b/app/workers/post_receive.rb index 3cc232ef1ae..f3327ca9e61 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -39,15 +39,15 @@ class PostReceive end if Gitlab::Git.tag_ref?(ref) - GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref) - else + GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute + elsif Gitlab::Git.branch_ref?(ref) GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end end end private - + def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") 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..a76729e3c74 --- /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) + repositories = [project.repository] + repositories << project.wiki.repository if project.wiki_enabled? + # Use 'map do', not 'all? do', to prevent short-circuiting + repositories.map { |repository| git_fsck(repository.path_to_repo) }.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 |