diff options
Diffstat (limited to 'app')
712 files changed, 12054 insertions, 7510 deletions
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf Binary files differindex cb89a2d171e..9c9b5cb7f03 100755..100644 --- a/app/assets/fonts/SourceSansPro-Black.ttf +++ b/app/assets/fonts/SourceSansPro-Black.ttf diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf Binary files differnew file mode 100644 index 00000000000..294ce5abe8f --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf Binary files differindex 5d65c93242f..5d65c93242f 100755..100644 --- a/app/assets/fonts/SourceSansPro-Bold.ttf +++ b/app/assets/fonts/SourceSansPro-Bold.ttf diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf Binary files differnew file mode 100644 index 00000000000..3decd130070 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf Binary files differindex bb4176c6fff..253eafa3783 100755..100644 --- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf +++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf Binary files differnew file mode 100644 index 00000000000..00d7e9a7aa8 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf Binary files differnew file mode 100644 index 00000000000..f7af5377595 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-It.ttf diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf Binary files differindex 83a0a336661..83a0a336661 100755..100644 --- a/app/assets/fonts/SourceSansPro-Light.ttf +++ b/app/assets/fonts/SourceSansPro-Light.ttf diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf Binary files differnew file mode 100644 index 00000000000..f18827985ef --- /dev/null +++ b/app/assets/fonts/SourceSansPro-LightIt.ttf diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf Binary files differindex 44486cdc670..44486cdc670 100755..100644 --- a/app/assets/fonts/SourceSansPro-Regular.ttf +++ b/app/assets/fonts/SourceSansPro-Regular.ttf diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf Binary files differindex 86b00c067e0..86b00c067e0 100755..100644 --- a/app/assets/fonts/SourceSansPro-Semibold.ttf +++ b/app/assets/fonts/SourceSansPro-Semibold.ttf diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf Binary files differnew file mode 100644 index 00000000000..13d66a1fc45 --- /dev/null +++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf diff --git a/app/assets/images/auth_buttons/facebook_64.png b/app/assets/images/auth_buttons/facebook_64.png Binary files differnew file mode 100644 index 00000000000..1f1a80d7368 --- /dev/null +++ b/app/assets/images/auth_buttons/facebook_64.png diff --git a/app/assets/images/brand_logo.png b/app/assets/images/brand_logo.png Binary files differdeleted file mode 100644 index 9c564bb6141..00000000000 --- a/app/assets/images/brand_logo.png +++ /dev/null diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png Binary files differnew file mode 100644 index 00000000000..a8ad7b6eab6 --- /dev/null +++ b/app/assets/images/emoji.png diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png Binary files differnew file mode 100644 index 00000000000..0c157546b9c --- /dev/null +++ b/app/assets/images/gitlab_logo.png diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png Binary files differindex 60021d5ac47..7d89da97e11 100644 --- a/app/assets/images/icon-link.png +++ b/app/assets/images/icon-link.png diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 9e5d594c861..746fa3cea87 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -2,6 +2,8 @@ groups_path: "/api/:version/groups.json" group_path: "/api/:version/groups/:id.json" namespaces_path: "/api/:version/namespaces.json" + group_projects_path: "/api/:version/groups/:id/projects.json" + projects_path: "/api/:version/projects.json" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -44,6 +46,35 @@ ).done (namespaces) -> callback(namespaces) + # Return projects list. Filtered by query + projects: (query, callback) -> + url = Api.buildUrl(Api.projects_path) + + $.ajax( + url: url + data: + private_token: gon.api_token + search: query + per_page: 20 + dataType: "json" + ).done (projects) -> + callback(projects) + + # Return group projects list. Filtered by query + groupProjects: (group_id, query, callback) -> + url = Api.buildUrl(Api.group_projects_path) + url = url.replace(':id', group_id) + + $.ajax( + url: url + data: + private_token: gon.api_token + search: query + per_page: 20 + dataType: "json" + ).done (projects) -> + callback(projects) + 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 945ffb660e6..b9b095e004a 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -5,17 +5,17 @@ # the compiled file. # #= require jquery -#= require jquery.ui.all +#= require jquery-ui #= require jquery_ujs #= require jquery.cookie #= require jquery.endless-scroll #= require jquery.highlight -#= require jquery.history #= require jquery.waitforimages #= require jquery.atwho #= require jquery.scrollTo -#= require jquery.blockUI #= require jquery.turbolinks +#= require d3 +#= require cal-heatmap #= require turbolinks #= require autosave #= require bootstrap @@ -27,7 +27,6 @@ #= require branch-graph #= require ace/ace #= require ace/ext-searchbox -#= require d3 #= require underscore #= require nprogress #= require nprogress-turbolinks @@ -39,7 +38,6 @@ #= require shortcuts_dashboard_navigation #= require shortcuts_issuable #= require shortcuts_network -#= require cal-heatmap #= require jquery.nicescroll.min #= require_tree . @@ -135,17 +133,25 @@ $ -> ), 1 # Initialize tooltips - $('body').tooltip({ - selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a' + $('body').tooltip( + selector: '.has_tooltip, [data-toggle="tooltip"]' placement: (_, el) -> $el = $(el) - if $el.attr('id') == 'js-shortcuts-home' - # Place the logo tooltip on the right when collapsed, bottom when expanded - $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom' - else - # Otherwise use the data-placement attribute, or 'bottom' if undefined - $el.data('placement') or 'bottom' - }) + $el.data('placement') || 'bottom' + ) + + $('.header-logo .home').tooltip( + placement: (_, el) -> + $el = $(el) + if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom' + container: 'body' + ) + + $('.page-with-sidebar').tooltip( + selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user' + placement: 'right' + container: 'body' + ) # Form submitter $('.trigger-submit').on 'change', -> diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee new file mode 100644 index 00000000000..619abb1fb07 --- /dev/null +++ b/app/assets/javascripts/awards_handler.coffee @@ -0,0 +1,166 @@ +class @AwardsHandler + constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> + $(".add-award").click (event)-> + event.stopPropagation() + event.preventDefault() + $(".emoji-menu").show() + + $("html").click -> + if !$(event.target).closest(".emoji-menu").length + if $(".emoji-menu").is(":visible") + $(".emoji-menu").hide() + + @renderFrequentlyUsedBlock() + @setupSearch() + + addAward: (emoji) -> + emoji = @normilizeEmojiName(emoji) + @postEmoji emoji, => + @addAwardToEmojiBar(emoji) + + $(".emoji-menu").hide() + + addAwardToEmojiBar: (emoji) -> + @addEmojiToFrequentlyUsedList(emoji) + + emoji = @normilizeEmojiName(emoji) + if @exist(emoji) + if @isActive(emoji) + @decrementCounter(emoji) + else + counter = @findEmojiIcon(emoji).siblings(".counter") + counter.text(parseInt(counter.text()) + 1) + counter.parent().addClass("active") + @addMeToAuthorList(emoji) + else + @createEmoji(emoji) + + exist: (emoji) -> + @findEmojiIcon(emoji).length > 0 + + isActive: (emoji) -> + @findEmojiIcon(emoji).parent().hasClass("active") + + decrementCounter: (emoji) -> + counter = @findEmojiIcon(emoji).siblings(".counter") + emojiIcon = counter.parent() + + if parseInt(counter.text()) > 1 + counter.text(parseInt(counter.text()) - 1) + emojiIcon.removeClass("active") + @removeMeFromAuthorList(emoji) + else if emoji =="thumbsup" || emoji == "thumbsdown" + emojiIcon.tooltip("destroy") + counter.text(0) + emojiIcon.removeClass("active") + else + emojiIcon.tooltip("destroy") + emojiIcon.remove() + + removeMeFromAuthorList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block.attr("data-original-title").split(", ") + authors = _.without(authors, "me").join(", ") + award_block.attr("title", authors) + @resetTooltip(award_block) + + addMeToAuthorList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block.attr("data-original-title").split(", ") + authors.push("me") + award_block.attr("title", authors.join(", ")) + @resetTooltip(award_block) + + resetTooltip: (award) -> + award.tooltip("destroy") + + # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. + setTimeout (-> + award.tooltip() + ), 200 + + + createEmoji: (emoji) -> + emojiCssClass = @resolveNameToCssClass(emoji) + + nodes = [] + nodes.push("<div class='award active' title='me'>") + nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") + nodes.push("<div class='counter'>1</div>") + nodes.push("</div>") + + emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) + + $(".award").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}" + + postEmoji: (emoji, callback) -> + $.post @post_emoji_url, { note: { + note: ":#{emoji}:" + noteable_type: @noteable_type + noteable_id: @noteable_id + }},(data) -> + if data.ok + callback.call() + + findEmojiIcon: (emoji) -> + $(".award [data-emoji='#{emoji}']") + + scrollToAwards: -> + $('body, html').animate({ + scrollTop: $('.awards').offset().top - 80 + }, 200) + + normilizeEmojiName: (emoji) -> + @aliases[emoji] || emoji + + addEmojiToFrequentlyUsedList: (emoji) -> + frequently_used_emojis = @getFrequentlyUsedEmojis() + frequently_used_emojis.push(emoji) + $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) + + getFrequentlyUsedEmojis: -> + frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") + _.compact(_.uniq(frequently_used_emojis)) + + renderFrequentlyUsedBlock: -> + if $.cookie('frequently_used_emojis') + frequently_used_emojis = @getFrequentlyUsedEmojis() + + ul = $("<ul>") + + for emoji in frequently_used_emojis + do (emoji) -> + $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + + $("input.emoji-search").after(ul).after($("<h5>").text("Frequently used")) + + setupSearch: -> + $("input.emoji-search").keyup (ev) => + term = $(ev.target).val() + + # Clean previous search results + $("ul.emoji-search,h5.emoji-search").remove() + + if term + # Generate a search result block + h5 = $("<h5>").text("Search results").addClass("emoji-search") + found_emojis = @searchEmojis(term).show() + ul = $("<ul>").addClass("emoji-search").append(found_emojis) + $(".emoji-menu-content ul, .emoji-menu-content h5").hide() + $(".emoji-menu-content").append(h5).append(ul) + else + $(".emoji-menu-content").children().show() + + searchEmojis: (term)-> + $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee index 5b604adbbb1..9df932817f6 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee @@ -23,18 +23,6 @@ class @BlobFileDropzone init: -> this.on 'addedfile', (file) -> $('.dropzone-alerts').html('').hide() - commit_message = form.find('#commit_message')[0] - - if /^Upload/.test(commit_message.placeholder) - commit_message.placeholder = 'Upload ' + file.name - - return - - this.on 'removedfile', (file) -> - commit_message = form.find('#commit_message')[0] - - if /^Upload/.test(commit_message.placeholder) - commit_message.placeholder = 'Upload new file' return @@ -47,8 +35,9 @@ class @BlobFileDropzone return this.on 'sending', (file, xhr, formData) -> - formData.append('new_branch', form.find('#new_branch').val()) - formData.append('commit_message', form.find('#commit_message').val()) + formData.append('target_branch', form.find('.js-target-branch').val()) + formData.append('create_merge_request', form.find('.js-create-merge-request').val()) + formData.append('commit_message', form.find('.js-commit-message').val()) return # Override behavior of adding error underneath preview diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 050888f9c15..f6bf836f19f 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -11,10 +11,10 @@ class @EditBlob if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode - $(".js-commit-button").click -> - $("#file-content").val editor.getValue() - $(".file-editor form").submit() - return false + # Before a form submission, move the content from the Ace editor into the + # submitted textarea + $('form').submit -> + $("#file-content").val(editor.getValue()) editModePanes = $(".js-edit-mode-pane") editModeLinks = $(".js-edit-mode a") diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee index 1f36a53f191..68c5e5195e3 100644 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -11,10 +11,10 @@ class @NewBlob if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode - $(".js-commit-button").click -> - $("#file-content").val editor.getValue() - $(".file-editor form").submit() - return false + # 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/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 4c4bc3d66ed..d80e0e716ce 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,9 +1,4 @@ class @Calendar - options = - month: "short" - day: "numeric" - year: "numeric" - constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> cal = new CalHeatMap() cal.init @@ -25,7 +20,7 @@ class @Calendar 30 ] legendCellPadding: 3 - cellSize: $('.user-calendar').width() / 80 + cellSize: $('.user-calendar').width() / 73 onClick: (date, count) -> formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() $.ajax diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index c30859b484b..44d5ddb7d95 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -22,7 +22,7 @@ class CiBuild # Only valid for runnig build when output changes during time # CiBuild.interval = setInterval => - if window.location.href is build_url + if window.location.href.split("#").first() is build_url $.ajax url: build_url dataType: "json" @@ -31,7 +31,7 @@ class CiBuild $('#build-trace code').html build.trace_html $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>' @checkAutoscroll() - else + else if build.status != build_status Turbolinks.visit build_url , 4000 diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee new file mode 100644 index 00000000000..24301e01b10 --- /dev/null +++ b/app/assets/javascripts/copy_to_clipboard.js.coffee @@ -0,0 +1,37 @@ +#= require clipboard + +genericSuccess = (e) -> + showTooltip(e.trigger, 'Copied!') + + # Clear the selection and blur the trigger so it loses its border + e.clearSelection() + $(e.trigger).blur() + +# Safari doesn't support `execCommand`, so instead we inform the user to +# copy manually. +# +# See http://clipboardjs.com/#browser-support +genericError = (e) -> + if /Mac/i.test(navigator.userAgent) + key = '⌘' # Command + else + key = 'Ctrl' + + showTooltip(e.trigger, "Press #{key}-C to copy") + +showTooltip = (target, title) -> + $(target). + tooltip( + container: 'body' + html: 'true' + placement: 'auto bottom' + title: title + trigger: 'manual' + ). + tooltip('show'). + one('mouseleave', -> $(this).tooltip('hide')) + +$ -> + clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]' + clipboard.on 'success', genericSuccess + clipboard.on 'error', genericError diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 5bf0b302179..69e061ce6e9 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -28,6 +28,8 @@ class Dispatcher when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() new DropzoneInput($('.milestone-form')) + when 'groups:milestones:new' + new ZenMode() when 'projects:compare:show' new Diff() when 'projects:issues:new','projects:issues:edit' @@ -39,9 +41,15 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new DropzoneInput($('.merge-request-form')) new IssuableForm($('.merge-request-form')) + when 'projects:tags:new' + new ZenMode() + new DropzoneInput($('.tag-form')) + when 'projects:releases:edit' + new ZenMode() + new DropzoneInput($('.release-form')) when 'projects:merge_requests:show' new Diff() - shortcut_handler = new ShortcutsIssuable() + shortcut_handler = new ShortcutsIssuable(true) new ZenMode() when "projects:merge_requests:diffs" new Diff() @@ -75,7 +83,7 @@ class Dispatcher when 'projects:project_members:index' new ProjectMembers() new UsersSelect() - when 'groups:new', 'groups:edit', 'admin:groups:edit' + when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new' new GroupAvatar() when 'projects:tree:show' new TreeView() diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index 6f789e668af..30a35a04339 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -1,3 +1,5 @@ +#= require markdown_preview + class @DropzoneInput constructor: (form) -> Dropzone.autoDiscover = false @@ -11,17 +13,14 @@ class @DropzoneInput uploadProgress = $("<div class=\"div-dropzone-progress\"></div>") btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>" project_uploads_path = window.project_uploads_path or null - markdown_preview_path = window.markdown_preview_path or null max_file_size = gon.max_file_size or 10 form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "<div class=\"div-dropzone\"></div>" form_textarea.on 'paste', (event) => handlePaste(event) - form_textarea.on "input", -> - hideReferencedUsers() - form_textarea.on "blur", -> - renderMarkdown() + + $(form).setupMarkdownPreview() form_dropzone = $(form).find('.div-dropzone') form_dropzone.parent().addClass "div-dropzone-wrapper" @@ -34,42 +33,6 @@ class @DropzoneInput "opacity": 0 "display": "none" - # Preview button - $(document).off "click", ".js-md-preview-button" - $(document).on "click", ".js-md-preview-button", (e) -> - ### - Shows the Markdown preview. - - Lets the server render GFM into Html and displays it. - ### - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-md-write-button").parent().removeClass "active" - form.find(".js-md-preview-button").parent().addClass "active" - - # toggle content - form.find(".md-write-holder").hide() - form.find(".md-preview-holder").show() - - renderMarkdown() - - # Write button - $(document).off "click", ".js-md-write-button" - $(document).on "click", ".js-md-write-button", (e) -> - ### - Shows the Markdown textarea. - ### - e.preventDefault() - form = $(this).closest("form") - # toggle tabs - form.find(".js-md-write-button").parent().addClass "active" - form.find(".js-md-preview-button").parent().removeClass "active" - - # toggle content - form.find(".md-write-holder").show() - form.find(".md-preview-holder").hide() - dropzone = form_dropzone.dropzone( url: project_uploads_path dictDefaultMessage: "" @@ -136,41 +99,6 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") - hideReferencedUsers = -> - referencedUsers = form.find(".referenced-users") - referencedUsers.hide() - - renderReferencedUsers = (users) -> - referencedUsers = form.find(".referenced-users") - - if referencedUsers.length - if users.length >= 10 - referencedUsers.show() - referencedUsers.find(".js-referenced-users-count").text users.length - else - referencedUsers.hide() - - renderMarkdown = -> - preview = form.find(".js-md-preview") - mdText = form.find(".markdown-area").val() - if mdText.trim().length is 0 - preview.text "Nothing to preview." - hideReferencedUsers() - else - preview.text "Loading..." - $.ajax( - type: "POST", - url: markdown_preview_path, - data: { - text: mdText - }, - dataType: "json" - ).success (data) -> - preview.html data.body - preview.syntaxHighlight() - - renderReferencedUsers data.references.users - formatLink = (link) -> text = "[#{link.alt}](#{link.url})" text = "!#{text}" if link.is_image diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index b39ab0c4475..5de012e409f 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,12 +1,16 @@ class @Flash constructor: (message, type)-> - flash = $(".flash-container") - flash.html("") + @flash = $(".flash-container") + @flash.html("") - $('<div/>', + innerDiv = $('<div/>', class: "flash-#{type}", text: message - ).appendTo(".flash-container") + ) + innerDiv.appendTo(".flash-container") - flash.click -> $(@).fadeOut() - flash.show() + @flash.click -> $(@).fadeOut() + @flash.show() + + pinTo: (selector) -> + @flash.detach().appendTo(selector) diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index c4d3e619f5e..02232698bc2 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -5,9 +5,9 @@ class @IssuableContext new UsersSelect() $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) - $(".context .inline-update").on "change", "select", -> + $(".issuable-sidebar .inline-update").on "change", "select", -> $(this).submit() - $(".context .inline-update").on "change", ".js-assignee", -> + $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() $('.issuable-details').waitForImages -> @@ -21,3 +21,9 @@ class @IssuableContext @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) + + $(".edit-link").click (e) -> + block = $(@).parents('.block') + block.find('.selectbox').show() + block.find('.value').hide() + block.find('.js-select2').select2("open") diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 603a16da1ce..c256ec8f41b 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -1,3 +1,4 @@ +#= require flash #= require jquery.waitforimages #= require task_list @@ -6,16 +7,47 @@ class @Issue # Prevent duplicate event bindings @disableTaskList() - if $("a.btn-close").length + if $('a.btn-close').length @initTaskList() + @initIssueBtnEventListeners() initTaskList: -> - $('.issue-details .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList + $('.detail-page-description .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList + + initIssueBtnEventListeners: -> + issueFailMessage = 'Unable to update this issue at this time.' + $('a.btn-close, a.btn-reopen').on 'click', (e) -> + e.preventDefault() + e.stopImmediatePropagation() + $this = $(this) + isClose = $this.hasClass('btn-close') + $this.prop('disabled', true) + url = $this.attr('href') + $.ajax + type: 'PUT' + url: url, + error: (jqXHR, textStatus, errorThrown) -> + issueStatus = if isClose then 'close' else 'open' + new Flash(issueFailMessage, 'alert') + success: (data, textStatus, jqXHR) -> + if data.saved + $this.addClass('hidden') + if isClose + $('a.btn-reopen').removeClass('hidden') + $('div.status-box-closed').removeClass('hidden') + $('div.status-box-open').addClass('hidden') + else + $('a.btn-close').removeClass('hidden') + $('div.status-box-closed').addClass('hidden') + $('div.status-box-open').removeClass('hidden') + else + new Flash(issueFailMessage, 'alert') + $this.prop('disabled', false) disableTaskList: -> - $('.issue-details .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.issue-details .js-task-list-container' + $('.detail-page-description .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' # TODO (rspeicher): Make the issue description inline-editable like a note so # that we can re-use its form here diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 40bb9e9cb0c..a0acf3028bf 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -15,13 +15,6 @@ $(this).html totalIssues + 1 else $(this).html totalIssues - 1 - $("body").on "click", ".issues-other-filters .dropdown-menu a", -> - $('.issues-list').block( - message: null, - overlayCSS: - backgroundColor: '#DDD' - opacity: .4 - ) reload: -> Issues.initSelects() @@ -29,7 +22,7 @@ $('#filter_issue_search').val($('#issue_search').val()) initSelects: -> - $("select#update_status").select2(width: 'resolve', dropdownAutoWidth: true) + $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true) $("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true) $("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true) $("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true) @@ -54,7 +47,7 @@ form = $("#issue_search_form") search = $("#issue_search").val() $('.issues-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '? '+ form.serialize() + issues_url = form.attr('action') + '?' + form.serialize() $.ajax type: "GET" @@ -65,7 +58,7 @@ success: (data) -> $('.issues-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 + history.replaceState {page: issues_url}, document.title, issues_url Issues.reload() dataType: "json" diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee new file mode 100644 index 00000000000..e864a674cdd --- /dev/null +++ b/app/assets/javascripts/logo.js.coffee @@ -0,0 +1,43 @@ +NProgress.configure(showSpinner: false) + +defaultClass = 'tanuki-shape' +pieces = [ + 'path#tanuki-right-cheek', + 'path#tanuki-right-eye, path#tanuki-right-ear', + 'path#tanuki-nose', + 'path#tanuki-left-eye, path#tanuki-left-ear', + 'path#tanuki-left-cheek', +] +pieceIndex = 0 +firstPiece = pieces[0] + +currentTimer = null +delay = 150 + +clearHighlights = -> + $(".#{defaultClass}.highlight").attr('class', defaultClass) + +start = -> + clearHighlights() + pieceIndex = 0 + pieces.reverse() unless pieces[0] == firstPiece + currentTimer = setInterval(work, delay) + +stop = -> + clearInterval(currentTimer) + clearHighlights() + +work = -> + clearHighlights() + $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight") + + # If we hit the last piece, reset the index and then reverse the array to + # get a nice back-and-forth sweeping look + if pieceIndex == pieces.length - 1 + pieceIndex = 0 + pieces.reverse() + else + pieceIndex++ + +$(document).on('page:fetch', start) +$(document).on('page:change', stop) diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee new file mode 100644 index 00000000000..98fc8f17340 --- /dev/null +++ b/app/assets/javascripts/markdown_preview.js.coffee @@ -0,0 +1,87 @@ +# MarkdownPreview +# +# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, +# and showing a warning when more than `x` users are referenced. +# +class @MarkdownPreview + # Minimum number of users referenced before triggering a warning + referenceThreshold: 10 + + showPreview: (form) -> + preview = form.find('.js-md-preview') + mdText = form.find('textarea.markdown-area').val() + + if mdText.trim().length == 0 + preview.text('Nothing to preview.') + @hideReferencedUsers(form) + else + preview.text('Loading...') + @renderMarkdown mdText, (response) => + preview.html(response.body) + preview.syntaxHighlight() + @renderReferencedUsers(response.references.users, form) + + renderMarkdown: (text, success) -> + return unless window.markdown_preview_path + + $.ajax + type: 'POST' + url: window.markdown_preview_path + data: { text: text } + dataType: 'json' + success: success + + hideReferencedUsers: (form) -> + referencedUsers = form.find('.referenced-users') + referencedUsers.hide() + + renderReferencedUsers: (users, form) -> + referencedUsers = form.find('.referenced-users') + + if referencedUsers.length + if users.length >= @referenceThreshold + referencedUsers.show() + referencedUsers.find('.js-referenced-users-count').text(users.length) + else + referencedUsers.hide() + +markdownPreview = new MarkdownPreview() + +previewButtonSelector = '.js-md-preview-button' +writeButtonSelector = '.js-md-write-button' + +$.fn.setupMarkdownPreview = -> + $form = $(this) + + form_textarea = $form.find('textarea.markdown-area') + + form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) + form_textarea.on 'blur', -> markdownPreview.showPreview($form) + +$(document).on 'click', previewButtonSelector, (e) -> + e.preventDefault() + + $form = $(this).closest('form') + + # toggle tabs + $form.find(writeButtonSelector).parent().removeClass('active') + $form.find(previewButtonSelector).parent().addClass('active') + + # toggle content + $form.find('.md-write-holder').hide() + $form.find('.md-preview-holder').show() + + markdownPreview.showPreview($form) + +$(document).on 'click', writeButtonSelector, (e) -> + e.preventDefault() + + $form = $(this).closest('form') + + # toggle tabs + $form.find(writeButtonSelector).parent().addClass('active') + $form.find(previewButtonSelector).parent().removeClass('active') + + # toggle content + $form.find('.md-write-holder').show() + $form.find('.md-preview-holder').hide() diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index b21cb7904b5..9047587db81 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -40,12 +40,12 @@ class @MergeRequest this.$('.all-commits').removeClass 'hide' initTaskList: -> - $('.merge-request-details .js-task-list-container').taskList('enable') - $(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList + $('.detail-page-description .js-task-list-container').taskList('enable') + $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList disableTaskList: -> - $('.merge-request-details .js-task-list-container').taskList('disable') - $(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container' + $('.detail-page-description .js-task-list-container').taskList('disable') + $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container' # TODO (rspeicher): Make the merge request description inline-editable like a # note so that we can re-use its form here diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 593a8f42130..9e2dc1250c9 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -43,6 +43,7 @@ # class @MergeRequestTabs diffsLoaded: false + buildsLoaded: false commitsLoaded: false constructor: (@opts = {}) -> @@ -54,6 +55,12 @@ class @MergeRequestTabs bindEvents: -> $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown + $(document).on 'click', '.js-show-tab', @showTab + + showTab: (event) => + event.preventDefault() + + @activateTab $(event.target).data('action') tabShown: (event) => $target = $(event.target) @@ -63,12 +70,14 @@ class @MergeRequestTabs @loadCommits($target.attr('href')) else if action == 'diffs' @loadDiff($target.attr('href')) + else if action == 'builds' + @loadBuilds($target.attr('href')) @setCurrentAction(action) scrollToElement: (container) -> if window.location.hash - $el = $("#{container} #{window.location.hash}") + $el = $("div#{container} #{window.location.hash}") $('body').scrollTo($el.offset().top) if $el.length # Activate a tab based on the current action @@ -101,7 +110,7 @@ class @MergeRequestTabs action = 'notes' if action == 'show' # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '') + new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '') # Append the new action if we're on a tab other than 'notes' unless action == 'notes' @@ -124,7 +133,7 @@ class @MergeRequestTabs @_get url: "#{source}.json" success: (data) => - document.getElementById('commits').innerHTML = data.html + document.querySelector("div#commits").innerHTML = data.html $('.js-timeago').timeago() @commitsLoaded = true @scrollToElement("#commits") @@ -135,10 +144,22 @@ class @MergeRequestTabs @_get url: "#{source}.json" + @_location.search success: (data) => - document.getElementById('diffs').innerHTML = data.html + document.querySelector("div#diffs").innerHTML = data.html + $('div#diffs .js-syntax-highlight').syntaxHighlight() @diffsLoaded = true @scrollToElement("#diffs") + loadBuilds: (source) -> + return if @buildsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#builds").innerHTML = data.html + $('.js-timeago').timeago() + @buildsLoaded = true + @scrollToElement("#builds") + # Show or hide the loading spinner # # status - Boolean, true to show, false to hide diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 3176e5a8965..738ffc8343b 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -10,17 +10,20 @@ class @MergeRequestWidget constructor: (@opts) -> modal = $('#modal_merge_info').modal(show: false) - mergeInProgress: -> + mergeInProgress: (deleteSourceBranch = false)-> $.ajax type: 'GET' url: $('.merge-request').data('url') success: (data) => if data.state == "merged" - location.reload() + urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' + + window.location.href = window.location.pathname + urlSuffix else if data.merge_error $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>") else - setTimeout(merge_request_widget.mergeInProgress, 2000) + callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) + setTimeout(callback, 2000) dataType: 'json' getMergeStatus: -> diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee index 83434c1b9ba..b3c73ffce5d 100644 --- a/app/assets/javascripts/merge_requests.js.coffee +++ b/app/assets/javascripts/merge_requests.js.coffee @@ -16,7 +16,7 @@ form = $("#issue_search_form") search = $("#issue_search").val() $('.merge-requests-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '? '+ form.serialize() + issues_url = form.attr('action') + '?' + form.serialize() $.ajax type: "GET" @@ -27,7 +27,7 @@ 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 + history.replaceState {page: issues_url}, document.title, issues_url MergeRequests.reload() dataType: "json" diff --git a/app/assets/javascripts/new_branch_form.js.coffee b/app/assets/javascripts/new_branch_form.js.coffee new file mode 100644 index 00000000000..4b350854f78 --- /dev/null +++ b/app/assets/javascripts/new_branch_form.js.coffee @@ -0,0 +1,78 @@ +class @NewBranchForm + constructor: (form, availableRefs) -> + @branchNameError = form.find('.js-branch-name-error') + @name = form.find('.js-branch-name') + @ref = form.find('#ref') + + @setupAvailableRefs(availableRefs) + @setupRestrictions() + @addBinding() + @init() + + addBinding: -> + @name.on 'blur', @validate + + init: -> + @name.trigger 'blur' if @name.val().length > 0 + + setupAvailableRefs: (availableRefs) -> + @ref.autocomplete + source: availableRefs, + minLength: 1 + + setupRestrictions: -> + startsWith = { + pattern: /^(\/|\.)/g, + prefix: "can't start with", + conjunction: "or" + } + + endsWith = { + pattern: /(\/|\.|\.lock)$/g, + prefix: "can't end in", + conjunction: "or" + } + + invalid = { + pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g + prefix: "can't contain", + conjunction: ", " + } + + single = { + pattern: /^@+$/g + prefix: "can't be", + conjunction: "or" + } + + @restrictions = [startsWith, invalid, endsWith, single] + + validate: => + @branchNameError.empty() + + unique = (values, value) -> + values.push(value) unless value in values + values + + formatter = (values, restriction) -> + formatted = values.map (value) -> + switch + when /\s/.test value then 'spaces' + when /\/{2,}/g.test value then 'consecutive slashes' + else "'#{value}'" + + "#{restriction.prefix} #{formatted.join(restriction.conjunction)}" + + validator = (errors, restriction) => + matched = @name.val().match(restriction.pattern) + + if matched + errors.concat formatter(matched.reduce(unique, []), restriction) + else + errors + + errors = @restrictions.reduce validator, [] + + if errors.length > 0 + errorMessage = $("<span/>").text(errors.join(', ')) + @branchNameError.append(errorMessage) diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee new file mode 100644 index 00000000000..03f0f51acfa --- /dev/null +++ b/app/assets/javascripts/new_commit_form.js.coffee @@ -0,0 +1,21 @@ +class @NewCommitForm + constructor: (form) -> + @newBranch = form.find('.js-target-branch') + @originalBranch = form.find('.js-original-branch') + @createMergeRequest = form.find('.js-create-merge-request') + @createMergeRequestContainer = form.find('.js-create-merge-request-container') + + @renderDestination() + @newBranch.keyup @renderDestination + + renderDestination: => + different = @newBranch.val() != @originalBranch.val() + + if different + @createMergeRequestContainer.show() + @createMergeRequest.prop('checked', true) unless @wasDifferent + else + @createMergeRequestContainer.hide() + @createMergeRequest.prop('checked', false) + + @wasDifferent = different diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ea75c656bcc..9e5204bfeeb 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -111,15 +111,25 @@ class @Notes Note: for rendering inline notes use renderDiscussionNote ### renderNote: (note) -> + unless note.valid + if note.award + flash = new Flash('You have already used this award emoji!', 'alert') + flash.pinTo('.header-content') + return + # render note if it not present in loaded list # or skip if rendered - if @isNewNote(note) + if @isNewNote(note) && !note.award @note_ids.push(note.id) $('ul.main-notes-list'). append(note.html). syntaxHighlight() @initTaskList() + if note.award + awards_handler.addAwardToEmojiBar(note.note) + awards_handler.scrollToAwards() + ### Check if note does not exists on page ### @@ -138,6 +148,8 @@ class @Notes @note_ids.push(note.id) form = $("form[rel='" + note.discussion_id + "']") row = form.closest("tr") + note_html = $(note.html) + note_html.syntaxHighlight() # is this the first note of discussion? if row.is(".js-temp-notes-holder") @@ -148,14 +160,16 @@ class @Notes row.next().find(".note").remove() # Add note to 'Changes' page discussions - $(".notes[rel='" + note.discussion_id + "']").append note.html + $(".notes[rel='" + note.discussion_id + "']").append note_html # Init discussion on 'Discussion' page if it is merge request page if $('body').attr('data-page').indexOf('projects:merge_request') == 0 - $('ul.main-notes-list').append(note.discussion_with_diff_html) + discussion_html = $(note.discussion_with_diff_html) + discussion_html.syntaxHighlight() + $('ul.main-notes-list').append(discussion_html) else # append new note to all matching discussions - $(".notes[rel='" + note.discussion_id + "']").append note.html + $(".notes[rel='" + note.discussion_id + "']").append note_html # cleanup after successfully creating a diff/discussion note @removeDiscussionNoteForm(form) @@ -255,7 +269,6 @@ class @Notes ### addNote: (xhr, note, status) => @renderNote(note) - @updateVotes() ### Called in response to the new note form being submitted @@ -277,7 +290,7 @@ class @Notes $html.find('.js-task-list-container').taskList('enable') # Find the note's `li` element by ID and replace it with the updated HTML - $note_li = $("#note_#{note.id}") + $note_li = $('.note-row-' + note.id) $note_li.replaceWith($html) ### @@ -337,18 +350,26 @@ class @Notes ### removeNote: -> note = $(this).closest(".note") - notes = note.closest(".notes") + note_id = note.attr('id') - # check if this is the last note for this line - if notes.find(".note").length is 1 + $('.note[id="' + note_id + '"]').each -> + note = $(this) + notes = note.closest(".notes") + count = notes.closest(".notes_holder").find(".discussion-notes-count") - # for discussions - notes.closest(".discussion").remove() + # check if this is the last note for this line + if notes.find(".note").length is 1 - # for diff lines - notes.closest("tr").remove() + # for discussions + notes.closest(".discussion").remove() + + # for diff lines + notes.closest("tr").remove() + else + # update notes count + count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}" - note.remove() + note.remove() ### Called in response to clicking the delete attachment link @@ -360,8 +381,8 @@ class @Notes note = $(this).closest(".note") note.find(".note-attachment").remove() note.find(".note-body > .note-text").show() - note.find(".js-note-attachment-delete").hide() - note.find(".note-edit-form").hide() + note.find(".note-header").show() + note.find(".current-note-edit-form").remove() ### Called when clicking on the "reply" button for a diff line. @@ -473,9 +494,6 @@ class @Notes form = $(e.target).closest(".js-discussion-note-form") @removeDiscussionNoteForm(form) - updateVotes: -> - true - ### Called after an attachment file has been selected. diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 0ea8fffce07..d7a658f8faa 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -1,13 +1,23 @@ class @Project constructor: -> - # Git clone panel switcher - cloneHolder = $('.git-clone-holder') - if cloneHolder.length - $('a, button', cloneHolder).click -> - $('a, button', cloneHolder).removeClass 'active' - $(@).addClass 'active' - $('#project_clone', cloneHolder).val $(@).data 'clone' - $(".clone").text("").append $(@).data 'clone' + # Git protocol switcher + $('ul.clone-options-dropdown a').click -> + return if $(@).hasClass('active') + + + # Remove the active class for all buttons (ssh, http, kerberos if shown) + $('.active').not($(@)).removeClass('active'); + # Add the active class for the clicked button + $(@).toggleClass('active') + + url = $("#project_clone").val() + console.log("url",url) + + # Update the input field + $('#project_clone').val(url) + + # Update the command line instructions + $('.clone').text(url) # Ref switcher $('.project-refs-select').on 'change', -> @@ -39,4 +49,4 @@ class @Project when 4 then label = ' On Mention ' $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>") $(@).parents('ul').find('li.active').removeClass 'active' - $(@).parent().addClass 'active'
\ No newline at end of file + $(@).parent().addClass 'active' diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee new file mode 100644 index 00000000000..0ae274f3363 --- /dev/null +++ b/app/assets/javascripts/project_select.js.coffee @@ -0,0 +1,39 @@ +class @ProjectSelect + constructor: -> + $('.ajax-project-select').each (i, select) -> + @groupId = $(select).data('group-id') + @includeGroups = $(select).data('include-groups') + + placeholder = "Search for project" + placeholder += " or group" if @includeGroups + + $(select).select2 + placeholder: placeholder + minimumInputLength: 0 + query: (query) => + finalCallback = (projects) -> + data = { results: projects } + query.callback(data) + + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups query.term, false, groupsCallback + else + projectsCallback = finalCallback + + if @groupId + Api.groupProjects @groupId, query.term, projectsCallback + else + Api.projects query.term, projectsCallback + + id: (project) -> + project.web_url + + text: (project) -> + project.name_with_namespace || project.name + + dropdownCssClass: "ajax-project-dropdown" diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index db5faf71faf..f2887af190b 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -8,17 +8,17 @@ class @ProjectsList $(".projects-list-filter").keyup -> terms = $(this).val() - uiBox = $(this).closest('.projects-list-holder') + uiBox = $('div.projects-list-holder') if terms == "" || terms == undefined - uiBox.find(".projects-list li").show() + uiBox.find("ul.projects-list li").show() else - uiBox.find(".projects-list li").each (index) -> - name = $(this).find(".filter-title").text() + uiBox.find("ul.projects-list li").each (index) -> + name = $(this).find("span.filter-title").text() if name.toLowerCase().search(terms.toLowerCase()) == -1 $(this).hide() else $(this).show() - uiBox.find(".projects-list li.bottom").hide() + uiBox.find("ul.projects-list li.bottom").hide() diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index e9aeb1e9525..4d915bfc8c5 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -7,7 +7,7 @@ class @Shortcuts selectiveHelp: (e) => Shortcuts.showHelp(e, @enabledHelp) - + @showHelp: (e, location) -> if $('#modal-shortcuts').length > 0 $('#modal-shortcuts').modal('show') @@ -17,8 +17,7 @@ class @Shortcuts dataType: 'script', success: (e) -> if location and location.length > 0 - for l in location - $(l).show() + $(l).show() for l in location else $('.hidden-shortcut').show() $('.js-more-help-button').remove() @@ -28,3 +27,8 @@ class @Shortcuts @focusSearch: (e) -> $('#search').focus() e.preventDefault() + +$(document).on 'click.more_help', '.js-more-help-button', (e) -> + $(@).remove() + $('.hidden-shortcut').show() + e.preventDefault() diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index fb08016fbae..ae59480af9e 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -5,6 +5,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") + $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) ) diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee new file mode 100644 index 00000000000..d849b2e7950 --- /dev/null +++ b/app/assets/javascripts/star.js.coffee @@ -0,0 +1,22 @@ +class @Star + constructor: -> + $('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) -> + $this = $(this) + $starSpan = $this.find('span') + $starIcon = $this.find('i') + + toggleStar = (isStarred) -> + $this.parent().find('span.count').text data.star_count + if isStarred + $starSpan.removeClass('starred').text 'Star' + $starIcon.removeClass('fa-star').addClass 'fa-star-o' + else + $starSpan.addClass('starred').text 'Unstar' + $starIcon.removeClass('fa-star-o').addClass 'fa-star' + return + + toggleStar $starSpan.hasClass('starred') + return + ).on 'ajax:error', (e, xhr, status, error) -> + new Flash('Star toggle failed. Try again later.', 'alert') + return
\ No newline at end of file diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index cfe5508290f..f5584bcfe4b 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil = for entry in log @add_date(entry.date, total) unless total[entry.date]? - data = by_author[entry.author_name] #|| by_email[entry.author_email] + data = by_author[entry.author_name] || by_email[entry.author_email] data ?= @add_author(entry, by_author, by_email) @add_date(entry.date, data) unless data[entry.date] @@ -95,5 +95,4 @@ window.ContributorsStatGraphUtil = if date_range is null || date_range[0] <= new Date(date) <= date_range[1] true else - false - + false
\ No newline at end of file diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee index d0d81f96921..ec4271b092c 100644 --- a/app/assets/javascripts/user.js.coffee +++ b/app/assets/javascripts/user.js.coffee @@ -2,3 +2,9 @@ class @User constructor: -> $('.profile-groups-avatars').tooltip("placement": "top") new ProjectsList() + + $('.hide-project-limit-message').on 'click', (e) -> + path = '/' + $.cookie('hide_project_limit_message', 'false', { path: path }) + $(@).parents('.project-limit-message').remove() + e.preventDefault() diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 9157562a5c5..9467011799f 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -32,17 +32,15 @@ class @UsersSelect if showNullUser nullUser = { name: 'Unassigned', - avatar: null, - username: 'none', id: 0 } data.results.unshift(nullUser) if showAnyUser + name = showAnyUser + name = 'Any User' if name == true anyUser = { - name: 'Any', - avatar: null, - username: 'none', + name: name, id: null } data.results.unshift(anyUser) @@ -50,7 +48,6 @@ class @UsersSelect if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/) emailUser = { name: "Invite \"#{query.term}\"", - avatar: null, username: query.term, id: query.term } @@ -58,11 +55,8 @@ class @UsersSelect query.callback(data) - initSelection: (element, callback) => - id = $(element).val() - if id != "" && id != "0" - @user(id, callback) - + initSelection: (args...) => + @initSelection(args...) formatResult: (args...) => @formatResult(args...) formatSelection: (args...) => @@ -71,16 +65,24 @@ class @UsersSelect escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results m + initSelection: (element, callback) -> + id = $(element).val() + if id == "0" + nullUser = { name: 'Unassigned' } + callback(nullUser) + else if id != "" + @user(id, callback) + formatResult: (user) -> if user.avatar_url avatar = user.avatar_url else avatar = gon.default_avatar_url - "<div class='user-result'> + "<div class='user-result #{'no-username' unless user.username}'> <div class='user-image'><img class='avatar s24' src='#{avatar}'></div> <div class='user-name'>#{user.name}</div> - <div class='user-username'>#{user.username}</div> + <div class='user-username'>#{user.username || ""}</div> </div>" formatSelection: (user) -> @@ -115,5 +117,5 @@ class @UsersSelect callback(users) buildUrl: (url) -> - url = gon.relative_url_root + url if gon.relative_url_root? + url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root? return url diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 7b060ce4853..0c0451fe4dd 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,8 +2,8 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery.ui.datepicker - *= require jquery.ui.autocomplete + *= require jquery-ui/datepicker + *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self @@ -48,4 +48,4 @@ /* * Styles for JS behaviors. */ -@import "behaviors.scss";
\ No newline at end of file +@import "behaviors.scss"; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 1ec9d2fd84f..48a4971c8fc 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,9 +1,9 @@ @import "framework/fonts"; @import "framework/variables"; @import "framework/mixins"; -@import "framework/layout"; @import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap'; +@import "framework/layout"; @import "framework/avatar.scss"; @import "framework/blocks.scss"; @@ -25,6 +25,7 @@ @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; @import "framework/pagination.scss"; +@import "framework/panels.scss"; @import "framework/selects.scss"; @import "framework/sidebar.scss"; @import "framework/tables.scss"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 5949a0fd5ad..206d39cc9b3 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -28,6 +28,10 @@ border-bottom: 1px solid $border-color; color: $gl-gray; + &.oneline-block { + line-height: 42px; + } + &.white { background-color: white; } @@ -64,11 +68,15 @@ .oneline { line-height: 42px; } + + > p:last-child { + margin-bottom: 0; + } } .cover-block { text-align: center; - background: #f7f8fa; + background: $background-color; margin: -$gl-padding; margin-bottom: 0; padding: 44px $gl-padding; @@ -100,7 +108,7 @@ } .cover-desc { - padding: 0 $gl-padding; + padding: 0 $gl-padding 3px; color: $gl-text-color; } @@ -108,5 +116,14 @@ position: absolute; top: 10px; right: 10px; + + &.left { + left: 10px; + right: auto; + } } } + +.block-connector { + margin-top: -1px; +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e5f0c0ad9ef..97a94638847 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,10 +1,9 @@ @mixin btn-default { - @include border-radius(2px); + @include border-radius(3px); border-width: 1px; border-style: solid; - text-transform: uppercase; - font-size: 13px; - font-weight: 600; + font-size: 15px; + font-weight: 500; line-height: 18px; padding: 11px $gl-padding; letter-spacing: .4px; @@ -18,7 +17,7 @@ @mixin btn-middle { @include btn-default; - @include border-radius(2px); + @include border-radius(3px); padding: 11px 24px; } @@ -51,6 +50,10 @@ @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF); } +@mixin btn-blue-medium { + @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF); +} + @mixin btn-orange { @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF); } @@ -60,7 +63,7 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236); + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, #313236); } @mixin btn-white { @@ -75,6 +78,10 @@ padding: 5px 10px; } + &.btn-nr { + padding: 7px 10px; + } + &.btn-xs { padding: 1px 5px; } @@ -91,11 +98,15 @@ @include btn-gray; } - &.btn-primary, + &.btn-primary { + @include btn-blue-medium; + } + &.btn-info { @include btn-blue; } + &.btn-close, &.btn-warning { @include btn-orange; } @@ -110,20 +121,8 @@ float: right; } - &.btn-close { - color: $gl-danger; - border-color: $gl-danger; - &:hover { - color: #B94A48; - } - } - &.btn-reopen { - color: $gl-success; - border-color: $gl-success; - &:hover { - color: #468847; - } + /* should be same as parent class for now */ } &.btn-grouped { @@ -162,10 +161,25 @@ border-color: #e7e9ed; width: 140px; + .badge { + font-weight: normal; + background-color: #eee; + color: #78a; + } + &.active { border-color: $gl-info; background: $gl-info; color: #fff; + + .badge { + color: $gl-info; + background-color: white; + } } } } + +.btn-clipboard { + border: none; +} diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index a36fefe22c5..580012abd77 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -19,38 +19,33 @@ } } } + /** * This overwrites the default values of the cal-heatmap gem */ .calendar { .qi { - background-color: #999; fill: #fff; } .q1 { - background-color: #dae289; - fill: #ededed; + fill: #ededed !important; } .q2 { - background-color: #cedb9c; - fill: #ACD5F2; + fill: #ACD5F2 !important; } .q3 { - background-color: #b5cf6b; - fill: #7FA8D1; + fill: #7FA8D1 !important; } .q4 { - background-color: #637939; - fill: #49729B; + fill: #49729B !important; } .q5 { - background-color: #3b6427; - fill: #254E77; + fill: #254E77 !important; } .domain-background { @@ -59,32 +54,7 @@ } .ch-tooltip { - position: absolute; - display: none; - margin-top: 22px; - margin-left: 1px; - font-size: 13px; padding: 3px; font-weight: 550; - background-color: #222; - span { - position: absolute; - width: 200px; - text-align: center; - visibility: hidden; - border-radius: 10px; - &:after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - margin-left: -8px; - width: 0; - height: 0; - border-top: 8px solid #000000; - border-right: 8px solid transparent; - border-left: 8px solid transparent; - } - } } } diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index f1699d21c9b..20a9bfb9816 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -7,11 +7,11 @@ /* Common styles for all types */ .bs-callout { - margin: 20px 0; - padding: 20px; - border-left: 3px solid #eee; - color: #666; - background: #f9f9f9; + margin: $gl-padding 0; + padding: $gl-padding; + border-left: 3px solid $border-color; + color: $text-color; + background: $background-color; } .bs-callout h4 { margin-top: 0; @@ -42,4 +42,3 @@ border-color: #5cA64d; color: #3c763d; } - diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index e1a1793be9c..11730000f85 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -7,7 +7,7 @@ /** COMMON CLASSES **/ .prepend-top-10 { margin-top:10px } -.prepend-top-default { margin-top: $gl-padding; } +.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-20 { margin-top:20px } .prepend-left-10 { margin-left:10px } .prepend-left-20 { margin-left:20px } @@ -16,6 +16,7 @@ .append-bottom-10 { margin-bottom:10px } .append-bottom-15 { margin-bottom:15px } .append-bottom-20 { margin-bottom:20px } +.append-bottom-default { margin-bottom: $gl-padding; } .inline { display: inline-block } .center { text-align: center } @@ -51,6 +52,10 @@ pre { } } +hr { + margin: $gl-padding 0; +} + .dropdown-menu > li > a { text-shadow: none; } @@ -63,7 +68,7 @@ pre { .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { background: $gl-primary; - color: #FFF + color: #FFF; } .str-truncated { @@ -327,15 +332,15 @@ table { } } +.well { + margin-bottom: $gl-padding; +} + .search_box { @extend .well; text-align: center; } -.task-status { - margin-left: 10px; -} - #nprogress .spinner { top: 15px !important; right: 10px !important; @@ -369,14 +374,13 @@ table { } } -.center-top-menu { +.center-top-menu, .left-top-menu { @include nav-menu; text-align: center; margin-top: 5px; margin-bottom: $gl-padding; - height: 56px; + height: auto; margin-top: -$gl-padding; - padding-top: $gl-padding; &.no-bottom { margin-bottom: 0; @@ -385,6 +389,58 @@ table { &.no-top { margin-top: 0; } + + li a { + display: inline-block; + padding-top: $gl-padding; + padding-bottom: 11px; + margin-bottom: -1px; + } + + &.bottom-border { + border-bottom: 1px solid $border-color; + height: 57px; + } + + &.wide { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + } +} + +.left-top-menu { + text-align: left; + border-bottom: 1px solid #EEE; +} + +.center-middle-menu { + @include nav-menu; + padding: 0; + text-align: center; + margin: -$gl-padding; + margin-top: 0; + margin-bottom: 0; + height: 58px; + border-bottom: 1px solid $border-color; + + li { + &:after { + content: "|"; + color: $border-gray-light; + } + + &:last-child { + &:after { + content: none; + } + } + + > a { + display: inline-block; + text-transform: uppercase; + font-size: 13px; + } + } } .dropzone .dz-preview .dz-progress { @@ -398,3 +454,26 @@ table { .space-right { margin-right: 10px; } + +.alert, .progress { + margin-bottom: $gl-padding; +} + +.new-project-item-select-holder { + display: inline-block; + position: relative; + + .new-project-item-select { + position: absolute; + top: 0; + right: 0; + width: 250px !important; + visibility: hidden; + } +} + +.content-separator { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + border-top: 1px solid $border-color; +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 35db00281e5..cbfd4bc29b6 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -8,7 +8,6 @@ border: none; border-top: 1px solid #E7E9EE; border-bottom: 1px solid #E7E9EE; - margin-bottom: 1em; &.readme-holder { border-bottom: 0; @@ -22,10 +21,9 @@ position: relative; background: $background-color; border-bottom: 1px solid $border-color; - text-shadow: 0 1px 1px #fff; margin: 0; text-align: left; - padding: 10px 15px; + padding: 10px $gl-padding; .file-actions { float: right; @@ -171,4 +169,3 @@ } } } - diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 0edfe24f195..032d343df44 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -22,9 +22,10 @@ input[type='text'].danger { } .form-actions { - padding: 17px 20px 18px; - margin-top: 18px; - margin-bottom: 18px; + margin: -$gl-padding; + margin-top: 0; + margin-bottom: -$gl-padding; + padding: $gl-padding; background-color: $background-color; border-top: 1px solid $border-color; } @@ -73,6 +74,8 @@ label { .form-control { @include box-shadow(none); + height: 42px; + padding: 8px $gl-padding; } .wiki-content { @@ -88,7 +91,19 @@ label { } .input-group { + .select2-container { + display: table-cell; + width: 200px !important; + } .input-group-addon { background-color: #f7f8fa; } + .input-group-addon:not(:first-child):not(:last-child) { + border-left: 0; + border-right: 0; + } +} + +.help-block { + margin-bottom: 0; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 91e6975e269..4dbbb56104b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -6,15 +6,17 @@ header { transition-duration: .3s; &.navbar-empty { + height: 58px; background: #FFF; border-bottom: 1px solid #EEE; .center-logo { - margin: 8px 0; + margin: 11px 0; text-align: center; - img { - height: 32px; + #tanuki-logo, img { + width: 36px; + height: 36px; } } } @@ -118,6 +120,10 @@ header { } } } + + .impersonation i { + color: $red-normal; + } } @mixin collapsed-header { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 93377e45e70..e93dbab0c42 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -4,31 +4,32 @@ * */ -.issue-box { - @include border-radius(2px); +.status-box { + @include border-radius(3px); - display: inline-block; - padding: 10px $gl-padding; + display: block; + float: left; + padding: 0 $gl-padding; font-weight: normal; margin-right: 10px; font-size: $gl-font-size; - &.issue-box-closed { + &.status-box-closed { background-color: $gl-danger; color: #FFF; } - &.issue-box-merged { + &.status-box-merged { background-color: $gl-primary; color: #FFF; } - &.issue-box-open { - background-color: #019875; + &.status-box-open { + background-color: $green-light; color: #FFF; } - &.issue-box-expired { + &.status-box-expired { background: #cea61b; color: #FFF; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index c7b3b60e769..a1a9990241d 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -2,10 +2,13 @@ html { overflow-y: scroll; &.touch .tooltip { display: none !important; } +} + +body { + background-color: #F3F3F3 !important; - body { - padding-top: $header-height; - text-rendering: geometricPrecision; + &.navless { + background-color: white !important; } } @@ -19,7 +22,8 @@ html { } .navless-container { - margin-top: 30px; + margin-top: $header-height; + padding-top: $gl-padding * 2; } .container-limited { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index f6942db5816..1c74e525a60 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -7,7 +7,7 @@ padding: 0; list-style: none; - li { + > li { padding: 10px 15px; min-height: 20px; border-bottom: 1px solid #eee; @@ -72,13 +72,6 @@ } } -ol, ul { - &.styled { - li { - padding: 2px; - } - } -} /** light list with border-bottom between li **/ ul.bordered-list { @@ -95,8 +88,14 @@ ul.bordered-list { } } -li.task-list-item { - list-style-type: none; +ul.task-list { + li.task-list-item { + list-style-type: none; + } + + ul:not(.task-list) { + padding-left: 1.3em; + } } ul.content-list { @@ -117,7 +116,7 @@ ul.content-list { } .controls { - padding-top: 4px; + padding-top: 1px; float: right; .btn { @@ -127,3 +126,36 @@ ul.content-list { } } +.panel > .content-list { + li { + margin: 0; + } +} + +ul.controls { + padding-top: 1px; + float: right; + list-style: none; + + .btn { + padding: 10px 14px; + } + + > li { + float: left; + margin-right: 10px; + + &:last-child { + margin-right: 0; + } + + .author_link { + display: inline-block; + + .avatar-inline { + margin-left: 0; + margin-right: 0; + } + } + } +} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index ed0333d2336..4a00a197d9a 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -73,11 +73,8 @@ } .referenced-users { - padding: 10px 0; - color: #999; - margin-left: 10px; - margin-top: 1px; - margin-right: 130px; + color: #4c4e54; + padding-top: 10px; } .md-preview-holder { @@ -90,7 +87,7 @@ .new_note, .edit_note, -.issuable-description, +.detail-page-description, .milestone-description, .wiki-content, .merge-request-form { @@ -106,6 +103,7 @@ } .markdown-area { + @include border-radius(0); background: #FFF; border: 1px solid #ddd; min-height: 140px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 089e6958eeb..41fd890f14f 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -72,9 +72,10 @@ list-style: none; > li { + @include clearfix; + padding: 10px 0; border-bottom: 1px solid #EEE; - overflow: hidden; display: block; margin: 0px; @@ -122,7 +123,6 @@ padding: 0; margin: 0; list-style: none; - margin-top: 5px; height: 56px; li { @@ -130,31 +130,26 @@ a { padding: 14px; - font-size: 17px; + font-size: 15px; line-height: 28px; - color: #7f8fa4; + color: #959494; border-bottom: 2px solid transparent; &:hover, &:active, &:focus { text-decoration: none; + outline: none; } } &.active a { - color: #4c4e54; - border-bottom: 2px solid #1cacfc; + color: #616060; + border-bottom: 2px solid #4688f1; } .badge { font-weight: normal; - background-color: #fff; background-color: #eee; color: #78a; } } } - -.fa-align { - top: 20px; - position: relative; -} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index cea47fba192..c00709fb6bb 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -81,10 +81,7 @@ display: none; } - .center-top-menu { - height: 45px; - margin-bottom: 30px; - + .center-top-menu, .left-top-menu { li a { font-size: 14px; padding: 19px 10px; diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index 6677f94dafd..2cd30491bf5 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -32,3 +32,7 @@ } } } + +.panel > .gl-pagination { + margin: 0; +} diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss new file mode 100644 index 00000000000..57b9451b264 --- /dev/null +++ b/app/assets/stylesheets/framework/panels.scss @@ -0,0 +1,20 @@ +.panel { + margin-bottom: $gl-padding; + + .panel-heading { + padding: 7px $gl-padding; + } + + .panel-body { + padding: $gl-padding; + + .form-actions { + margin: -$gl-padding; + margin-top: $gl-padding; + } + } +} + +.container-blank .panel .panel-heading { + line-height: 42px !important; +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 78fff58d232..af145191bc8 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -15,6 +15,16 @@ border-left: none; padding-top: 5px; } + + .select2-chosen { + color: $gl-text-color; + } + + &.select2-default { + .select2-chosen { + color: #999; + } + } } } @@ -23,6 +33,7 @@ border: 1px solid #e7e9ed; } + .select2-drop { @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); @include border-radius (0px); @@ -48,17 +59,38 @@ color: #313236; } +.select2-container-multi { + .select2-choices { + @include border-radius(2px); + border-color: $input-border; + background: white; + padding-left: $gl-padding / 2; + + .select2-search-field input { + padding: $gl-padding / 2; + font-size: 13px; + height: auto; + font-family: inherit; + font-size: inherit; + } -.select2-container-multi .select2-choices { - @include border-radius(2px); - border-color: #CCC; -} - -.select2-container-multi .select2-choices .select2-search-field input { - padding: 8px 14px; - font-size: 13px; - line-height: 18px; - height: auto; + .select2-search-choice { + margin: 8px 0 0 8px; + background: white; + box-shadow: none; + border-color: $input-border; + color: $gl-text-color; + line-height: 15px; + + .select2-search-choice-close { + top: 5px; + } + + &.select2-search-choice-focus { + border-color: $gl-text-color; + } + } + } } .select2-drop-active { @@ -123,10 +155,16 @@ } .user-result { + min-height: 24px; + .user-image { float: left; } - .user-name { + + &.no-username { + .user-name { + line-height: 24px; + } } } @@ -143,4 +181,4 @@ .ajax-users-dropdown { min-width: 250px !important; -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 985ea164576..83243dd2457 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,4 +1,7 @@ .page-with-sidebar { + padding-top: $header-height; + transition-duration: .3s; + .sidebar-wrapper { position: fixed; top: 0; @@ -14,19 +17,15 @@ .sidebar-wrapper { z-index: 99; background: $background-color; - transition-duration: .3s; } .content-wrapper { - min-height: 100vh; width: 100%; padding: 20px; - background: #EAEBEC; .container-fluid { background: #FFF; padding: $gl-padding; - min-height: 90vh; &.container-blank { background: none; @@ -36,6 +35,83 @@ } } +.sidebar-wrapper { + .header-logo { + border-bottom: 1px solid transparent; + float: left; + height: $header-height; + width: $sidebar_width; + position: fixed; + z-index: 999; + overflow: hidden; + transition-duration: .3s; + + a { + float: left; + height: $header-height; + width: 100%; + padding: 11px 0 11px 22px; + overflow: hidden; + outline: none; + transition-duration: .3s; + + img { + width: 36px; + height: 36px; + } + + #tanuki-logo, img { + float: left; + } + + .gitlab-text-container { + width: 230px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 14px; + font-size: 19px; + line-height: 41px; + font-weight: normal; + } + } + } + + &:hover { + background-color: #EEE; + } + } + + .sidebar-user { + padding: 9px 22px; + position: fixed; + bottom: 40px; + width: $sidebar_width; + overflow: hidden; + transition-duration: .3s; + + .username { + margin-left: 10px; + width: $sidebar_width - 2 * 10px; + font-size: 16px; + line-height: 34px; + } + } +} + + +.tanuki-shape { + transition: all 0.8s; + + &:hover, &.highlight { + fill: rgb(255, 255, 255); + transition: all 0.1s; + } +} + + .nav-sidebar { margin-top: 14 + $header-height; margin-bottom: 100px; @@ -62,8 +138,9 @@ color: $gray; display: block; text-decoration: none; - padding-left: 22px; + padding-left: 23px; font-weight: normal; + outline: none; &:hover { text-decoration: none; @@ -85,6 +162,10 @@ padding: 0px 8px; @include border-radius(6px); } + + &.back-link i { + transition-duration: .3s; + } } } } @@ -100,7 +181,6 @@ @mixin expanded-sidebar { padding-left: $sidebar_width; - transition-duration: .3s; .sidebar-wrapper { width: $sidebar_width; @@ -114,16 +194,15 @@ &.back-link { i { - visibility: hidden; + opacity: 0; } } } } } -@mixin folded-sidebar { - padding-left: 60px; - transition-duration: .3s; +@mixin collapsed-sidebar { + padding-left: $sidebar_collapsed_width; .sidebar-wrapper { width: $sidebar_collapsed_width; @@ -132,7 +211,7 @@ width: $sidebar_collapsed_width; a { - padding-left: 12px; + padding-left: ($sidebar_collapsed_width - 36) / 2; .gitlab-text-container { display: none; @@ -143,9 +222,13 @@ .nav-sidebar { width: $sidebar_collapsed_width; - li a { - span { - display: none; + li { + width: auto; + + a { + span { + display: none; + } } } } @@ -155,7 +238,7 @@ } .sidebar-user { - padding-left: 12px; + padding-left: ($sidebar_collapsed_width - 36) / 2; width: $sidebar_collapsed_width; .username { @@ -176,6 +259,7 @@ text-align: center; line-height: 40px; transition-duration: .3s; + outline: none; } .collapse-nav a:hover { @@ -185,11 +269,11 @@ @media (max-width: $screen-md-max) { .page-sidebar-collapsed { - @include folded-sidebar; + @include collapsed-sidebar; } .page-sidebar-expanded { - @include folded-sidebar; + @include collapsed-sidebar; } .collapse-nav { @@ -199,82 +283,10 @@ @media(min-width: $screen-md-max) { .page-sidebar-collapsed { - @include folded-sidebar; + @include collapsed-sidebar; } .page-sidebar-expanded { @include expanded-sidebar; } } - -.sidebar-user { - padding: 9px 22px; - position: fixed; - bottom: 40px; - width: $sidebar_width; - overflow: hidden; - transition-duration: .3s; - - .username { - margin-left: 10px; - width: $sidebar_width - 2 * 10px; - font-size: 16px; - line-height: 34px; - } -} - -.sidebar-wrapper { - .header-logo { - border-bottom: 1px solid transparent; - float: left; - height: $header-height; - width: $sidebar_width; - overflow: hidden; - transition-duration: .3s; - - a { - float: left; - height: $header-height; - width: 100%; - padding: 10px 22px; - overflow: hidden; - - img { - width: 36px; - height: 36px; - } - - #tanuki-logo, img { - float: left; - } - - .gitlab-text-container { - width: 230px; - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 14px; - font-size: 19px; - line-height: 41px; - font-weight: normal; - } - } - } - - &:hover { - background-color: #EEE; - } - } -} - - -.tanuki-shape { - transition: all 0.8s; - - &:hover { - fill: rgb(255, 255, 255); - transition: all 0.1s; - } -} diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 66e16e8df75..793ab3d9bb9 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -6,6 +6,8 @@ table { &.table { + margin-bottom: $gl-padding; + .dropdown-menu a { text-decoration: none; } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index eb53c4153d3..ff41e26ed8a 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -10,8 +10,7 @@ margin-left: -$gl-padding; margin-right: -$gl-padding; color: $gl-gray; - border-bottom: 1px solid #ECEEF1; - border-right: 1px solid #ECEEF1; + border-bottom: 1px solid $border-white-light; &:target { background: $hover; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 99d028d1228..94f0ed761df 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -172,7 +172,7 @@ } .panel-body { - form { + form, pre { margin: 0; } @@ -190,6 +190,10 @@ .btn { min-width: 124px; } + + .btn-clipboard { + min-width: 0px; + } } &.panel-small { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index e6558a23858..714369d9f15 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -54,17 +54,17 @@ h3 { margin: 24px 0 12px 0; - font-size: 1.25em; + font-size: 1.1em; } h4 { margin: 24px 0 12px 0; - font-size: 1.1em; + font-size: 0.98em; } h5 { margin: 24px 0 12px 0; - font-size: 1em; + font-size: 0.95em; } h6 { @@ -173,7 +173,6 @@ * */ body { - text-rendering:optimizeLegibility; -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px; } @@ -182,6 +181,10 @@ body { line-height: 1.3; font-size: 1.25em; font-weight: 600; + + &:last-child { + margin-bottom: 0; + } } .page-title-empty { @@ -217,6 +220,7 @@ pre { .monospace { font-family: $monospace_font; + font-size: 90%; } code { @@ -257,3 +261,9 @@ textarea.js-gfm-input { .strikethrough { text-decoration: line-through; } + +h1, h2, h3, h4 { + small { + color: $gl-gray; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 91954683c3e..af75123b0af 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,9 +1,9 @@ -$hover: #FFFAF1; +$hover: #faf9f9; $gl-text-color: #54565B; $gl-text-green: #4A2; $gl-text-red: #D12F19; $gl-text-orange: #D90; -$gl-header-color: #4c4e54; +$gl-header-color: #323232; $gl-link-color: #333c48; $md-text-color: #444; $md-link-color: #3084bb; @@ -15,13 +15,14 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; -$border-color: #dce0e6; +$border-color: #efeff1; $table-border-color: #eef0f2; -$background-color: #F7F8FA; +$background-color: #faf9f9; $header-height: 58px; -$fixed-layout-width: 1200px; -$gl-gray: #7f8fa4; +$fixed-layout-width: 1280px; +$gl-gray: #5a5a5a; $gl-padding: 16px; +$gl-padding-top:10px; $gl-avatar-size: 46px; /* @@ -29,12 +30,12 @@ $gl-avatar-size: 46px; */ $white-light: #FFFFFF; -$white-normal: #DCE0E5; -$white-dark: #E4E7ED; +$white-normal: #ededed; +$white-dark: #ededed; -$gray-light: #F0F2F5; -$gray-normal: #DCE0E5; -$gray-dark: #E4E7ED; +$gray-light: #f7f7f7; +$gray-normal: #ededed; +$gray-dark: #ededed; $green-light: #31AF64; $green-normal: #2FAA60; @@ -44,6 +45,10 @@ $blue-light: #2EA8E5; $blue-normal: #2D9FD8; $blue-dark: #2897CE; +$blue-medium-light: #3498CB; +$blue-medium: #2F8EBF; +$blue-medium-dark: #2D86B4; + $orange-light: #FC6443; $orange-normal: #E75E40; $orange-dark: #CE5237; @@ -52,11 +57,11 @@ $red-light: #F43263; $red-normal: #E52C5A; $red-dark: #D22852; -$border-white-light: #E3E7EC; +$border-white-light: #F1F2F4; $border-white-normal: #D6DAE2; $border-white-dark: #C6CACF; -$border-gray-light: #DCE0E5; +$border-gray-light: #d1d1d1; $border-gray-normal: #D6DAE2; $border-gray-dark: #C6CACF; @@ -76,6 +81,8 @@ $border-red-light: #E52C5A; $border-red-normal: #D22852; $border-red-dark: #CA264F; +/* header */ +$light-grey-header: #faf9f9; /* * State colors: diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss new file mode 100644 index 00000000000..87dd30f4111 --- /dev/null +++ b/app/assets/stylesheets/pages/awards.scss @@ -0,0 +1,125 @@ +.awards { + @include clearfix; + line-height: 34px; + + .emoji-icon { + width: 20px; + height: 20px; + margin: 7px 0 0 5px; + } + + .award { + @include border-radius(5px); + + border: 1px solid; + padding: 0px 10px; + float: left; + margin-right: 5px; + border-color: $border-color; + cursor: pointer; + + &:hover { + background-color: #dce0e5; + } + + &.active { + border-color: $border-gray-light; + background-color: $gray-light; + + &:hover { + background-color: #dce0e5; + } + + .counter { + font-weight: bold; + } + } + + .icon { + float: left; + margin-right: 10px; + } + + .counter { + float: left; + } + } + + .awards-controls { + position: relative; + margin-left: 10px; + float: left; + + .add-award { + font-size: 24px; + color: $gl-gray; + position: relative; + top: 2px; + + &:hover, + &:link { + text-decoration: none; + } + } + + .emoji-menu{ + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0,0,0,.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px rgba(0,0,0,.175); + + .emoji-menu-content { + padding: $gl-padding; + width: 300px; + height: 300px; + overflow-y: scroll; + + h5 { + clear: left; + } + + ul { + list-style-type: none; + margin-left: -20px; + margin-bottom: 20px; + overflow: auto; + } + + input.emoji-search{ + background: image-url("icon-search.png") 240px no-repeat; + } + + li { + cursor: pointer; + width: 30px; + height: 30px; + text-align: center; + float: left; + margin: 3px; + list-decorate: none; + @include border-radius(5px); + + &:hover { + background-color: #ccc; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 74dc3e321c1..3c2997c1d5a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -21,7 +21,7 @@ .autoscroll-container { position: fixed; - bottom: 10px; + bottom: 20px; right: 20px; z-index: 100; } @@ -34,7 +34,7 @@ a { display: block; - margin-bottom: 5px; + margin-bottom: 10px; } } @@ -67,9 +67,4 @@ color: #3084bb !important; } } - - .build-top-menu { - margin-top: 0; - margin-bottom: 2px; - } } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index fbd7c363de1..17245d3be7b 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -2,10 +2,6 @@ display: block; } -.commit-title{ - margin-bottom: 10px; -} - .commit-author, .commit-committer{ display: block; color: #999; @@ -41,6 +37,8 @@ .commit-box { .commit-title { margin: 0; + font-size: 23px; + color: #313236; } .commit-description { @@ -56,6 +54,7 @@ li { padding: 3px 0px; + line-height: 20px; } } .new-file { @@ -107,16 +106,3 @@ z-index: 2; } } - -.commit-ci-menu { - padding: 0; - margin: 0; - list-style: none; - margin-top: 5px; - height: 56px; - margin: -16px; - padding: 16px; - text-align: center; - margin-top: 0px; - margin-bottom: 2px; -} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 4e121b95d13..879bd287470 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -33,6 +33,8 @@ } li.commit { + list-style: none; + .commit-row-title { font-size: $list-font-size; line-height: 20px; @@ -113,3 +115,66 @@ li.commit { } } } + +.branch-commit { + color: $gl-gray; + .commit-id, .commit-row-message { + color: $gl-gray; + } +} + +.divergence-graph { + padding: 12px 12px 0 0; + float: right; + + .graph-side { + position: relative; + width: 80px; + height: 22px; + padding: 5px 0 13px; + float: left; + + .bar { + position: absolute; + height: 4px; + background-color: #ccc; + } + + .bar-behind { + right: 0; + border-radius: 3px 0 0 3px; + } + + .bar-ahead { + left: 0; + border-radius: 0 3px 3px 0; + } + + .count { + padding-top: 6px; + padding-bottom: 0px; + font-size: 12px; + color: #333; + display: block; + } + + .count-behind { + padding-right: 4px; + text-align: right; + } + + .count-ahead { + padding-left: 4px; + text-align: left; + } + } + + .graph-separator { + position: relative; + width: 1px; + height: 18px; + margin: 5px 0 0; + float: left; + background-color: #ccc; + } +} diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss new file mode 100644 index 00000000000..deab805dbc2 --- /dev/null +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -0,0 +1,33 @@ +.detail-page-header { + margin: -$gl-padding; + padding: 7px $gl-padding; + margin-bottom: 0px; + border-bottom: 1px solid $border-color; + color: #5c5d5e; + font-size: 16px; + line-height: 34px; + + .author { + color: #5c5d5e; + } + + .identifier { + color: #5c5d5e; + } +} + +.detail-page-description { + .title { + margin: 0; + font-size: 23px; + color: #313236; + } + + .description { + margin-top: 6px; + + p:last-child { + margin-bottom: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d9ef06dc6b6..afd6fb73675 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -367,7 +367,6 @@ .inline-parallel-buttons { float: right; - margin-top: -5px; } // Mobile diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 1d565477dd4..39d916cd336 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -19,48 +19,38 @@ color: #B94A48; } } - .commit-button-annotation { - display: inline-block; - margin: 0; - padding: 2px; - - > * { - float: left; - } - - .message { - display: inline-block; - margin: 5px 8px 0 8px; - } - } .file-title { @extend .monospace; + + line-height: 42px; + padding-top: 7px; + padding-bottom: 7px; } .editor-ref { background: $background-color; - padding: 11px 15px; + padding-right: $gl-padding; border-right: 1px solid $border-color; - display: inline-block; - margin: -5px -5px; + display: block; + float: left; margin-right: 10px; } .editor-file-name { - .new-file-name { - display: inline-block; - width: 200px; - } + @extend .monospace; + + float: left; + margin-right: 10px; + } - .form-control { - margin-top: -3px; - } + .new-file-name { + display: inline-block; + width: 450px; + float: left; } - .form-actions { - margin: -$gl-padding; - margin-top: 0; - padding: $gl-padding + .select2 { + float: right; } } diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss new file mode 100644 index 00000000000..89a94c5a780 --- /dev/null +++ b/app/assets/stylesheets/pages/emojis.scss @@ -0,0 +1,1272 @@ +/* +File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy +The source: gemojione gem. +*/ + +.emoji-icon{ + background-image: image-url("emoji.png"); + background-repeat: no-repeat; +} + +.emoji-0023-20E3 { background-position: 0px 0px; } +.emoji-0030-20E3 { background-position: -20px 0px; } +.emoji-0031-20E3 { background-position: -40px 0px; } +.emoji-0032-20E3 { background-position: -60px 0px; } +.emoji-0033-20E3 { background-position: -80px 0px; } +.emoji-0034-20E3 { background-position: -100px 0px; } +.emoji-0035-20E3 { background-position: -120px 0px; } +.emoji-0036-20E3 { background-position: -140px 0px; } +.emoji-0037-20E3 { background-position: -160px 0px; } +.emoji-0038-20E3 { background-position: -180px 0px; } +.emoji-0039-20E3 { background-position: -200px 0px; } +.emoji-00A9 { background-position: -220px 0px; } +.emoji-00AE { background-position: -240px 0px; } +.emoji-1F004 { background-position: -260px 0px; } +.emoji-1F0CF { background-position: -280px 0px; } +.emoji-1F170 { background-position: -300px 0px; } +.emoji-1F171 { background-position: -320px 0px; } +.emoji-1F17E { background-position: -340px 0px; } +.emoji-1F17F { background-position: -360px 0px; } +.emoji-1F18E { background-position: -380px 0px; } +.emoji-1F191 { background-position: -400px 0px; } +.emoji-1F192 { background-position: -420px 0px; } +.emoji-1F193 { background-position: -440px 0px; } +.emoji-1F194 { background-position: -460px 0px; } +.emoji-1F195 { background-position: -480px 0px; } +.emoji-1F196 { background-position: -500px 0px; } +.emoji-1F197 { background-position: -520px 0px; } +.emoji-1F198 { background-position: -540px 0px; } +.emoji-1F199 { background-position: -560px 0px; } +.emoji-1F19A { background-position: -580px 0px; } +.emoji-1F1E6-1F1E8 { background-position: -600px 0px; } +.emoji-1F1E6-1F1E9 { background-position: -620px 0px; } +.emoji-1F1E6-1F1EA { background-position: -640px 0px; } +.emoji-1F1E6-1F1EB { background-position: -660px 0px; } +.emoji-1F1E6-1F1EC { background-position: -680px 0px; } +.emoji-1F1E6-1F1EE { background-position: -700px 0px; } +.emoji-1F1E6-1F1F1 { background-position: -720px 0px; } +.emoji-1F1E6-1F1F2 { background-position: -740px 0px; } +.emoji-1F1E6-1F1F4 { background-position: -760px 0px; } +.emoji-1F1E6-1F1F7 { background-position: -780px 0px; } +.emoji-1F1E6-1F1F9 { background-position: -800px 0px; } +.emoji-1F1E6-1F1FA { background-position: -820px 0px; } +.emoji-1F1E6-1F1FC { background-position: -840px 0px; } +.emoji-1F1E6-1F1FF { background-position: -860px 0px; } +.emoji-1F1E7-1F1E6 { background-position: -880px 0px; } +.emoji-1F1E7-1F1E7 { background-position: -900px 0px; } +.emoji-1F1E7-1F1E9 { background-position: -920px 0px; } +.emoji-1F1E7-1F1EA { background-position: -940px 0px; } +.emoji-1F1E7-1F1EB { background-position: -960px 0px; } +.emoji-1F1E7-1F1EC { background-position: -980px 0px; } +.emoji-1F1E7-1F1ED { background-position: -1000px 0px; } +.emoji-1F1E7-1F1EE { background-position: -1020px 0px; } +.emoji-1F1E7-1F1EF { background-position: -1040px 0px; } +.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; } +.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; } +.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; } +.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; } +.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; } +.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; } +.emoji-1F1E7-1F1FC { background-position: -1180px 0px; } +.emoji-1F1E7-1F1FE { background-position: -1200px 0px; } +.emoji-1F1E7-1F1FF { background-position: -1220px 0px; } +.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; } +.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; } +.emoji-1F1E8-1F1EB { background-position: -1280px 0px; } +.emoji-1F1E8-1F1EC { background-position: -1300px 0px; } +.emoji-1F1E8-1F1ED { background-position: -1320px 0px; } +.emoji-1F1E8-1F1EE { background-position: -1340px 0px; } +.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; } +.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; } +.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; } +.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; } +.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; } +.emoji-1F1E8-1F1FA { background-position: -1460px 0px; } +.emoji-1F1E8-1F1FB { background-position: -1480px 0px; } +.emoji-1F1E8-1F1FE { background-position: -1500px 0px; } +.emoji-1F1E8-1F1FF { background-position: -1520px 0px; } +.emoji-1F1E9-1F1EA { background-position: -1540px 0px; } +.emoji-1F1E9-1F1EF { background-position: -1560px 0px; } +.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; } +.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; } +.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; } +.emoji-1F1E9-1F1FF { background-position: -1640px 0px; } +.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; } +.emoji-1F1EA-1F1EA { background-position: -1680px 0px; } +.emoji-1F1EA-1F1EC { background-position: -1700px 0px; } +.emoji-1F1EA-1F1ED { background-position: -1720px 0px; } +.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; } +.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; } +.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; } +.emoji-1F1EB-1F1EE { background-position: -1800px 0px; } +.emoji-1F1EB-1F1EF { background-position: -1820px 0px; } +.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; } +.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; } +.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; } +.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; } +.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; } +.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; } +.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; } +.emoji-1F1EC-1F1EA { background-position: -1980px 0px; } +.emoji-1F1EC-1F1ED { background-position: -2000px 0px; } +.emoji-1F1EC-1F1EE { background-position: -2020px 0px; } +.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; } +.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; } +.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; } +.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; } +.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; } +.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; } +.emoji-1F1EC-1F1FA { background-position: -2160px 0px; } +.emoji-1F1EC-1F1FC { background-position: -2180px 0px; } +.emoji-1F1EC-1F1FE { background-position: -2200px 0px; } +.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; } +.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; } +.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; } +.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; } +.emoji-1F1ED-1F1FA { background-position: -2300px 0px; } +.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; } +.emoji-1F1EE-1F1EA { background-position: -2340px 0px; } +.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; } +.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; } +.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; } +.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; } +.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; } +.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; } +.emoji-1F1EF-1F1EA { background-position: -2480px 0px; } +.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; } +.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; } +.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; } +.emoji-1F1F0-1F1EA { background-position: -2560px 0px; } +.emoji-1F1F0-1F1EC { background-position: -2580px 0px; } +.emoji-1F1F0-1F1ED { background-position: -2600px 0px; } +.emoji-1F1F0-1F1EE { background-position: -2620px 0px; } +.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; } +.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; } +.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; } +.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; } +.emoji-1F1F0-1F1FC { background-position: -2720px 0px; } +.emoji-1F1F0-1F1FE { background-position: -2740px 0px; } +.emoji-1F1F0-1F1FF { background-position: -2760px 0px; } +.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; } +.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; } +.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; } +.emoji-1F1F1-1F1EE { background-position: -2840px 0px; } +.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; } +.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; } +.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; } +.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; } +.emoji-1F1F1-1F1FA { background-position: -2940px 0px; } +.emoji-1F1F1-1F1FB { background-position: -2960px 0px; } +.emoji-1F1F1-1F1FE { background-position: -2980px 0px; } +.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; } +.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; } +.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; } +.emoji-1F1F2-1F1EA { background-position: -3060px 0px; } +.emoji-1F1F2-1F1EC { background-position: -3080px 0px; } +.emoji-1F1F2-1F1ED { background-position: -3100px 0px; } +.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; } +.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; } +.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; } +.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; } +.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; } +.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; } +.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; } +.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; } +.emoji-1F1F2-1F1FA { background-position: -3280px 0px; } +.emoji-1F1F2-1F1FB { background-position: -3300px 0px; } +.emoji-1F1F2-1F1FC { background-position: -3320px 0px; } +.emoji-1F1F2-1F1FD { background-position: -3340px 0px; } +.emoji-1F1F2-1F1FE { background-position: -3360px 0px; } +.emoji-1F1F2-1F1FF { background-position: -3380px 0px; } +.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; } +.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; } +.emoji-1F1F3-1F1EA { background-position: -3440px 0px; } +.emoji-1F1F3-1F1EC { background-position: -3460px 0px; } +.emoji-1F1F3-1F1EE { background-position: -3480px 0px; } +.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; } +.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; } +.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; } +.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; } +.emoji-1F1F3-1F1FA { background-position: -3580px 0px; } +.emoji-1F1F3-1F1FF { background-position: -3600px 0px; } +.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; } +.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; } +.emoji-1F1F5-1F1EA { background-position: -3660px 0px; } +.emoji-1F1F5-1F1EB { background-position: -3680px 0px; } +.emoji-1F1F5-1F1EC { background-position: -3700px 0px; } +.emoji-1F1F5-1F1ED { background-position: -3720px 0px; } +.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; } +.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; } +.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; } +.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; } +.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; } +.emoji-1F1F5-1F1FC { background-position: -3840px 0px; } +.emoji-1F1F5-1F1FE { background-position: -3860px 0px; } +.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; } +.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; } +.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; } +.emoji-1F1F7-1F1FA { background-position: -3940px 0px; } +.emoji-1F1F7-1F1FC { background-position: -3960px 0px; } +.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; } +.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; } +.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; } +.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; } +.emoji-1F1F8-1F1EA { background-position: -4060px 0px; } +.emoji-1F1F8-1F1EC { background-position: -4080px 0px; } +.emoji-1F1F8-1F1ED { background-position: -4100px 0px; } +.emoji-1F1F8-1F1EE { background-position: -4120px 0px; } +.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; } +.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; } +.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; } +.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; } +.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; } +.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; } +.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; } +.emoji-1F1F8-1F1FB { background-position: -4280px 0px; } +.emoji-1F1F8-1F1FE { background-position: -4300px 0px; } +.emoji-1F1F8-1F1FF { background-position: -4320px 0px; } +.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; } +.emoji-1F1F9-1F1EC { background-position: -4360px 0px; } +.emoji-1F1F9-1F1ED { background-position: -4380px 0px; } +.emoji-1F1F9-1F1EF { background-position: -4400px 0px; } +.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; } +.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; } +.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; } +.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; } +.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; } +.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; } +.emoji-1F1F9-1F1FB { background-position: -4540px 0px; } +.emoji-1F1F9-1F1FC { background-position: -4560px 0px; } +.emoji-1F1F9-1F1FF { background-position: -4580px 0px; } +.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; } +.emoji-1F1FA-1F1EC { background-position: -4620px 0px; } +.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; } +.emoji-1F1FA-1F1FE { background-position: -4660px 0px; } +.emoji-1F1FA-1F1FF { background-position: -4680px 0px; } +.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; } +.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; } +.emoji-1F1FB-1F1EA { background-position: -4740px 0px; } +.emoji-1F1FB-1F1EE { background-position: -4760px 0px; } +.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; } +.emoji-1F1FB-1F1FA { background-position: -4800px 0px; } +.emoji-1F1FC-1F1EB { background-position: -4820px 0px; } +.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; } +.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; } +.emoji-1F1FE-1F1EA { background-position: -4880px 0px; } +.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; } +.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; } +.emoji-1F1FF-1F1FC { background-position: -4940px 0px; } +.emoji-1F201 { background-position: -4960px 0px; } +.emoji-1F202 { background-position: -4980px 0px; } +.emoji-1F21A { background-position: -5000px 0px; } +.emoji-1F22F { background-position: -5020px 0px; } +.emoji-1F232 { background-position: -5040px 0px; } +.emoji-1F233 { background-position: -5060px 0px; } +.emoji-1F234 { background-position: -5080px 0px; } +.emoji-1F235 { background-position: -5100px 0px; } +.emoji-1F236 { background-position: -5120px 0px; } +.emoji-1F237 { background-position: -5140px 0px; } +.emoji-1F238 { background-position: -5160px 0px; } +.emoji-1F239 { background-position: -5180px 0px; } +.emoji-1F23A { background-position: -5200px 0px; } +.emoji-1F250 { background-position: -5220px 0px; } +.emoji-1F251 { background-position: -5240px 0px; } +.emoji-1F300 { background-position: -5260px 0px; } +.emoji-1F301 { background-position: -5280px 0px; } +.emoji-1F302 { background-position: -5300px 0px; } +.emoji-1F303 { background-position: -5320px 0px; } +.emoji-1F304 { background-position: -5340px 0px; } +.emoji-1F305 { background-position: -5360px 0px; } +.emoji-1F306 { background-position: -5380px 0px; } +.emoji-1F307 { background-position: -5400px 0px; } +.emoji-1F308 { background-position: -5420px 0px; } +.emoji-1F309 { background-position: -5440px 0px; } +.emoji-1F30A { background-position: -5460px 0px; } +.emoji-1F30B { background-position: -5480px 0px; } +.emoji-1F30C { background-position: -5500px 0px; } +.emoji-1F30D { background-position: -5520px 0px; } +.emoji-1F30E { background-position: -5540px 0px; } +.emoji-1F30F { background-position: -5560px 0px; } +.emoji-1F310 { background-position: -5580px 0px; } +.emoji-1F311 { background-position: -5600px 0px; } +.emoji-1F312 { background-position: -5620px 0px; } +.emoji-1F313 { background-position: -5640px 0px; } +.emoji-1F314 { background-position: -5660px 0px; } +.emoji-1F315 { background-position: -5680px 0px; } +.emoji-1F316 { background-position: -5700px 0px; } +.emoji-1F317 { background-position: -5720px 0px; } +.emoji-1F318 { background-position: -5740px 0px; } +.emoji-1F319 { background-position: -5760px 0px; } +.emoji-1F31A { background-position: -5780px 0px; } +.emoji-1F31B { background-position: -5800px 0px; } +.emoji-1F31C { background-position: -5820px 0px; } +.emoji-1F31D { background-position: -5840px 0px; } +.emoji-1F31E { background-position: -5860px 0px; } +.emoji-1F31F { background-position: -5880px 0px; } +.emoji-1F320 { background-position: -5900px 0px; } +.emoji-1F321 { background-position: -5920px 0px; } +.emoji-1F327 { background-position: -5940px 0px; } +.emoji-1F328 { background-position: -5960px 0px; } +.emoji-1F329 { background-position: -5980px 0px; } +.emoji-1F32A { background-position: -6000px 0px; } +.emoji-1F32B { background-position: -6020px 0px; } +.emoji-1F32C { background-position: -6040px 0px; } +.emoji-1F330 { background-position: -6060px 0px; } +.emoji-1F331 { background-position: -6080px 0px; } +.emoji-1F332 { background-position: -6100px 0px; } +.emoji-1F333 { background-position: -6120px 0px; } +.emoji-1F334 { background-position: -6140px 0px; } +.emoji-1F335 { background-position: -6160px 0px; } +.emoji-1F336 { background-position: -6180px 0px; } +.emoji-1F337 { background-position: -6200px 0px; } +.emoji-1F338 { background-position: -6220px 0px; } +.emoji-1F339 { background-position: -6240px 0px; } +.emoji-1F33A { background-position: -6260px 0px; } +.emoji-1F33B { background-position: -6280px 0px; } +.emoji-1F33C { background-position: -6300px 0px; } +.emoji-1F33D { background-position: -6320px 0px; } +.emoji-1F33E { background-position: -6340px 0px; } +.emoji-1F33F { background-position: -6360px 0px; } +.emoji-1F340 { background-position: -6380px 0px; } +.emoji-1F341 { background-position: -6400px 0px; } +.emoji-1F342 { background-position: -6420px 0px; } +.emoji-1F343 { background-position: -6440px 0px; } +.emoji-1F344 { background-position: -6460px 0px; } +.emoji-1F345 { background-position: -6480px 0px; } +.emoji-1F346 { background-position: -6500px 0px; } +.emoji-1F347 { background-position: -6520px 0px; } +.emoji-1F348 { background-position: -6540px 0px; } +.emoji-1F349 { background-position: -6560px 0px; } +.emoji-1F34A { background-position: -6580px 0px; } +.emoji-1F34B { background-position: -6600px 0px; } +.emoji-1F34C { background-position: -6620px 0px; } +.emoji-1F34D { background-position: -6640px 0px; } +.emoji-1F34E { background-position: -6660px 0px; } +.emoji-1F34F { background-position: -6680px 0px; } +.emoji-1F350 { background-position: -6700px 0px; } +.emoji-1F351 { background-position: -6720px 0px; } +.emoji-1F352 { background-position: -6740px 0px; } +.emoji-1F353 { background-position: -6760px 0px; } +.emoji-1F354 { background-position: -6780px 0px; } +.emoji-1F355 { background-position: -6800px 0px; } +.emoji-1F356 { background-position: -6820px 0px; } +.emoji-1F357 { background-position: -6840px 0px; } +.emoji-1F358 { background-position: -6860px 0px; } +.emoji-1F359 { background-position: -6880px 0px; } +.emoji-1F35A { background-position: -6900px 0px; } +.emoji-1F35B { background-position: -6920px 0px; } +.emoji-1F35C { background-position: -6940px 0px; } +.emoji-1F35D { background-position: -6960px 0px; } +.emoji-1F35E { background-position: -6980px 0px; } +.emoji-1F35F { background-position: -7000px 0px; } +.emoji-1F360 { background-position: -7020px 0px; } +.emoji-1F361 { background-position: -7040px 0px; } +.emoji-1F362 { background-position: -7060px 0px; } +.emoji-1F363 { background-position: -7080px 0px; } +.emoji-1F364 { background-position: -7100px 0px; } +.emoji-1F365 { background-position: -7120px 0px; } +.emoji-1F366 { background-position: -7140px 0px; } +.emoji-1F367 { background-position: -7160px 0px; } +.emoji-1F368 { background-position: -7180px 0px; } +.emoji-1F369 { background-position: -7200px 0px; } +.emoji-1F36A { background-position: -7220px 0px; } +.emoji-1F36B { background-position: -7240px 0px; } +.emoji-1F36C { background-position: -7260px 0px; } +.emoji-1F36D { background-position: -7280px 0px; } +.emoji-1F36E { background-position: -7300px 0px; } +.emoji-1F36F { background-position: -7320px 0px; } +.emoji-1F370 { background-position: -7340px 0px; } +.emoji-1F371 { background-position: -7360px 0px; } +.emoji-1F372 { background-position: -7380px 0px; } +.emoji-1F373 { background-position: -7400px 0px; } +.emoji-1F374 { background-position: -7420px 0px; } +.emoji-1F375 { background-position: -7440px 0px; } +.emoji-1F376 { background-position: -7460px 0px; } +.emoji-1F377 { background-position: -7480px 0px; } +.emoji-1F378 { background-position: -7500px 0px; } +.emoji-1F379 { background-position: -7520px 0px; } +.emoji-1F37A { background-position: -7540px 0px; } +.emoji-1F37B { background-position: -7560px 0px; } +.emoji-1F37C { background-position: -7580px 0px; } +.emoji-1F37D { background-position: -7600px 0px; } +.emoji-1F380 { background-position: -7620px 0px; } +.emoji-1F381 { background-position: -7640px 0px; } +.emoji-1F382 { background-position: -7660px 0px; } +.emoji-1F383 { background-position: -7680px 0px; } +.emoji-1F384 { background-position: -7700px 0px; } +.emoji-1F385 { background-position: -7720px 0px; } +.emoji-1F386 { background-position: -7740px 0px; } +.emoji-1F387 { background-position: -7760px 0px; } +.emoji-1F388 { background-position: -7780px 0px; } +.emoji-1F389 { background-position: -7800px 0px; } +.emoji-1F38A { background-position: -7820px 0px; } +.emoji-1F38B { background-position: -7840px 0px; } +.emoji-1F38C { background-position: -7860px 0px; } +.emoji-1F38D { background-position: -7880px 0px; } +.emoji-1F38E { background-position: -7900px 0px; } +.emoji-1F38F { background-position: -7920px 0px; } +.emoji-1F390 { background-position: -7940px 0px; } +.emoji-1F391 { background-position: -7960px 0px; } +.emoji-1F392 { background-position: -7980px 0px; } +.emoji-1F393 { background-position: -8000px 0px; } +.emoji-1F394 { background-position: -8020px 0px; } +.emoji-1F395 { background-position: -8040px 0px; } +.emoji-1F396 { background-position: -8060px 0px; } +.emoji-1F397 { background-position: -8080px 0px; } +.emoji-1F398 { background-position: -8100px 0px; } +.emoji-1F399 { background-position: -8120px 0px; } +.emoji-1F39A { background-position: -8140px 0px; } +.emoji-1F39B { background-position: -8160px 0px; } +.emoji-1F39C { background-position: -8180px 0px; } +.emoji-1F39D { background-position: -8200px 0px; } +.emoji-1F39E { background-position: -8220px 0px; } +.emoji-1F39F { background-position: -8240px 0px; } +.emoji-1F3A0 { background-position: -8260px 0px; } +.emoji-1F3A1 { background-position: -8280px 0px; } +.emoji-1F3A2 { background-position: -8300px 0px; } +.emoji-1F3A3 { background-position: -8320px 0px; } +.emoji-1F3A4 { background-position: -8340px 0px; } +.emoji-1F3A5 { background-position: -8360px 0px; } +.emoji-1F3A6 { background-position: -8380px 0px; } +.emoji-1F3A7 { background-position: -8400px 0px; } +.emoji-1F3A8 { background-position: -8420px 0px; } +.emoji-1F3A9 { background-position: -8440px 0px; } +.emoji-1F3AA { background-position: -8460px 0px; } +.emoji-1F3AB { background-position: -8480px 0px; } +.emoji-1F3AC { background-position: -8500px 0px; } +.emoji-1F3AD { background-position: -8520px 0px; } +.emoji-1F3AE { background-position: -8540px 0px; } +.emoji-1F3AF { background-position: -8560px 0px; } +.emoji-1F3B0 { background-position: -8580px 0px; } +.emoji-1F3B1 { background-position: -8600px 0px; } +.emoji-1F3B2 { background-position: -8620px 0px; } +.emoji-1F3B3 { background-position: -8640px 0px; } +.emoji-1F3B4 { background-position: -8660px 0px; } +.emoji-1F3B5 { background-position: -8680px 0px; } +.emoji-1F3B6 { background-position: -8700px 0px; } +.emoji-1F3B7 { background-position: -8720px 0px; } +.emoji-1F3B8 { background-position: -8740px 0px; } +.emoji-1F3B9 { background-position: -8760px 0px; } +.emoji-1F3BA { background-position: -8780px 0px; } +.emoji-1F3BB { background-position: -8800px 0px; } +.emoji-1F3BC { background-position: -8820px 0px; } +.emoji-1F3BD { background-position: -8840px 0px; } +.emoji-1F3BE { background-position: -8860px 0px; } +.emoji-1F3BF { background-position: -8880px 0px; } +.emoji-1F3C0 { background-position: -8900px 0px; } +.emoji-1F3C1 { background-position: -8920px 0px; } +.emoji-1F3C2 { background-position: -8940px 0px; } +.emoji-1F3C3 { background-position: -8960px 0px; } +.emoji-1F3C4 { background-position: -8980px 0px; } +.emoji-1F3C5 { background-position: -9000px 0px; } +.emoji-1F3C6 { background-position: -9020px 0px; } +.emoji-1F3C7 { background-position: -9040px 0px; } +.emoji-1F3C8 { background-position: -9060px 0px; } +.emoji-1F3C9 { background-position: -9080px 0px; } +.emoji-1F3CA { background-position: -9100px 0px; } +.emoji-1F3CB { background-position: -9120px 0px; } +.emoji-1F3CC { background-position: -9140px 0px; } +.emoji-1F3CD { background-position: -9160px 0px; } +.emoji-1F3CE { background-position: -9180px 0px; } +.emoji-1F3D4 { background-position: -9200px 0px; } +.emoji-1F3D5 { background-position: -9220px 0px; } +.emoji-1F3D6 { background-position: -9240px 0px; } +.emoji-1F3D7 { background-position: -9260px 0px; } +.emoji-1F3D8 { background-position: -9280px 0px; } +.emoji-1F3D9 { background-position: -9300px 0px; } +.emoji-1F3DA { background-position: -9320px 0px; } +.emoji-1F3DB { background-position: -9340px 0px; } +.emoji-1F3DC { background-position: -9360px 0px; } +.emoji-1F3DD { background-position: -9380px 0px; } +.emoji-1F3DE { background-position: -9400px 0px; } +.emoji-1F3DF { background-position: -9420px 0px; } +.emoji-1F3E0 { background-position: -9440px 0px; } +.emoji-1F3E1 { background-position: -9460px 0px; } +.emoji-1F3E2 { background-position: -9480px 0px; } +.emoji-1F3E3 { background-position: -9500px 0px; } +.emoji-1F3E4 { background-position: -9520px 0px; } +.emoji-1F3E5 { background-position: -9540px 0px; } +.emoji-1F3E6 { background-position: -9560px 0px; } +.emoji-1F3E7 { background-position: -9580px 0px; } +.emoji-1F3E8 { background-position: -9600px 0px; } +.emoji-1F3E9 { background-position: -9620px 0px; } +.emoji-1F3EA { background-position: -9640px 0px; } +.emoji-1F3EB { background-position: -9660px 0px; } +.emoji-1F3EC { background-position: -9680px 0px; } +.emoji-1F3ED { background-position: -9700px 0px; } +.emoji-1F3EE { background-position: -9720px 0px; } +.emoji-1F3EF { background-position: -9740px 0px; } +.emoji-1F3F0 { background-position: -9760px 0px; } +.emoji-1F3F1 { background-position: -9780px 0px; } +.emoji-1F3F2 { background-position: -9800px 0px; } +.emoji-1F3F3 { background-position: -9820px 0px; } +.emoji-1F3F4 { background-position: -9840px 0px; } +.emoji-1F3F5 { background-position: -9860px 0px; } +.emoji-1F3F6 { background-position: -9880px 0px; } +.emoji-1F3F7 { background-position: -9900px 0px; } +.emoji-1F400 { background-position: -9920px 0px; } +.emoji-1F401 { background-position: -9940px 0px; } +.emoji-1F402 { background-position: -9960px 0px; } +.emoji-1F403 { background-position: -9980px 0px; } +.emoji-1F404 { background-position: -10000px 0px; } +.emoji-1F405 { background-position: -10020px 0px; } +.emoji-1F406 { background-position: -10040px 0px; } +.emoji-1F407 { background-position: -10060px 0px; } +.emoji-1F408 { background-position: -10080px 0px; } +.emoji-1F409 { background-position: -10100px 0px; } +.emoji-1F40A { background-position: -10120px 0px; } +.emoji-1F40B { background-position: -10140px 0px; } +.emoji-1F40C { background-position: -10160px 0px; } +.emoji-1F40D { background-position: -10180px 0px; } +.emoji-1F40E { background-position: -10200px 0px; } +.emoji-1F40F { background-position: -10220px 0px; } +.emoji-1F410 { background-position: -10240px 0px; } +.emoji-1F411 { background-position: -10260px 0px; } +.emoji-1F412 { background-position: -10280px 0px; } +.emoji-1F413 { background-position: -10300px 0px; } +.emoji-1F414 { background-position: -10320px 0px; } +.emoji-1F415 { background-position: -10340px 0px; } +.emoji-1F416 { background-position: -10360px 0px; } +.emoji-1F417 { background-position: -10380px 0px; } +.emoji-1F418 { background-position: -10400px 0px; } +.emoji-1F419 { background-position: -10420px 0px; } +.emoji-1F41A { background-position: -10440px 0px; } +.emoji-1F41B { background-position: -10460px 0px; } +.emoji-1F41C { background-position: -10480px 0px; } +.emoji-1F41D { background-position: -10500px 0px; } +.emoji-1F41E { background-position: -10520px 0px; } +.emoji-1F41F { background-position: -10540px 0px; } +.emoji-1F420 { background-position: -10560px 0px; } +.emoji-1F421 { background-position: -10580px 0px; } +.emoji-1F422 { background-position: -10600px 0px; } +.emoji-1F423 { background-position: -10620px 0px; } +.emoji-1F424 { background-position: -10640px 0px; } +.emoji-1F425 { background-position: -10660px 0px; } +.emoji-1F426 { background-position: -10680px 0px; } +.emoji-1F427 { background-position: -10700px 0px; } +.emoji-1F428 { background-position: -10720px 0px; } +.emoji-1F429 { background-position: -10740px 0px; } +.emoji-1F42A { background-position: -10760px 0px; } +.emoji-1F42B { background-position: -10780px 0px; } +.emoji-1F42C { background-position: -10800px 0px; } +.emoji-1F42D { background-position: -10820px 0px; } +.emoji-1F42E { background-position: -10840px 0px; } +.emoji-1F42F { background-position: -10860px 0px; } +.emoji-1F430 { background-position: -10880px 0px; } +.emoji-1F431 { background-position: -10900px 0px; } +.emoji-1F432 { background-position: -10920px 0px; } +.emoji-1F433 { background-position: -10940px 0px; } +.emoji-1F434 { background-position: -10960px 0px; } +.emoji-1F435 { background-position: -10980px 0px; } +.emoji-1F436 { background-position: -11000px 0px; } +.emoji-1F437 { background-position: -11020px 0px; } +.emoji-1F438 { background-position: -11040px 0px; } +.emoji-1F439 { background-position: -11060px 0px; } +.emoji-1F43A { background-position: -11080px 0px; } +.emoji-1F43B { background-position: -11100px 0px; } +.emoji-1F43C { background-position: -11120px 0px; } +.emoji-1F43D { background-position: -11140px 0px; } +.emoji-1F43E { background-position: -11160px 0px; } +.emoji-1F43F { background-position: -11180px 0px; } +.emoji-1F440 { background-position: -11200px 0px; } +.emoji-1F441 { background-position: -11220px 0px; } +.emoji-1F442 { background-position: -11240px 0px; } +.emoji-1F443 { background-position: -11260px 0px; } +.emoji-1F444 { background-position: -11280px 0px; } +.emoji-1F445 { background-position: -11300px 0px; } +.emoji-1F446 { background-position: -11320px 0px; } +.emoji-1F447 { background-position: -11340px 0px; } +.emoji-1F448 { background-position: -11360px 0px; } +.emoji-1F449 { background-position: -11380px 0px; } +.emoji-1F44A { background-position: -11400px 0px; } +.emoji-1F44B { background-position: -11420px 0px; } +.emoji-1F44C { background-position: -11440px 0px; } +.emoji-1F44D { background-position: -11460px 0px; } +.emoji-1F44E { background-position: -11480px 0px; } +.emoji-1F44F { background-position: -11500px 0px; } +.emoji-1F450 { background-position: -11520px 0px; } +.emoji-1F451 { background-position: -11540px 0px; } +.emoji-1F452 { background-position: -11560px 0px; } +.emoji-1F453 { background-position: -11580px 0px; } +.emoji-1F454 { background-position: -11600px 0px; } +.emoji-1F455 { background-position: -11620px 0px; } +.emoji-1F456 { background-position: -11640px 0px; } +.emoji-1F457 { background-position: -11660px 0px; } +.emoji-1F458 { background-position: -11680px 0px; } +.emoji-1F459 { background-position: -11700px 0px; } +.emoji-1F45A { background-position: -11720px 0px; } +.emoji-1F45B { background-position: -11740px 0px; } +.emoji-1F45C { background-position: -11760px 0px; } +.emoji-1F45D { background-position: -11780px 0px; } +.emoji-1F45E { background-position: -11800px 0px; } +.emoji-1F45F { background-position: -11820px 0px; } +.emoji-1F460 { background-position: -11840px 0px; } +.emoji-1F461 { background-position: -11860px 0px; } +.emoji-1F462 { background-position: -11880px 0px; } +.emoji-1F463 { background-position: -11900px 0px; } +.emoji-1F464 { background-position: -11920px 0px; } +.emoji-1F465 { background-position: -11940px 0px; } +.emoji-1F466 { background-position: -11960px 0px; } +.emoji-1F467 { background-position: -11980px 0px; } +.emoji-1F468 { background-position: -12000px 0px; } +.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; } +.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; } +.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; } +.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; } +.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; } +.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; } +.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; } +.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; } +.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; } +.emoji-1F468-2764-1F468 { background-position: -12200px 0px; } +.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; } +.emoji-1F469 { background-position: -12240px 0px; } +.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; } +.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; } +.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; } +.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; } +.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; } +.emoji-1F469-2764-1F469 { background-position: -12360px 0px; } +.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; } +.emoji-1F46A { background-position: -12400px 0px; } +.emoji-1F46B { background-position: -12420px 0px; } +.emoji-1F46C { background-position: -12440px 0px; } +.emoji-1F46D { background-position: -12460px 0px; } +.emoji-1F46E { background-position: -12480px 0px; } +.emoji-1F46F { background-position: -12500px 0px; } +.emoji-1F470 { background-position: -12520px 0px; } +.emoji-1F471 { background-position: -12540px 0px; } +.emoji-1F472 { background-position: -12560px 0px; } +.emoji-1F473 { background-position: -12580px 0px; } +.emoji-1F474 { background-position: -12600px 0px; } +.emoji-1F475 { background-position: -12620px 0px; } +.emoji-1F476 { background-position: -12640px 0px; } +.emoji-1F477 { background-position: -12660px 0px; } +.emoji-1F478 { background-position: -12680px 0px; } +.emoji-1F479 { background-position: -12700px 0px; } +.emoji-1F47A { background-position: -12720px 0px; } +.emoji-1F47B { background-position: -12740px 0px; } +.emoji-1F47C { background-position: -12760px 0px; } +.emoji-1F47D { background-position: -12780px 0px; } +.emoji-1F47E { background-position: -12800px 0px; } +.emoji-1F47F { background-position: -12820px 0px; } +.emoji-1F480 { background-position: -12840px 0px; } +.emoji-1F481 { background-position: -12860px 0px; } +.emoji-1F482 { background-position: -12880px 0px; } +.emoji-1F483 { background-position: -12900px 0px; } +.emoji-1F484 { background-position: -12920px 0px; } +.emoji-1F485 { background-position: -12940px 0px; } +.emoji-1F486 { background-position: -12960px 0px; } +.emoji-1F487 { background-position: -12980px 0px; } +.emoji-1F488 { background-position: -13000px 0px; } +.emoji-1F489 { background-position: -13020px 0px; } +.emoji-1F48A { background-position: -13040px 0px; } +.emoji-1F48B { background-position: -13060px 0px; } +.emoji-1F48C { background-position: -13080px 0px; } +.emoji-1F48D { background-position: -13100px 0px; } +.emoji-1F48E { background-position: -13120px 0px; } +.emoji-1F48F { background-position: -13140px 0px; } +.emoji-1F490 { background-position: -13160px 0px; } +.emoji-1F491 { background-position: -13180px 0px; } +.emoji-1F492 { background-position: -13200px 0px; } +.emoji-1F493 { background-position: -13220px 0px; } +.emoji-1F494 { background-position: -13240px 0px; } +.emoji-1F495 { background-position: -13260px 0px; } +.emoji-1F496 { background-position: -13280px 0px; } +.emoji-1F497 { background-position: -13300px 0px; } +.emoji-1F498 { background-position: -13320px 0px; } +.emoji-1F499 { background-position: -13340px 0px; } +.emoji-1F49A { background-position: -13360px 0px; } +.emoji-1F49B { background-position: -13380px 0px; } +.emoji-1F49C { background-position: -13400px 0px; } +.emoji-1F49D { background-position: -13420px 0px; } +.emoji-1F49E { background-position: -13440px 0px; } +.emoji-1F49F { background-position: -13460px 0px; } +.emoji-1F4A0 { background-position: -13480px 0px; } +.emoji-1F4A1 { background-position: -13500px 0px; } +.emoji-1F4A2 { background-position: -13520px 0px; } +.emoji-1F4A3 { background-position: -13540px 0px; } +.emoji-1F4A4 { background-position: -13560px 0px; } +.emoji-1F4A5 { background-position: -13580px 0px; } +.emoji-1F4A6 { background-position: -13600px 0px; } +.emoji-1F4A7 { background-position: -13620px 0px; } +.emoji-1F4A8 { background-position: -13640px 0px; } +.emoji-1F4A9 { background-position: -13660px 0px; } +.emoji-1F4AA { background-position: -13680px 0px; } +.emoji-1F4AB { background-position: -13700px 0px; } +.emoji-1F4AC { background-position: -13720px 0px; } +.emoji-1F4AD { background-position: -13740px 0px; } +.emoji-1F4AE { background-position: -13760px 0px; } +.emoji-1F4AF { background-position: -13780px 0px; } +.emoji-1F4B0 { background-position: -13800px 0px; } +.emoji-1F4B1 { background-position: -13820px 0px; } +.emoji-1F4B2 { background-position: -13840px 0px; } +.emoji-1F4B3 { background-position: -13860px 0px; } +.emoji-1F4B4 { background-position: -13880px 0px; } +.emoji-1F4B5 { background-position: -13900px 0px; } +.emoji-1F4B6 { background-position: -13920px 0px; } +.emoji-1F4B7 { background-position: -13940px 0px; } +.emoji-1F4B8 { background-position: -13960px 0px; } +.emoji-1F4B9 { background-position: -13980px 0px; } +.emoji-1F4BA { background-position: -14000px 0px; } +.emoji-1F4BB { background-position: -14020px 0px; } +.emoji-1F4BC { background-position: -14040px 0px; } +.emoji-1F4BD { background-position: -14060px 0px; } +.emoji-1F4BE { background-position: -14080px 0px; } +.emoji-1F4BF { background-position: -14100px 0px; } +.emoji-1F4C0 { background-position: -14120px 0px; } +.emoji-1F4C1 { background-position: -14140px 0px; } +.emoji-1F4C2 { background-position: -14160px 0px; } +.emoji-1F4C3 { background-position: -14180px 0px; } +.emoji-1F4C4 { background-position: -14200px 0px; } +.emoji-1F4C5 { background-position: -14220px 0px; } +.emoji-1F4C6 { background-position: -14240px 0px; } +.emoji-1F4C7 { background-position: -14260px 0px; } +.emoji-1F4C8 { background-position: -14280px 0px; } +.emoji-1F4C9 { background-position: -14300px 0px; } +.emoji-1F4CA { background-position: -14320px 0px; } +.emoji-1F4CB { background-position: -14340px 0px; } +.emoji-1F4CC { background-position: -14360px 0px; } +.emoji-1F4CD { background-position: -14380px 0px; } +.emoji-1F4CE { background-position: -14400px 0px; } +.emoji-1F4CF { background-position: -14420px 0px; } +.emoji-1F4D0 { background-position: -14440px 0px; } +.emoji-1F4D1 { background-position: -14460px 0px; } +.emoji-1F4D2 { background-position: -14480px 0px; } +.emoji-1F4D3 { background-position: -14500px 0px; } +.emoji-1F4D4 { background-position: -14520px 0px; } +.emoji-1F4D5 { background-position: -14540px 0px; } +.emoji-1F4D6 { background-position: -14560px 0px; } +.emoji-1F4D7 { background-position: -14580px 0px; } +.emoji-1F4D8 { background-position: -14600px 0px; } +.emoji-1F4D9 { background-position: -14620px 0px; } +.emoji-1F4DA { background-position: -14640px 0px; } +.emoji-1F4DB { background-position: -14660px 0px; } +.emoji-1F4DC { background-position: -14680px 0px; } +.emoji-1F4DD { background-position: -14700px 0px; } +.emoji-1F4DE { background-position: -14720px 0px; } +.emoji-1F4DF { background-position: -14740px 0px; } +.emoji-1F4E0 { background-position: -14760px 0px; } +.emoji-1F4E1 { background-position: -14780px 0px; } +.emoji-1F4E2 { background-position: -14800px 0px; } +.emoji-1F4E3 { background-position: -14820px 0px; } +.emoji-1F4E4 { background-position: -14840px 0px; } +.emoji-1F4E5 { background-position: -14860px 0px; } +.emoji-1F4E6 { background-position: -14880px 0px; } +.emoji-1F4E7 { background-position: -14900px 0px; } +.emoji-1F4E8 { background-position: -14920px 0px; } +.emoji-1F4E9 { background-position: -14940px 0px; } +.emoji-1F4EA { background-position: -14960px 0px; } +.emoji-1F4EB { background-position: -14980px 0px; } +.emoji-1F4EC { background-position: -15000px 0px; } +.emoji-1F4ED { background-position: -15020px 0px; } +.emoji-1F4EE { background-position: -15040px 0px; } +.emoji-1F4EF { background-position: -15060px 0px; } +.emoji-1F4F0 { background-position: -15080px 0px; } +.emoji-1F4F1 { background-position: -15100px 0px; } +.emoji-1F4F2 { background-position: -15120px 0px; } +.emoji-1F4F3 { background-position: -15140px 0px; } +.emoji-1F4F4 { background-position: -15160px 0px; } +.emoji-1F4F5 { background-position: -15180px 0px; } +.emoji-1F4F6 { background-position: -15200px 0px; } +.emoji-1F4F7 { background-position: -15220px 0px; } +.emoji-1F4F8 { background-position: -15240px 0px; } +.emoji-1F4F9 { background-position: -15260px 0px; } +.emoji-1F4FA { background-position: -15280px 0px; } +.emoji-1F4FB { background-position: -15300px 0px; } +.emoji-1F4FC { background-position: -15320px 0px; } +.emoji-1F4FD { background-position: -15340px 0px; } +.emoji-1F4FE { background-position: -15360px 0px; } +.emoji-1F500 { background-position: -15380px 0px; } +.emoji-1F501 { background-position: -15400px 0px; } +.emoji-1F502 { background-position: -15420px 0px; } +.emoji-1F503 { background-position: -15440px 0px; } +.emoji-1F504 { background-position: -15460px 0px; } +.emoji-1F505 { background-position: -15480px 0px; } +.emoji-1F506 { background-position: -15500px 0px; } +.emoji-1F507 { background-position: -15520px 0px; } +.emoji-1F508 { background-position: -15540px 0px; } +.emoji-1F509 { background-position: -15560px 0px; } +.emoji-1F50A { background-position: -15580px 0px; } +.emoji-1F50B { background-position: -15600px 0px; } +.emoji-1F50C { background-position: -15620px 0px; } +.emoji-1F50D { background-position: -15640px 0px; } +.emoji-1F50E { background-position: -15660px 0px; } +.emoji-1F50F { background-position: -15680px 0px; } +.emoji-1F510 { background-position: -15700px 0px; } +.emoji-1F511 { background-position: -15720px 0px; } +.emoji-1F512 { background-position: -15740px 0px; } +.emoji-1F513 { background-position: -15760px 0px; } +.emoji-1F514 { background-position: -15780px 0px; } +.emoji-1F515 { background-position: -15800px 0px; } +.emoji-1F516 { background-position: -15820px 0px; } +.emoji-1F517 { background-position: -15840px 0px; } +.emoji-1F518 { background-position: -15860px 0px; } +.emoji-1F519 { background-position: -15880px 0px; } +.emoji-1F51A { background-position: -15900px 0px; } +.emoji-1F51B { background-position: -15920px 0px; } +.emoji-1F51C { background-position: -15940px 0px; } +.emoji-1F51D { background-position: -15960px 0px; } +.emoji-1F51E { background-position: -15980px 0px; } +.emoji-1F51F { background-position: -16000px 0px; } +.emoji-1F520 { background-position: -16020px 0px; } +.emoji-1F521 { background-position: -16040px 0px; } +.emoji-1F522 { background-position: -16060px 0px; } +.emoji-1F523 { background-position: -16080px 0px; } +.emoji-1F524 { background-position: -16100px 0px; } +.emoji-1F525 { background-position: -16120px 0px; } +.emoji-1F526 { background-position: -16140px 0px; } +.emoji-1F527 { background-position: -16160px 0px; } +.emoji-1F528 { background-position: -16180px 0px; } +.emoji-1F529 { background-position: -16200px 0px; } +.emoji-1F52A { background-position: -16220px 0px; } +.emoji-1F52B { background-position: -16240px 0px; } +.emoji-1F52C { background-position: -16260px 0px; } +.emoji-1F52D { background-position: -16280px 0px; } +.emoji-1F52E { background-position: -16300px 0px; } +.emoji-1F52F { background-position: -16320px 0px; } +.emoji-1F530 { background-position: -16340px 0px; } +.emoji-1F531 { background-position: -16360px 0px; } +.emoji-1F532 { background-position: -16380px 0px; } +.emoji-1F533 { background-position: -16400px 0px; } +.emoji-1F534 { background-position: -16420px 0px; } +.emoji-1F535 { background-position: -16440px 0px; } +.emoji-1F536 { background-position: -16460px 0px; } +.emoji-1F537 { background-position: -16480px 0px; } +.emoji-1F538 { background-position: -16500px 0px; } +.emoji-1F539 { background-position: -16520px 0px; } +.emoji-1F53A { background-position: -16540px 0px; } +.emoji-1F53B { background-position: -16560px 0px; } +.emoji-1F53C { background-position: -16580px 0px; } +.emoji-1F53D { background-position: -16600px 0px; } +.emoji-1F546 { background-position: -16620px 0px; } +.emoji-1F547 { background-position: -16640px 0px; } +.emoji-1F548 { background-position: -16660px 0px; } +.emoji-1F549 { background-position: -16680px 0px; } +.emoji-1F54A { background-position: -16700px 0px; } +.emoji-1F550 { background-position: -16720px 0px; } +.emoji-1F551 { background-position: -16740px 0px; } +.emoji-1F552 { background-position: -16760px 0px; } +.emoji-1F553 { background-position: -16780px 0px; } +.emoji-1F554 { background-position: -16800px 0px; } +.emoji-1F555 { background-position: -16820px 0px; } +.emoji-1F556 { background-position: -16840px 0px; } +.emoji-1F557 { background-position: -16860px 0px; } +.emoji-1F558 { background-position: -16880px 0px; } +.emoji-1F559 { background-position: -16900px 0px; } +.emoji-1F55A { background-position: -16920px 0px; } +.emoji-1F55B { background-position: -16940px 0px; } +.emoji-1F55C { background-position: -16960px 0px; } +.emoji-1F55D { background-position: -16980px 0px; } +.emoji-1F55E { background-position: -17000px 0px; } +.emoji-1F55F { background-position: -17020px 0px; } +.emoji-1F560 { background-position: -17040px 0px; } +.emoji-1F561 { background-position: -17060px 0px; } +.emoji-1F562 { background-position: -17080px 0px; } +.emoji-1F563 { background-position: -17100px 0px; } +.emoji-1F564 { background-position: -17120px 0px; } +.emoji-1F565 { background-position: -17140px 0px; } +.emoji-1F566 { background-position: -17160px 0px; } +.emoji-1F567 { background-position: -17180px 0px; } +.emoji-1F568 { background-position: -17200px 0px; } +.emoji-1F569 { background-position: -17220px 0px; } +.emoji-1F56A { background-position: -17240px 0px; } +.emoji-1F56B { background-position: -17260px 0px; } +.emoji-1F56C { background-position: -17280px 0px; } +.emoji-1F56D { background-position: -17300px 0px; } +.emoji-1F56E { background-position: -17320px 0px; } +.emoji-1F56F { background-position: -17340px 0px; } +.emoji-1F570 { background-position: -17360px 0px; } +.emoji-1F571 { background-position: -17380px 0px; } +.emoji-1F572 { background-position: -17400px 0px; } +.emoji-1F573 { background-position: -17420px 0px; } +.emoji-1F574 { background-position: -17440px 0px; } +.emoji-1F575 { background-position: -17460px 0px; } +.emoji-1F576 { background-position: -17480px 0px; } +.emoji-1F577 { background-position: -17500px 0px; } +.emoji-1F578 { background-position: -17520px 0px; } +.emoji-1F579 { background-position: -17540px 0px; } +.emoji-1F57B { background-position: -17560px 0px; } +.emoji-1F57E { background-position: -17580px 0px; } +.emoji-1F57F { background-position: -17600px 0px; } +.emoji-1F581 { background-position: -17620px 0px; } +.emoji-1F582 { background-position: -17640px 0px; } +.emoji-1F583 { background-position: -17660px 0px; } +.emoji-1F585 { background-position: -17680px 0px; } +.emoji-1F586 { background-position: -17700px 0px; } +.emoji-1F587 { background-position: -17720px 0px; } +.emoji-1F588 { background-position: -17740px 0px; } +.emoji-1F589 { background-position: -17760px 0px; } +.emoji-1F58A { background-position: -17780px 0px; } +.emoji-1F58B { background-position: -17800px 0px; } +.emoji-1F58C { background-position: -17820px 0px; } +.emoji-1F58D { background-position: -17840px 0px; } +.emoji-1F58E { background-position: -17860px 0px; } +.emoji-1F58F { background-position: -17880px 0px; } +.emoji-1F590 { background-position: -17900px 0px; } +.emoji-1F591 { background-position: -17920px 0px; } +.emoji-1F592 { background-position: -17940px 0px; } +.emoji-1F593 { background-position: -17960px 0px; } +.emoji-1F594 { background-position: -17980px 0px; } +.emoji-1F595 { background-position: -18000px 0px; } +.emoji-1F596 { background-position: -18020px 0px; } +.emoji-1F597 { background-position: -18040px 0px; } +.emoji-1F598 { background-position: -18060px 0px; } +.emoji-1F599 { background-position: -18080px 0px; } +.emoji-1F59E { background-position: -18100px 0px; } +.emoji-1F59F { background-position: -18120px 0px; } +.emoji-1F5A5 { background-position: -18140px 0px; } +.emoji-1F5A6 { background-position: -18160px 0px; } +.emoji-1F5A7 { background-position: -18180px 0px; } +.emoji-1F5A8 { background-position: -18200px 0px; } +.emoji-1F5A9 { background-position: -18220px 0px; } +.emoji-1F5AA { background-position: -18240px 0px; } +.emoji-1F5AB { background-position: -18260px 0px; } +.emoji-1F5AD { background-position: -18280px 0px; } +.emoji-1F5AE { background-position: -18300px 0px; } +.emoji-1F5AF { background-position: -18320px 0px; } +.emoji-1F5B2 { background-position: -18340px 0px; } +.emoji-1F5B3 { background-position: -18360px 0px; } +.emoji-1F5B4 { background-position: -18380px 0px; } +.emoji-1F5B8 { background-position: -18400px 0px; } +.emoji-1F5B9 { background-position: -18420px 0px; } +.emoji-1F5BC { background-position: -18440px 0px; } +.emoji-1F5BD { background-position: -18460px 0px; } +.emoji-1F5BE { background-position: -18480px 0px; } +.emoji-1F5C0 { background-position: -18500px 0px; } +.emoji-1F5C1 { background-position: -18520px 0px; } +.emoji-1F5C2 { background-position: -18540px 0px; } +.emoji-1F5C3 { background-position: -18560px 0px; } +.emoji-1F5C4 { background-position: -18580px 0px; } +.emoji-1F5C6 { background-position: -18600px 0px; } +.emoji-1F5C7 { background-position: -18620px 0px; } +.emoji-1F5C9 { background-position: -18640px 0px; } +.emoji-1F5CA { background-position: -18660px 0px; } +.emoji-1F5CE { background-position: -18680px 0px; } +.emoji-1F5CF { background-position: -18700px 0px; } +.emoji-1F5D0 { background-position: -18720px 0px; } +.emoji-1F5D1 { background-position: -18740px 0px; } +.emoji-1F5D2 { background-position: -18760px 0px; } +.emoji-1F5D3 { background-position: -18780px 0px; } +.emoji-1F5D4 { background-position: -18800px 0px; } +.emoji-1F5D8 { background-position: -18820px 0px; } +.emoji-1F5D9 { background-position: -18840px 0px; } +.emoji-1F5DC { background-position: -18860px 0px; } +.emoji-1F5DD { background-position: -18880px 0px; } +.emoji-1F5DE { background-position: -18900px 0px; } +.emoji-1F5E0 { background-position: -18920px 0px; } +.emoji-1F5E1 { background-position: -18940px 0px; } +.emoji-1F5E2 { background-position: -18960px 0px; } +.emoji-1F5E3 { background-position: -18980px 0px; } +.emoji-1F5E8 { background-position: -19000px 0px; } +.emoji-1F5E9 { background-position: -19020px 0px; } +.emoji-1F5EA { background-position: -19040px 0px; } +.emoji-1F5EB { background-position: -19060px 0px; } +.emoji-1F5EC { background-position: -19080px 0px; } +.emoji-1F5ED { background-position: -19100px 0px; } +.emoji-1F5EE { background-position: -19120px 0px; } +.emoji-1F5EF { background-position: -19140px 0px; } +.emoji-1F5F0 { background-position: -19160px 0px; } +.emoji-1F5F1 { background-position: -19180px 0px; } +.emoji-1F5F2 { background-position: -19200px 0px; } +.emoji-1F5F3 { background-position: -19220px 0px; } +.emoji-1F5F4 { background-position: -19240px 0px; } +.emoji-1F5F5 { background-position: -19260px 0px; } +.emoji-1F5F8 { background-position: -19280px 0px; } +.emoji-1F5F9 { background-position: -19300px 0px; } +.emoji-1F5FA { background-position: -19320px 0px; } +.emoji-1F5FB { background-position: -19340px 0px; } +.emoji-1F5FC { background-position: -19360px 0px; } +.emoji-1F5FD { background-position: -19380px 0px; } +.emoji-1F5FE { background-position: -19400px 0px; } +.emoji-1F5FF { background-position: -19420px 0px; } +.emoji-1F600 { background-position: -19440px 0px; } +.emoji-1F601 { background-position: -19460px 0px; } +.emoji-1F602 { background-position: -19480px 0px; } +.emoji-1F603 { background-position: -19500px 0px; } +.emoji-1F604 { background-position: -19520px 0px; } +.emoji-1F605 { background-position: -19540px 0px; } +.emoji-1F606 { background-position: -19560px 0px; } +.emoji-1F607 { background-position: -19580px 0px; } +.emoji-1F608 { background-position: -19600px 0px; } +.emoji-1F609 { background-position: -19620px 0px; } +.emoji-1F60A { background-position: -19640px 0px; } +.emoji-1F60B { background-position: -19660px 0px; } +.emoji-1F60C { background-position: -19680px 0px; } +.emoji-1F60D { background-position: -19700px 0px; } +.emoji-1F60E { background-position: -19720px 0px; } +.emoji-1F60F { background-position: -19740px 0px; } +.emoji-1F610 { background-position: -19760px 0px; } +.emoji-1F611 { background-position: -19780px 0px; } +.emoji-1F612 { background-position: -19800px 0px; } +.emoji-1F613 { background-position: -19820px 0px; } +.emoji-1F614 { background-position: -19840px 0px; } +.emoji-1F615 { background-position: -19860px 0px; } +.emoji-1F616 { background-position: -19880px 0px; } +.emoji-1F617 { background-position: -19900px 0px; } +.emoji-1F618 { background-position: -19920px 0px; } +.emoji-1F619 { background-position: -19940px 0px; } +.emoji-1F61A { background-position: -19960px 0px; } +.emoji-1F61B { background-position: -19980px 0px; } +.emoji-1F61C { background-position: -20000px 0px; } +.emoji-1F61D { background-position: -20020px 0px; } +.emoji-1F61E { background-position: -20040px 0px; } +.emoji-1F61F { background-position: -20060px 0px; } +.emoji-1F620 { background-position: -20080px 0px; } +.emoji-1F621 { background-position: -20100px 0px; } +.emoji-1F622 { background-position: -20120px 0px; } +.emoji-1F623 { background-position: -20140px 0px; } +.emoji-1F624 { background-position: -20160px 0px; } +.emoji-1F625 { background-position: -20180px 0px; } +.emoji-1F626 { background-position: -20200px 0px; } +.emoji-1F627 { background-position: -20220px 0px; } +.emoji-1F628 { background-position: -20240px 0px; } +.emoji-1F629 { background-position: -20260px 0px; } +.emoji-1F62A { background-position: -20280px 0px; } +.emoji-1F62B { background-position: -20300px 0px; } +.emoji-1F62C { background-position: -20320px 0px; } +.emoji-1F62D { background-position: -20340px 0px; } +.emoji-1F62E { background-position: -20360px 0px; } +.emoji-1F62F { background-position: -20380px 0px; } +.emoji-1F630 { background-position: -20400px 0px; } +.emoji-1F631 { background-position: -20420px 0px; } +.emoji-1F632 { background-position: -20440px 0px; } +.emoji-1F633 { background-position: -20460px 0px; } +.emoji-1F634 { background-position: -20480px 0px; } +.emoji-1F635 { background-position: -20500px 0px; } +.emoji-1F636 { background-position: -20520px 0px; } +.emoji-1F637 { background-position: -20540px 0px; } +.emoji-1F638 { background-position: -20560px 0px; } +.emoji-1F639 { background-position: -20580px 0px; } +.emoji-1F63A { background-position: -20600px 0px; } +.emoji-1F63B { background-position: -20620px 0px; } +.emoji-1F63C { background-position: -20640px 0px; } +.emoji-1F63D { background-position: -20660px 0px; } +.emoji-1F63E { background-position: -20680px 0px; } +.emoji-1F63F { background-position: -20700px 0px; } +.emoji-1F640 { background-position: -20720px 0px; } +.emoji-1F641 { background-position: -20740px 0px; } +.emoji-1F642 { background-position: -20760px 0px; } +.emoji-1F645 { background-position: -20780px 0px; } +.emoji-1F646 { background-position: -20800px 0px; } +.emoji-1F647 { background-position: -20820px 0px; } +.emoji-1F648 { background-position: -20840px 0px; } +.emoji-1F649 { background-position: -20860px 0px; } +.emoji-1F64A { background-position: -20880px 0px; } +.emoji-1F64B { background-position: -20900px 0px; } +.emoji-1F64C { background-position: -20920px 0px; } +.emoji-1F64D { background-position: -20940px 0px; } +.emoji-1F64E { background-position: -20960px 0px; } +.emoji-1F64F { background-position: -20980px 0px; } +.emoji-1F680 { background-position: -21000px 0px; } +.emoji-1F681 { background-position: -21020px 0px; } +.emoji-1F682 { background-position: -21040px 0px; } +.emoji-1F683 { background-position: -21060px 0px; } +.emoji-1F684 { background-position: -21080px 0px; } +.emoji-1F685 { background-position: -21100px 0px; } +.emoji-1F686 { background-position: -21120px 0px; } +.emoji-1F687 { background-position: -21140px 0px; } +.emoji-1F688 { background-position: -21160px 0px; } +.emoji-1F689 { background-position: -21180px 0px; } +.emoji-1F68A { background-position: -21200px 0px; } +.emoji-1F68B { background-position: -21220px 0px; } +.emoji-1F68C { background-position: -21240px 0px; } +.emoji-1F68D { background-position: -21260px 0px; } +.emoji-1F68E { background-position: -21280px 0px; } +.emoji-1F68F { background-position: -21300px 0px; } +.emoji-1F690 { background-position: -21320px 0px; } +.emoji-1F691 { background-position: -21340px 0px; } +.emoji-1F692 { background-position: -21360px 0px; } +.emoji-1F693 { background-position: -21380px 0px; } +.emoji-1F694 { background-position: -21400px 0px; } +.emoji-1F695 { background-position: -21420px 0px; } +.emoji-1F696 { background-position: -21440px 0px; } +.emoji-1F697 { background-position: -21460px 0px; } +.emoji-1F698 { background-position: -21480px 0px; } +.emoji-1F699 { background-position: -21500px 0px; } +.emoji-1F69A { background-position: -21520px 0px; } +.emoji-1F69B { background-position: -21540px 0px; } +.emoji-1F69C { background-position: -21560px 0px; } +.emoji-1F69D { background-position: -21580px 0px; } +.emoji-1F69E { background-position: -21600px 0px; } +.emoji-1F69F { background-position: -21620px 0px; } +.emoji-1F6A0 { background-position: -21640px 0px; } +.emoji-1F6A1 { background-position: -21660px 0px; } +.emoji-1F6A2 { background-position: -21680px 0px; } +.emoji-1F6A3 { background-position: -21700px 0px; } +.emoji-1F6A4 { background-position: -21720px 0px; } +.emoji-1F6A5 { background-position: -21740px 0px; } +.emoji-1F6A6 { background-position: -21760px 0px; } +.emoji-1F6A7 { background-position: -21780px 0px; } +.emoji-1F6A8 { background-position: -21800px 0px; } +.emoji-1F6A9 { background-position: -21820px 0px; } +.emoji-1F6AA { background-position: -21840px 0px; } +.emoji-1F6AB { background-position: -21860px 0px; } +.emoji-1F6AC { background-position: -21880px 0px; } +.emoji-1F6AD { background-position: -21900px 0px; } +.emoji-1F6AE { background-position: -21920px 0px; } +.emoji-1F6AF { background-position: -21940px 0px; } +.emoji-1F6B0 { background-position: -21960px 0px; } +.emoji-1F6B1 { background-position: -21980px 0px; } +.emoji-1F6B2 { background-position: -22000px 0px; } +.emoji-1F6B3 { background-position: -22020px 0px; } +.emoji-1F6B4 { background-position: -22040px 0px; } +.emoji-1F6B5 { background-position: -22060px 0px; } +.emoji-1F6B6 { background-position: -22080px 0px; } +.emoji-1F6B7 { background-position: -22100px 0px; } +.emoji-1F6B8 { background-position: -22120px 0px; } +.emoji-1F6B9 { background-position: -22140px 0px; } +.emoji-1F6BA { background-position: -22160px 0px; } +.emoji-1F6BB { background-position: -22180px 0px; } +.emoji-1F6BC { background-position: -22200px 0px; } +.emoji-1F6BD { background-position: -22220px 0px; } +.emoji-1F6BE { background-position: -22240px 0px; } +.emoji-1F6BF { background-position: -22260px 0px; } +.emoji-1F6C0 { background-position: -22280px 0px; } +.emoji-1F6C1 { background-position: -22300px 0px; } +.emoji-1F6C2 { background-position: -22320px 0px; } +.emoji-1F6C3 { background-position: -22340px 0px; } +.emoji-1F6C4 { background-position: -22360px 0px; } +.emoji-1F6C5 { background-position: -22380px 0px; } +.emoji-1F6C6 { background-position: -22400px 0px; } +.emoji-1F6C7 { background-position: -22420px 0px; } +.emoji-1F6C8 { background-position: -22440px 0px; } +.emoji-1F6C9 { background-position: -22460px 0px; } +.emoji-1F6CA { background-position: -22480px 0px; } +.emoji-1F6CB { background-position: -22500px 0px; } +.emoji-1F6CC { background-position: -22520px 0px; } +.emoji-1F6CD { background-position: -22540px 0px; } +.emoji-1F6CE { background-position: -22560px 0px; } +.emoji-1F6CF { background-position: -22580px 0px; } +.emoji-1F6E0 { background-position: -22600px 0px; } +.emoji-1F6E1 { background-position: -22620px 0px; } +.emoji-1F6E2 { background-position: -22640px 0px; } +.emoji-1F6E3 { background-position: -22660px 0px; } +.emoji-1F6E4 { background-position: -22680px 0px; } +.emoji-1F6E5 { background-position: -22700px 0px; } +.emoji-1F6E6 { background-position: -22720px 0px; } +.emoji-1F6E7 { background-position: -22740px 0px; } +.emoji-1F6E8 { background-position: -22760px 0px; } +.emoji-1F6E9 { background-position: -22780px 0px; } +.emoji-1F6EA { background-position: -22800px 0px; } +.emoji-1F6EB { background-position: -22820px 0px; } +.emoji-1F6EC { background-position: -22840px 0px; } +.emoji-1F6F0 { background-position: -22860px 0px; } +.emoji-1F6F1 { background-position: -22880px 0px; } +.emoji-1F6F2 { background-position: -22900px 0px; } +.emoji-1F6F3 { background-position: -22920px 0px; } +.emoji-203C { background-position: -22940px 0px; } +.emoji-2049 { background-position: -22960px 0px; } +.emoji-2122 { background-position: -22980px 0px; } +.emoji-2139 { background-position: -23000px 0px; } +.emoji-2194 { background-position: -23020px 0px; } +.emoji-2195 { background-position: -23040px 0px; } +.emoji-2196 { background-position: -23060px 0px; } +.emoji-2197 { background-position: -23080px 0px; } +.emoji-2198 { background-position: -23100px 0px; } +.emoji-2199 { background-position: -23120px 0px; } +.emoji-21A9 { background-position: -23140px 0px; } +.emoji-21AA { background-position: -23160px 0px; } +.emoji-231A { background-position: -23180px 0px; } +.emoji-231B { background-position: -23200px 0px; } +.emoji-23E9 { background-position: -23220px 0px; } +.emoji-23EA { background-position: -23240px 0px; } +.emoji-23EB { background-position: -23260px 0px; } +.emoji-23EC { background-position: -23280px 0px; } +.emoji-23F0 { background-position: -23300px 0px; } +.emoji-23F3 { background-position: -23320px 0px; } +.emoji-24C2 { background-position: -23340px 0px; } +.emoji-25AA { background-position: -23360px 0px; } +.emoji-25AB { background-position: -23380px 0px; } +.emoji-25B6 { background-position: -23400px 0px; } +.emoji-25C0 { background-position: -23420px 0px; } +.emoji-25FB { background-position: -23440px 0px; } +.emoji-25FC { background-position: -23460px 0px; } +.emoji-25FD { background-position: -23480px 0px; } +.emoji-25FE { background-position: -23500px 0px; } +.emoji-2600 { background-position: -23520px 0px; } +.emoji-2601 { background-position: -23540px 0px; } +.emoji-260E { background-position: -23560px 0px; } +.emoji-2611 { background-position: -23580px 0px; } +.emoji-2614 { background-position: -23600px 0px; } +.emoji-2615 { background-position: -23620px 0px; } +.emoji-261D { background-position: -23640px 0px; } +.emoji-263A { background-position: -23660px 0px; } +.emoji-2648 { background-position: -23680px 0px; } +.emoji-2649 { background-position: -23700px 0px; } +.emoji-264A { background-position: -23720px 0px; } +.emoji-264B { background-position: -23740px 0px; } +.emoji-264C { background-position: -23760px 0px; } +.emoji-264D { background-position: -23780px 0px; } +.emoji-264E { background-position: -23800px 0px; } +.emoji-264F { background-position: -23820px 0px; } +.emoji-2650 { background-position: -23840px 0px; } +.emoji-2651 { background-position: -23860px 0px; } +.emoji-2652 { background-position: -23880px 0px; } +.emoji-2653 { background-position: -23900px 0px; } +.emoji-2660 { background-position: -23920px 0px; } +.emoji-2663 { background-position: -23940px 0px; } +.emoji-2665 { background-position: -23960px 0px; } +.emoji-2666 { background-position: -23980px 0px; } +.emoji-2668 { background-position: -24000px 0px; } +.emoji-267B { background-position: -24020px 0px; } +.emoji-267F { background-position: -24040px 0px; } +.emoji-2693 { background-position: -24060px 0px; } +.emoji-26A0 { background-position: -24080px 0px; } +.emoji-26A1 { background-position: -24100px 0px; } +.emoji-26AA { background-position: -24120px 0px; } +.emoji-26AB { background-position: -24140px 0px; } +.emoji-26BD { background-position: -24160px 0px; } +.emoji-26BE { background-position: -24180px 0px; } +.emoji-26C4 { background-position: -24200px 0px; } +.emoji-26C5 { background-position: -24220px 0px; } +.emoji-26CE { background-position: -24240px 0px; } +.emoji-26D4 { background-position: -24260px 0px; } +.emoji-26EA { background-position: -24280px 0px; } +.emoji-26F2 { background-position: -24300px 0px; } +.emoji-26F3 { background-position: -24320px 0px; } +.emoji-26F5 { background-position: -24340px 0px; } +.emoji-26FA { background-position: -24360px 0px; } +.emoji-26FD { background-position: -24380px 0px; } +.emoji-2702 { background-position: -24400px 0px; } +.emoji-2705 { background-position: -24420px 0px; } +.emoji-2708 { background-position: -24440px 0px; } +.emoji-2709 { background-position: -24460px 0px; } +.emoji-270A { background-position: -24480px 0px; } +.emoji-270B { background-position: -24500px 0px; } +.emoji-270C { background-position: -24520px 0px; } +.emoji-270F { background-position: -24540px 0px; } +.emoji-2712 { background-position: -24560px 0px; } +.emoji-2714 { background-position: -24580px 0px; } +.emoji-2716 { background-position: -24600px 0px; } +.emoji-2728 { background-position: -24620px 0px; } +.emoji-2733 { background-position: -24640px 0px; } +.emoji-2734 { background-position: -24660px 0px; } +.emoji-2744 { background-position: -24680px 0px; } +.emoji-2747 { background-position: -24700px 0px; } +.emoji-274C { background-position: -24720px 0px; } +.emoji-274E { background-position: -24740px 0px; } +.emoji-2753 { background-position: -24760px 0px; } +.emoji-2754 { background-position: -24780px 0px; } +.emoji-2755 { background-position: -24800px 0px; } +.emoji-2757 { background-position: -24820px 0px; } +.emoji-2764 { background-position: -24840px 0px; } +.emoji-2795 { background-position: -24860px 0px; } +.emoji-2796 { background-position: -24880px 0px; } +.emoji-2797 { background-position: -24900px 0px; } +.emoji-27A1 { background-position: -24920px 0px; } +.emoji-27B0 { background-position: -24940px 0px; } +.emoji-27BF { background-position: -24960px 0px; } +.emoji-2934 { background-position: -24980px 0px; } +.emoji-2935 { background-position: -25000px 0px; } +.emoji-2B05 { background-position: -25020px 0px; } +.emoji-2B06 { background-position: -25040px 0px; } +.emoji-2B07 { background-position: -25060px 0px; } +.emoji-2B1B { background-position: -25080px 0px; } +.emoji-2B1C { background-position: -25100px 0px; } +.emoji-2B50 { background-position: -25120px 0px; } +.emoji-2B55 { background-position: -25140px 0px; } +.emoji-3030 { background-position: -25160px 0px; } +.emoji-303D { background-position: -25180px 0px; } +.emoji-3297 { background-position: -25200px 0px; } +.emoji-3299 { background-position: -25220px 0px; }
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index dfb901652bf..282aaf2219b 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,7 +4,7 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding; + padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px); margin-left: -$gl-padding; margin-right: -$gl-padding; border-bottom: 1px solid $table-border-color; @@ -16,10 +16,7 @@ top: -2px; } - .event-title { - line-height: 44px; - } - + .event-title, .event-item-timestamp { line-height: 44px; } @@ -30,7 +27,7 @@ } .avatar { - margin-right: 15px; + margin-left: -($gl-avatar-size + 15px); } .event-title { @@ -43,8 +40,7 @@ } .event-body { - margin-left: 63px; - margin-right: 80px; + margin-right: 174px; .event-note { margin-top: 5px; @@ -155,6 +151,8 @@ @media (max-width: $screen-xs-max) { .event-item { + padding-left: $gl-padding; + .event-title { white-space: normal; overflow: visible; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 07a38a19fad..263993f59a5 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,8 +1,3 @@ -.new-group-member-holder { - margin-top: 50px; - padding-top: 20px; -} - .member-search-form { float: left; } @@ -15,4 +10,4 @@ .form-control { height: 42px; } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9da085a3473..9da273a0b6b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -24,20 +24,6 @@ } } -.issuable-context-title { - margin-bottom: 5px; - - .avatar { - margin-left: 0; - } - - label { - color: $gl-gray; - font-weight: normal; - margin-right: 4px; - } -} - .project-issuable-filter { .controls { float: right; @@ -50,33 +36,82 @@ } .issuable-details { - .page-title { - margin-top: -15px; - padding: 10px 0; - margin-bottom: 0; - color: #5c5d5e; - font-size: 16px; - - .author { - color: #5c5d5e; + section { + border-right: 1px solid $border-white-light; + + .issuable-discussion { + margin-right: 1px; } + } +} + +.issuable-filter-count { + span { + display: block; + margin-bottom: -16px; + padding: 13px 0; + } +} - .issue-id { - color: #5c5d5e; +.issuable-show-labels { + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; } } +} + +.issuable-sidebar { + .block { + @include clearfix; + padding: $gl-padding 0; + border-bottom: 1px solid #F0F0F0; - .issue-title { - margin: 0; - font-size: 23px; - color: #313236; + &:last-child { + border: none; + } } - .description { - margin-top: 6px; + .title { + color: $gl-text-color; + margin-bottom: 8px; - p:last-child { - margin-bottom: 0; + .avatar { + margin-left: 0; } + + label { + font-weight: normal; + margin-right: 4px; + } + + .edit-link { + color: $gl-gray; + } + } + + .cross-project-reference { + font-weight: bold; + color: $gl-link-color; + + button { + float: right; + } + } + + .selectbox { + display: none + } + + .btn-clipboard { + color: $gl-gray; + } + + .participants .avatar { + margin-top: 6px; + margin-right: 2px; } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 41c069f0ad3..a02a3a72e79 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -56,21 +56,30 @@ } } -.issue-show-labels { - a { - margin-right: 5px; - margin-bottom: 5px; - display: inline-block; - .color-label { - padding: 6px 10px; - } - } -} - form.edit-issue { margin: 0; } +.merge-requests-title { + font-size: 16px; + font-weight: 600; +} + +.merge-request-id { + display: inline-block; + width: 3em; +} + +.merge-request-info { + padding-left: 5px; +} + +.merge-request-status { + color: $gl-gray; + font-size: 15px; + font-weight: bold; +} + .merge-request, .issue { &.today { @@ -132,11 +141,6 @@ form.edit-issue { } } -.issue-closed-by-widget { - padding: 16px 0; - margin: 0px; -} - .issue-form .select2-container { width: 250px !important; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 83b866c3a64..f9c6f1b39f9 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -19,6 +19,7 @@ h1:first-child { font-weight: normal; margin-bottom: 30px; + margin-top: 0; } img { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index a1a5208c59c..82effde0bf3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -4,7 +4,6 @@ */ .mr-state-widget { background: #F7F8FA; - margin-bottom: 20px; color: $gl-gray; border: 1px solid #dce0e6; @include border-radius(2px); @@ -19,18 +18,34 @@ .accept-merge-holder { .accept-action { display: inline-block; + float: left; + + .accept_merge_request { + &.ci-pending, + &.ci-running { + @include btn-orange; + } + + &.ci-skipped, + &.ci-failed, + &.ci-canceled, + &.ci-error { + @include btn-red; + } + } } .accept-control { display: inline-block; + float: left; margin: 0; margin-left: 20px; padding: 5px; + padding-top: 12px; line-height: 20px; &.right { float: right; - padding-top: 12px; a { color: $gl-gray; } @@ -68,12 +83,16 @@ &.ci-error { color: $gl-danger; } + + a.monospace { + color: inherit; + } } .mr-widget-body, .ci_widget, .mr-widget-footer { - padding: 15px; + padding: $gl-padding; } .normal { @@ -102,28 +121,6 @@ } } -.merge-request .merge-request-tabs { - @include nav-menu; - margin: -$gl-padding; - padding: $gl-padding; - text-align: center; - margin-bottom: 1px; -} - -// Mobile -@media (max-width: 480px) { - .merge-request .merge-request-tabs { - margin: 0; - padding: 0; - - li { - a { - padding: 0; - } - } - } -} - .mr_source_commit, .mr_target_commit { .commit { @@ -141,7 +138,7 @@ font-family: $monospace_font; font-weight: bold; overflow: hidden; - font-size: 14px; + font-size: 90%; margin: 0 3px; } @@ -178,33 +175,27 @@ line-height: 1.1; } -.merge-request-form-info { - padding-top: 15px; -} - // hide mr close link for inline diff comment form .diff-file .close-mr-link, .diff-file .reopen-mr-link { display: none; } -.merge-request-show-labels { - a { - margin-right: 5px; - margin-bottom: 5px; - display: inline-block; - .color-label { - padding: 6px 10px; - } - } -} - .merge-request-form .select2-container { width: 250px !important; } #modal_merge_info .modal-dialog { width: 600px; + + .btn-clipboard { + @extend .pull-right; + + margin-right: 20px; + margin-top: 5px; + position: absolute; + right: 0; + } } .mr-source-target { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4392f08942b..d86259f93fb 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -7,6 +7,7 @@ } .reply-btn { @extend .btn-primary; + margin: 10px $gl-padding; } .diff-file .diff-content { tr.line_holder:hover { @@ -38,9 +39,8 @@ } .new_note, .edit_note { - .buttons { - margin-top: 8px; - margin-bottom: 3px; + .note-form-actions { + margin-top: $gl-padding; } .note-preview-holder { @@ -56,6 +56,10 @@ .note_text { width: 100%; } + + .comment-hints { + margin-top: -12px; + } } /* loading indicator */ @@ -71,17 +75,15 @@ .common-note-form { margin: 0; - background: #F7F8FA; + background: #fff; padding: $gl-padding; margin-left: -$gl-padding; margin-right: -$gl-padding; - border-right: 1px solid #ECEEF1; - border-top: 1px solid #ECEEF1; margin-bottom: -$gl-padding; } .note-form-actions { - background: #F9F9F9; + background: #fff; .note-form-option { margin-top: 8px; @@ -146,7 +148,6 @@ .discussion-reply-holder { background: $background-color; - padding: 10px 15px; border-top: 1px solid $border-color; } } @@ -168,7 +169,7 @@ color: #999; background: #FFF; padding: 7px; - margin-top: -11px; + margin-top: -7px; border: 1px solid $border-color; font-size: 13px; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 1980fe0d458..72b0ed29a69 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -109,13 +109,9 @@ ul.notes { } } - // Reduce left padding of first task list ul element - ul.task-list:first-child { - padding-left: 10px; - - // sub-tasks should be padded normally - ul { - padding-left: 20px; + ul.task-list { + ul:not(.task-list) { + padding-left: 1.3em; } } @@ -132,7 +128,7 @@ ul.notes { } &:last-child { - border-bottom: none; + border-bottom: 1px solid $border-color; } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index b7391e5303b..95fc26a608a 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -5,12 +5,6 @@ } } -.btn-build-token { - float: left; - padding: 6px 20px; - margin-right: 12px; -} - .profile-avatar-form-option { hr { margin: 10px 0; @@ -53,3 +47,25 @@ float: right; font-size: 12px; } + +.profile-link-holder { + display: inline; + + &:after { + content: "\00B7"; + padding: 0px 6px; + font-weight: bold; + } + + &:last-child { + &:after { + content: ""; + padding: 0; + } + } + + a { + color: $blue-dark; + text-decoration: none; + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 41bea0ec5c8..be6ef43e49c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -5,7 +5,7 @@ font-weight: normal; } } -.no-ssh-key-message { +.no-ssh-key-message, .project-limit-message { background-color: #f28d35; margin-bottom: 16px; } @@ -18,10 +18,6 @@ } } -.project-edit-content { - padding: 7px; -} - .project-name-holder { .help-inline { vertical-align: top; @@ -30,11 +26,12 @@ } .project-home-panel { - text-align: center; - background: #f7f8fa; - margin: -$gl-padding; - padding: $gl-padding; - padding: 44px 0 17px 0; + + .cover-controls { + .project-settings-dropdown { + margin-left: 10px; + } + } .project-identicon-holder { margin-bottom: 16px; @@ -50,7 +47,17 @@ } .project-home-dropdown { - margin: 11px 3px 0; + margin: 13px 0px 0; + } + + .notifications-btn { + .fa-bell { + margin-right: 6px; + } + + .fa-angle-down { + margin-left: 6px; + } } .project-home-desc { @@ -80,27 +87,94 @@ } .visibility-level-label { + @extend .btn; + @extend .btn-gray; + color: $gray; + cursor: default; + i { color: inherit; } } - .input-group { + + .git-clone-holder { display: inline-table; position: relative; - top: 17px; - margin-bottom: 44px; } .project-repo-buttons { margin-top: 12px; margin-bottom: 0px; + .count-buttons { + display: block; + margin-bottom: 12px; + } + .btn { @include btn-gray; - + text-transform: none; + } + .count-with-arrow { + display: inline-block; + position: relative; + margin-left: 4px; + + .arrow { + &:before { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 50%; + left: 0; + margin-top: -6px; + border-width: 7px 5px 7px 0; + border-right-color: #dce0e5; + } + + &:after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: 50%; + left: 1px; + margin-top: -9px; + border-width: 10px 7px 10px 0; + border-right-color: #FFF; + } + } .count { + @include btn-gray; display: inline-block; + background: white; + border-radius: 2px; + border-width: 1px; + border-style: solid; + font-size: 13px; + font-weight: 600; + line-height: 20px; + padding: 11px 16px; + letter-spacing: .4px; + padding: 10px; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + background-image: none; + white-space: nowrap; + margin: 0 11px 0px 4px; + + &:hover { + background: #FFF; + } } } } @@ -120,6 +194,13 @@ margin-right: 45px; } + .clone-options { + display: table-cell; + a.btn { + width: 100%; + } + } + .form-control { cursor: auto; @extend .monospace; @@ -167,6 +248,11 @@ &:active { outline: none; } + + &.btn-clipboard { + padding-left: 15px; + padding-right: 15px; + } } .active { @@ -233,23 +319,11 @@ } } - .fa-fw { + i { margin-right: 8px; } } -.fa-bell { - margin-right: 6px; -} - -.fa-angle-down { - margin-left: 6px; -} - -.project-home-panel .project-home-dropdown { - margin: 13px 0px 0; -} - .project-visibility-level-holder { .radio { margin-bottom: 10px; @@ -337,6 +411,38 @@ ul.nav.nav-projects-tabs { } } +.top-area { + border-bottom: 1px solid #EEE; + margin: 0 -16px; + padding: 0 $gl-padding; + + ul.left-top-menu { + display: inline-block; + width: 50%; + margin-bottom: 0px; + border-bottom: none; + } + + .projects-search-form { + width: 50%; + display: inline-block; + float: right; + padding-top: 7px; + text-align: right; + + .btn-green { + margin-top: -2px; + margin-left: 10px; + } + } + + @media (max-width: $screen-xs-max) { + .projects-search-form { + padding-top: 15px; + } + } +} + .fork-namespaces { .fork-thumbnail { text-align: center; @@ -367,7 +473,7 @@ table.table.protected-branches-list tr.no-border { .project-stats { text-align: center; - margin-top: 15px; + margin-top: $gl-padding; margin-bottom: 0; padding-top: 10px; padding-bottom: 4px; @@ -414,11 +520,18 @@ pre.light-well { .projects-search-form { margin: -$gl-padding; - background-color: #f8fafc; padding: $gl-padding; margin-bottom: 0px; - border-top: 1px solid #e7e9ed; - border-bottom: 1px solid #e7e9ed; + + input { + display: inline-block; + width: calc(100% - 151px); + } + + .btn { + display: inline-block; + width: 135px; + } } .git-empty { @@ -544,5 +657,13 @@ pre.light-well { } .project-show-readme .readme-holder { + margin-left: -$gl-padding; + margin-right: -$gl-padding; + padding: ($gl-padding + 7px); border-top: 0; + + .edit-project-readme { + z-index: 100; + position: relative; + } } diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 2b15ab83129..a9111a7388f 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -1,36 +1,34 @@ -.ci-body { - .runner-state { - padding: 6px 12px; - margin-right: 10px; - color: #FFF; +.runner-state { + padding: 6px 12px; + margin-right: 10px; + color: #FFF; - &.runner-state-shared { - background: #32b186; - } - &.runner-state-specific { - background: #3498db; - } + &.runner-state-shared { + background: #32b186; } - - .runner-status-online { - color: green; + &.runner-state-specific { + background: #3498db; } +} - .runner-status-offline { - color: gray; - } +.runner-status-online { + color: green; +} - .runner-status-paused { - color: red; - } +.runner-status-offline { + color: gray; +} + +.runner-status-paused { + color: red; +} - .runner { - .btn { - padding: 1px 6px; - } +.runner { + .btn { + padding: 1px 6px; + } - h4 { - font-weight: normal; - } + h4 { + font-weight: normal; } } diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss new file mode 100644 index 00000000000..92d84d9640f --- /dev/null +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -0,0 +1,33 @@ +table .sherlock-code { + max-width: 700px; +} + +.sherlock-code { + pre { + word-wrap: normal; + } + + pre code { + white-space: pre; + } +} + +.sherlock-line-samples-table { + margin-bottom: 0px !important; + + thead tr th, + tbody tr td { + font-size: 13px !important; + text-align: right; + padding: 0px 10px !important; + } +} + +.sherlock-file-sample pre { + padding-top: 28px !important; +} + +.sherlock-line-samples-table .slow { + color: $red-light; + font-weight: bold; +} diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index a3d7aba054d..1430d01859d 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -1,8 +1,3 @@ -.my-snippets li:first-child { - h4 { margin-top: 0; } - padding-top: 0; -} - .snippet-form-holder .file-holder .file-title { padding: 2px; } @@ -30,3 +25,30 @@ } } } + +.snippet-holder { + margin-bottom: -$gl-padding; + + .file-holder { + border-top: 0; + } + + .file-actions { + .btn-clipboard { + @extend .btn; + } + } +} + +.snippet-box { + @include border-radius(2px); + + display: block; + float: left; + padding: 0 $gl-padding; + font-weight: normal; + margin-right: 10px; + font-size: $gl-font-size; + border: 1px solid; + line-height: 40px; +} diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index a7d3b2197f1..4b6ef035673 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -35,3 +35,20 @@ border-color: $gl-warning; } } + +.ci-status-icon-success { + @extend .cgreen; +} +.ci-status-icon-failed { + @extend .cred; +} +.ci-status-icon-running, +.ci-status-icon-pending { + // These are standard text color +} +.ci-status-icon-canceled, +.ci-status-icon-disabled, +.ci-status-icon-not-found, +.ci-status-icon-skipped { + @extend .cgray; +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 1b0cef481d6..d4ab6967ccd 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -5,7 +5,7 @@ tr { > td, > th { - line-height: 32px; + line-height: 28px; } &:hover { diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 277afa1db9e..185f3622e64 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -1,9 +1,6 @@ .gitlab-ui-dev-kit { > h2 { - font-size: 27px; - border-bottom: 1px solid #CCC; - color: #666; - margin: 30px 0; + margin: 35px 0 20px; font-weight: bold; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index dfaeba41cf6..cdf514197cb 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -4,3 +4,8 @@ margin-right: auto; padding-right: 7px; } + +.wiki-last-edit-by { + font-size: 80%; + font-weight: normal; +} diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 2f4054eaa11..38814459f66 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -9,12 +9,10 @@ class AbuseReportsController < ApplicationController @abuse_report.reporter = current_user if @abuse_report.save - if current_application_settings.admin_notification_email.present? - AbuseReportMailer.delay.notify(@abuse_report.id) - end + @abuse_report.notify message = "Thank you for your report. A GitLab administrator will look into it shortly." - redirect_to root_path, notice: message + redirect_to @abuse_report.user, notice: message else render :new end @@ -23,6 +21,9 @@ class AbuseReportsController < ApplicationController private def report_params - params.require(:abuse_report).permit(:user_id, :message) + params.require(:abuse_report).permit(%i( + message + user_id + )) end end diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 56e24386463..9083bfb41cf 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -8,4 +8,10 @@ class Admin::ApplicationController < ApplicationController def authenticate_admin! return render_404 unless current_user.is_admin? end + + def authorize_impersonator! + if session[:impersonator_id] + User.find_by!(username: session[:impersonator_id]).admin? + end + end end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 039f18f23e0..10e736fd362 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -13,6 +13,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + def reset_runners_token + @application_setting.reset_runners_registration_token! + flash[:notice] = 'New runners registration token has been generated!' + redirect_to admin_runners_path + end + private def set_application_setting @@ -43,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :default_branch_protection, :signup_enabled, :signin_enabled, + :require_two_factor_authentication, + :two_factor_grace_period, :gravatar_enabled, :twitter_sharing_enabled, :sign_in_text, @@ -57,6 +65,19 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :version_check_enabled, :admin_notification_email, :user_oauth_applications, + :shared_runners_enabled, + :max_artifacts_size, + :metrics_enabled, + :metrics_host, + :metrics_port, + :metrics_username, + :metrics_password, + :metrics_pool_size, + :metrics_timeout, + :metrics_method_call_threshold, + :recaptcha_enabled, + :recaptcha_site_key, + :recaptcha_private_key, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb new file mode 100644 index 00000000000..0db91eaaf2e --- /dev/null +++ b/app/controllers/admin/builds_controller.rb @@ -0,0 +1,23 @@ +class Admin::BuildsController < Admin::ApplicationController + def index + @scope = params[:scope] + @all_builds = Ci::Build + @builds = @all_builds.order('created_at DESC') + @builds = + case @scope + when 'running' + @builds.running_or_pending.reverse_order + when 'finished' + @builds.finished + else + @builds + end + @builds = @builds.page(params[:page]).per(30) + end + + def cancel_all + Ci::Build.running_or_pending.each(&:cancel) + + redirect_to admin_builds_path + end +end diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index d28614731f9..e383fe38ea6 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -1,6 +1,21 @@ class Admin::IdentitiesController < Admin::ApplicationController before_action :user - before_action :identity, except: :index + before_action :identity, except: [:index, :new, :create] + + def new + @identity = Identity.new + end + + def create + @identity = Identity.new(identity_params) + @identity.user_id = user.id + + if @identity.save + redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully created.' + else + render :new + end + end def index @identities = @user.identities diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb new file mode 100644 index 00000000000..bf98af78615 --- /dev/null +++ b/app/controllers/admin/impersonation_controller.rb @@ -0,0 +1,38 @@ +class Admin::ImpersonationController < Admin::ApplicationController + skip_before_action :authenticate_admin!, only: :destroy + + before_action :user + before_action :authorize_impersonator! + + def create + if @user.blocked? + flash[:alert] = "You cannot impersonate a blocked user" + + redirect_to admin_user_path(@user) + else + session[:impersonator_id] = current_user.username + session[:impersonator_return_to] = admin_user_path(@user) + + warden.set_user(user, scope: 'user') + + flash[:alert] = "You are impersonating #{user.username}." + + redirect_to root_path + end + end + + def destroy + redirect = session[:impersonator_return_to] + + warden.set_user(user, scope: 'user') + + session[:impersonator_return_to] = nil + session[:impersonator_id] = nil + + redirect_to redirect || root_path + end + + def user + @user ||= User.find_by!(username: params[:id] || session[:impersonator_id]) + end +end diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb new file mode 100644 index 00000000000..d25619d94e0 --- /dev/null +++ b/app/controllers/admin/runner_projects_controller.rb @@ -0,0 +1,35 @@ +class Admin::RunnerProjectsController < Admin::ApplicationController + before_action :project, only: [:create] + + def index + @runner_projects = project.runner_projects.all + @runner_project = project.runner_projects.new + end + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + if @runner.assign_to(@project, current_user) + redirect_to admin_runner_path(@runner) + else + redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project' + end + end + + def destroy + rp = Ci::RunnerProject.find(params[:id]) + runner = rp.runner + rp.destroy + + redirect_to admin_runner_path(runner) + end + + private + + def project + @project = Project.find_with_namespace( + [params[:namespace_id], '/', params[:project_id]].join('') + ) + @project || render_404 + end +end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb new file mode 100644 index 00000000000..a701d49b844 --- /dev/null +++ b/app/controllers/admin/runners_controller.rb @@ -0,0 +1,63 @@ +class Admin::RunnersController < Admin::ApplicationController + before_action :runner, except: :index + + def index + @runners = Ci::Runner.order('id DESC') + @runners = @runners.search(params[:search]) if params[:search].present? + @runners = @runners.page(params[:page]).per(30) + @active_runners_cnt = Ci::Runner.online.count + end + + def show + @builds = @runner.builds.order('id DESC').first(30) + @projects = + if params[:search].present? + ::Project.search(params[:search]) + else + Project.all + end + @projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any? + @projects = @projects.page(params[:page]).per(30) + end + + def update + @runner.update_attributes(runner_params) + + respond_to do |format| + format.js + format.html { redirect_to admin_runner_path(@runner) } + end + end + + def destroy + @runner.destroy + + redirect_to admin_runners_path + end + + def resume + if @runner.update_attributes(active: true) + redirect_to admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to admin_runners_path, alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to admin_runners_path, alert: 'Runner was not updated.' + end + end + + private + + def runner + @runner ||= Ci::Runner.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:token, :description, :tag_list, :active) + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index c63d0793e31..d7c927d444c 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -63,12 +63,6 @@ class Admin::UsersController < Admin::ApplicationController end end - def login_as - sign_in(user) - flash[:alert] = "Logged in as #{user.username}" - redirect_to root_path - end - def disable_two_factor user.disable_two_factor! redirect_to admin_user_path(user), diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 865deb7d46a..d9a37a4d45f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,8 +10,10 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_token! before_action :authenticate_user! + before_action :validate_user_service_ticket! before_action :reject_blocked! before_action :check_password_expiration + before_action :check_2fa_requirement before_action :ldap_security_check before_action :default_headers before_action :add_gon_variables @@ -59,13 +61,8 @@ class ApplicationController < ActionController::Base end def authenticate_user!(*args) - # If user is not signed-in and tries to access root_path - redirect him to landing page - # Don't redirect to the default URL to prevent endless redirections - if current_application_settings.home_page_url.present? && - current_application_settings.home_page_url.chomp('/') != Gitlab.config.gitlab['url'].chomp('/') - if current_user.nil? && root_path == request.path - redirect_to current_application_settings.home_page_url and return - end + if redirect_to_home_page_url? + redirect_to current_application_settings.home_page_url and return end super(*args) @@ -124,7 +121,6 @@ class ApplicationController < ActionController::Base project_path = "#{namespace}/#{id}" @project = Project.find_with_namespace(project_path) - if @project and can?(current_user, :read_project, @project) if @project.path_with_namespace != project_path redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return @@ -208,12 +204,32 @@ class ApplicationController < ActionController::Base end end + def validate_user_service_ticket! + return unless signed_in? && session[:service_tickets] + + valid = session[:service_tickets].all? do |provider, ticket| + Gitlab::OAuth::Session.valid?(provider, ticket) + end + + unless valid + session[:service_tickets] = nil + sign_out current_user + redirect_to new_user_session_path + end + end + def check_password_expiration if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return end end + def check_2fa_requirement + if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? + redirect_to new_profile_two_factor_auth_path + end + end + def ldap_security_check if current_user && current_user.requires_ldap_check? unless Gitlab::LDAP::Access.allowed?(current_user) @@ -347,4 +363,34 @@ class ApplicationController < ActionController::Base def git_import_enabled? current_application_settings.import_sources.include?('git') end + + def two_factor_authentication_required? + current_application_settings.require_two_factor_authentication + end + + def two_factor_grace_period + current_application_settings.two_factor_grace_period + end + + def two_factor_grace_period_expired? + date = current_user.otp_grace_period_started_at + date && (date + two_factor_grace_period.hours) < Time.current + end + + def skip_two_factor? + session[:skip_tfa] && session[:skip_tfa] > Time.current + end + + def redirect_to_home_page_url? + # If user is not signed-in and tries to access root_path - redirect him to landing page + # Don't redirect to the default URL to prevent endless redirections + return false unless current_application_settings.home_page_url.present? + + home_page_url = current_application_settings.home_page_url.chomp('/') + root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] + + return false if root_urls.include?(home_page_url) + + current_user.nil? && root_path == request.path + end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 202e9da9eee..77c8dafc012 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,41 +1,15 @@ class AutocompleteController < ApplicationController skip_before_action :authenticate_user!, only: [:users] + before_action :find_users, only: [:users] def users - begin - @users = - if params[:project_id].present? - project = Project.find(params[:project_id]) - - if can?(current_user, :read_project, project) - project.team.users - end - elsif params[:group_id] - group = Group.find(params[:group_id]) - - if can?(current_user, :read_group, group) - group.users - end - elsif current_user - User.all - end - rescue ActiveRecord::RecordNotFound - if current_user - return render json: {}, status: 404 - end - end - - if @users.nil? && current_user.nil? - authenticate_user! - end - @users ||= User.none @users = @users.search(params[:search]) if params[:search].present? @users = @users.active @users = @users.reorder(:name) @users = @users.page(params[:page]).per(PER_PAGE) - unless params[:search].present? + if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user] && current_user @users = [*@users, current_user].uniq @@ -49,4 +23,25 @@ class AutocompleteController < ApplicationController @user = User.find(params[:id]) render json: @user, only: [:name, :username, :id], methods: [:avatar_url] end + + private + + def find_users + @users = + if params[:project_id].present? + project = Project.find(params[:project_id]) + return render_404 unless can?(current_user, :read_project, project) + + project.team.users + elsif params[:group_id].present? + group = Group.find(params[:group_id]) + return render_404 unless can?(current_user, :read_group, group) + + group.users + elsif current_user + User.all + else + User.none + end + end end diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb deleted file mode 100644 index 4ec2dc9c2cf..00000000000 --- a/app/controllers/ci/admin/application_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Ci - module Admin - class ApplicationController < Ci::ApplicationController - before_action :authenticate_user! - before_action :authenticate_admin! - - layout "ci/admin" - end - end -end diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb deleted file mode 100644 index 71e253fac67..00000000000 --- a/app/controllers/ci/admin/application_settings_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Ci - class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController - before_action :set_application_setting - - def show - end - - def update - if @application_setting.update_attributes(application_setting_params) - redirect_to ci_admin_application_settings_path, - notice: 'Application settings saved successfully' - else - render :show - end - end - - private - - def set_application_setting - @application_setting = Ci::ApplicationSetting.current - @application_setting ||= Ci::ApplicationSetting.create_from_defaults - end - - def application_setting_params - params.require(:application_setting).permit( - :all_broken_builds, - :add_pusher, - ) - end - end -end diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb deleted file mode 100644 index 38abfdeafbf..00000000000 --- a/app/controllers/ci/admin/builds_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Ci - class Admin::BuildsController < Ci::Admin::ApplicationController - def index - @scope = params[:scope] - @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30) - - @builds = - case @scope - when "pending" - @builds.pending - when "running" - @builds.running - else - @builds - end - end - end -end diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb deleted file mode 100644 index 5939efff980..00000000000 --- a/app/controllers/ci/admin/events_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Ci - class Admin::EventsController < Ci::Admin::ApplicationController - EVENTS_PER_PAGE = 50 - - def index - @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE) - end - end -end diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb deleted file mode 100644 index 5bbd0ce7396..00000000000 --- a/app/controllers/ci/admin/projects_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class Admin::ProjectsController < Ci::Admin::ApplicationController - def index - @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30) - end - - def destroy - project.destroy - - redirect_to ci_projects_url - end - - protected - - def project - @project ||= Ci::Project.find(params[:id]) - end - end -end diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb deleted file mode 100644 index e7de6eb12ca..00000000000 --- a/app/controllers/ci/admin/runner_projects_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Ci - class Admin::RunnerProjectsController < Ci::Admin::ApplicationController - layout 'ci/project' - - def index - @runner_projects = project.runner_projects.all - @runner_project = project.runner_projects.new - end - - def create - @runner = Ci::Runner.find(params[:runner_project][:runner_id]) - - if @runner.assign_to(project, current_user) - redirect_to ci_admin_runner_path(@runner) - else - redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project' - end - end - - def destroy - rp = Ci::RunnerProject.find(params[:id]) - runner = rp.runner - rp.destroy - - redirect_to ci_admin_runner_path(runner) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb deleted file mode 100644 index 110954a612d..00000000000 --- a/app/controllers/ci/admin/runners_controller.rb +++ /dev/null @@ -1,72 +0,0 @@ -module Ci - class Admin::RunnersController < Ci::Admin::ApplicationController - before_action :runner, except: :index - - def index - @runners = Ci::Runner.order('id DESC') - @runners = @runners.search(params[:search]) if params[:search].present? - @runners = @runners.page(params[:page]).per(30) - @active_runners_cnt = Ci::Runner.online.count - end - - def show - @builds = @runner.builds.order('id DESC').first(30) - @projects = Ci::Project.all - if params[:search].present? - @gl_projects = ::Project.search(params[:search]) - @projects = @projects.where(gitlab_id: @gl_projects.select(:id)) - end - @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any? - @projects = @projects.page(params[:page]).per(30) - end - - def update - @runner.update_attributes(runner_params) - - respond_to do |format| - format.js - format.html { redirect_to ci_admin_runner_path(@runner) } - end - end - - def destroy - @runner.destroy - - redirect_to ci_admin_runners_path - end - - def resume - if @runner.update_attributes(active: true) - redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' - else - redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' - end - end - - def pause - if @runner.update_attributes(active: false) - redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' - else - redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' - end - end - - def assign_all - Ci::Project.unassigned(@runner).all.each do |project| - @runner.assign_to(project, current_user) - end - - redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects" - end - - private - - def runner - @runner ||= Ci::Runner.find(params[:id]) - end - - def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :active) - end - end -end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb index 9be470660e6..c420b59c3a2 100644 --- a/app/controllers/ci/application_controller.rb +++ b/app/controllers/ci/application_controller.rb @@ -4,32 +4,16 @@ module Ci "app/helpers/ci" end - helper_method :gl_project - private - def authenticate_public_page! - unless project.public - authenticate_user! - - return access_denied! unless can?(current_user, :read_project, gl_project) - end - end - - def authenticate_token! - unless project.valid_token?(params[:token]) - return head(403) - end - end - def authorize_access_project! - unless can?(current_user, :read_project, gl_project) + unless can?(current_user, :read_project, project) return page_404 end end def authorize_manage_builds! - unless can?(current_user, :manage_builds, gl_project) + unless can?(current_user, :manage_builds, project) return page_404 end end @@ -39,7 +23,7 @@ module Ci end def authorize_manage_project! - unless can?(current_user, :admin_project, gl_project) + unless can?(current_user, :admin_project, project) return page_404 end end @@ -66,9 +50,5 @@ module Ci count: count } end - - def gl_project - ::Project.find(@project.gitlab_id) - end end end diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb deleted file mode 100644 index 89b784a1e89..00000000000 --- a/app/controllers/ci/events_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Ci - class EventsController < Ci::ApplicationController - EVENTS_PER_PAGE = 50 - - before_action :authenticate_user! - before_action :project - before_action :authorize_manage_project! - - layout 'ci/project' - - def index - @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 24dd1b5c93a..e782a51e7eb 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,5 +1,5 @@ module Ci - class LintsController < Ci::ApplicationController + class LintsController < ApplicationController before_action :authenticate_user! def show @@ -15,12 +15,14 @@ module Ci @builds = @config_processor.builds @status = true end - rescue Ci::GitlabCiYamlProcessor::ValidationError => e + rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e @error = e.message @status = false - rescue Exception - @error = "Undefined error" + rescue + @error = 'Undefined error' @status = false + ensure + render :show end end end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 809b44387ba..3004c2d27f0 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -3,13 +3,12 @@ module Ci before_action :project, except: [:index] before_action :authenticate_user!, except: [:index, :build, :badge] before_action :authorize_access_project!, except: [:index, :badge] - before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml] before_action :no_cache, only: [:badge] protect_from_forgery def show # Temporary compatibility with CI badges pointing to CI project page - redirect_to namespace_project_path(project.gl_project.namespace, project.gl_project) + redirect_to namespace_project_path(project.namespace, project) end # Project status badge @@ -20,20 +19,10 @@ module Ci send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" end - def toggle_shared_runners - project.toggle!(:shared_runners_enabled) - - redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project) - end - - def dumped_yaml - send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml' - end - protected def project - @project ||= Ci::Project.find(params[:id]) + @project ||= Project.find_by(ci_id: params[:id].to_i) end def no_cache diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb deleted file mode 100644 index 97f01d40af5..00000000000 --- a/app/controllers/ci/runner_projects_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - class RunnerProjectsController < Ci::ApplicationController - before_action :authenticate_user! - before_action :project - before_action :authorize_manage_project! - - layout 'ci/project' - - def create - @runner = Ci::Runner.find(params[:runner_project][:runner_id]) - - return head(403) unless current_user.ci_authorized_runners.include?(@runner) - - path = runners_path(@project.gl_project) - - if @runner.assign_to(project, current_user) - redirect_to path - else - redirect_to path, alert: 'Failed adding runner to project' - end - end - - def destroy - runner_project = project.runner_projects.find(params[:id]) - runner_project.destroy - - redirect_to runners_path(@project.gl_project) - end - - private - - def project - @project ||= Ci::Project.find(params[:project_id]) - end - end -end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb new file mode 100644 index 00000000000..62127a09081 --- /dev/null +++ b/app/controllers/concerns/creates_commit.rb @@ -0,0 +1,103 @@ +module CreatesCommit + extend ActiveSupport::Concern + + def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) + set_commit_variables + + commit_params = @commit_params.merge( + source_project: @project, + source_branch: @ref, + target_branch: @target_branch + ) + + result = service.new(@tree_edit_project, current_user, commit_params).execute + + if result[:status] == :success + flash[:notice] = success_notice || "Your changes have been successfully committed." + + if create_merge_request? + success_path = new_merge_request_path + target = different_project? ? "project" : "branch" + flash[:notice] << " You can now submit a merge request to get this change into the original #{target}." + end + + respond_to do |format| + format.html { redirect_to success_path } + format.json { render json: { message: "success", filePath: success_path } } + end + else + flash[:alert] = result[:message] + respond_to do |format| + format.html do + if failure_view + render failure_view + else + redirect_to failure_path + end + end + format.json { render json: { message: "failed", filePath: failure_path } } + end + end + end + + def authorize_edit_tree! + return if can?(current_user, :push_code, project) + return if current_user && current_user.already_forked?(project) + + access_denied! + end + + private + + def new_merge_request_path + new_namespace_project_merge_request_path( + @mr_source_project.namespace, + @mr_source_project, + merge_request: { + source_project_id: @mr_source_project.id, + target_project_id: @mr_target_project.id, + source_branch: @mr_source_branch, + target_branch: @mr_target_branch + } + ) + end + + def different_project? + @mr_source_project != @mr_target_project + end + + def different_branch? + @mr_source_branch != @mr_target_branch || different_project? + end + + def create_merge_request? + params[:create_merge_request].present? && different_branch? + end + + def set_commit_variables + @mr_source_branch = @target_branch + + if can?(current_user, :push_code, @project) + # Edit file in this project + @tree_edit_project = @project + @mr_source_project = @project + + if @project.forked? + # Merge request from this project to fork origin + @mr_target_project = @project.forked_from_project + @mr_target_branch = @mr_target_project.repository.root_ref + else + # Merge request to this project + @mr_target_project = @project + @mr_target_branch = @ref + end + else + # Edit file in fork + @tree_edit_project = current_user.fork_of(@project) + # Merge request from fork to this project + @mr_source_project = @tree_edit_project + @mr_target_project = @project + @mr_target_branch = @mr_target_project.repository.root_ref + end + end +end diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb new file mode 100644 index 00000000000..3e4c0e63601 --- /dev/null +++ b/app/controllers/concerns/global_milestones.rb @@ -0,0 +1,21 @@ +module GlobalMilestones + extend ActiveSupport::Concern + + def milestones + epoch = DateTime.parse('1970-01-01') + @milestones = MilestonesFinder.new.execute(@projects, params) + @milestones = GlobalMilestone.build_collection(@milestones) + @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } + @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE) + end + + def milestone + milestones = Milestone.of_projects(@projects).where(title: params[:title]) + + if milestones.present? + @milestone = GlobalMilestone.new(params[:title], milestones) + else + render_404 + end + end +end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb new file mode 100644 index 00000000000..effd4721949 --- /dev/null +++ b/app/controllers/concerns/issues_action.rb @@ -0,0 +1,14 @@ +module IssuesAction + extend ActiveSupport::Concern + + def issues + @issues = get_issues_collection + @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE) + @issues = @issues.preload(:author, :project) + + respond_to do |format| + format.html + format.atom { render layout: false } + end + end +end diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb new file mode 100644 index 00000000000..f7a25111db9 --- /dev/null +++ b/app/controllers/concerns/merge_requests_action.rb @@ -0,0 +1,9 @@ +module MergeRequestsAction + extend ActiveSupport::Concern + + def merge_requests + @merge_requests = get_merge_requests_collection + @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE) + @merge_requests = @merge_requests.preload(:author, :target_project) + end +end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 53896d4f2c7..2bdce0f8a00 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,34 +1,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController - before_action :load_projects + include GlobalMilestones + + before_action :projects + before_action :milestones, only: [:index] + before_action :milestone, only: [:show] def index - project_milestones = case params[:state] - when 'all'; state - when 'closed'; state('closed') - else state('active') - end - @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute - @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE) end def show - project_milestones = Milestone.where(project_id: @projects).order("due_date ASC") - @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title) end private - def load_projects - @projects = current_user.authorized_projects.sorted_by_activity.non_archived - end - - def title - params[:title] - end - - def state(state = nil) - conditions = { project_id: @projects } - conditions.reverse_merge!(state: state) if state - Milestone.where(conditions).order("title ASC") + def projects + @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived end end diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index f4354c6d8ca..b3594d82530 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -1,6 +1,7 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController def index - @snippets = SnippetsFinder.new.execute(current_user, + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_user, user: current_user, scope: params[:scope] diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4ebb3d7276e..087da935087 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,25 +1,12 @@ class DashboardController < Dashboard::ApplicationController + include IssuesAction + include MergeRequestsAction + before_action :event_filter, only: :activity + before_action :projects, only: [:issues, :merge_requests] respond_to :html - def merge_requests - @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) - @merge_requests = @merge_requests.preload(:author, :target_project) - end - - def issues - @issues = get_issues_collection - @issues = @issues.page(params[:page]).per(PER_PAGE) - @issues = @issues.preload(:author, :project) - - respond_to do |format| - format.html - format.atom { render layout: false } - end - end - def activity @last_push = current_user.recent_push @@ -47,4 +34,8 @@ class DashboardController < Dashboard::ApplicationController @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end + + def projects + @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived + end end diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index 9575a87ee41..a9bf4321f73 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,6 +1,6 @@ class Explore::GroupsController < Explore::ApplicationController def index - @groups = GroupsFinder.new.execute(current_user) + @groups = Group.order_id_desc @groups = @groups.search(params[:search]) if params[:search].present? @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.page(params[:page]).per(PER_PAGE) diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 6878d4bc07e..be801858eaf 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -1,8 +1,13 @@ class Groups::ApplicationController < ApplicationController layout 'group' + before_action :group private - + + def group + @group ||= Group.find_by(path: params[:group_id]) + end + def authorize_read_group! unless @group and can?(current_user, :read_group, @group) if current_user.nil? @@ -12,13 +17,13 @@ class Groups::ApplicationController < ApplicationController end end end - + def authorize_admin_group! unless can?(current_user, :admin_group, group) return render_404 end end - + def authorize_admin_group_member! unless can?(current_user, :admin_group_member, group) return render_403 diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb index 6aa64222f77..76c87366baa 100644 --- a/app/controllers/groups/avatars_controller.rb +++ b/app/controllers/groups/avatars_controller.rb @@ -1,8 +1,6 @@ -class Groups::AvatarsController < ApplicationController +class Groups::AvatarsController < Groups::ApplicationController def destroy - @group = Group.find_by(path: params[:group_id]) @group.remove_avatar! - @group.save redirect_to edit_group_path(@group) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 91518c44a98..0e902c4bb43 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,11 +1,9 @@ class Groups::GroupMembersController < Groups::ApplicationController skip_before_action :authenticate_user!, only: [:index] - before_action :group # Authorize before_action :authorize_read_group! - before_action :authorize_admin_group!, except: [:index, :leave] - before_action :authorize_admin_group_member!, only: [:create, :resend_invite] + before_action :authorize_admin_group_member!, except: [:index, :leave] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController end @members = @members.order('access_level DESC').page(params[:page]).per(50) - @group_member = GroupMember.new + + @group_member = @group.group_members.new end def create @@ -28,24 +27,23 @@ class Groups::GroupMembersController < Groups::ApplicationController end def update - @member = @group.group_members.find(params[:id]) + @group_member = @group.group_members.find(params[:id]) - return render_403 unless can?(current_user, :update_group_member, @member) + return render_403 unless can?(current_user, :update_group_member, @group_member) - @member.update_attributes(member_params) + @group_member.update_attributes(member_params) end def destroy @group_member = @group.group_members.find(params[:id]) - if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. - @group_member.destroy - respond_to do |format| - format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } - end - else - return render_403 + return render_403 unless can?(current_user, :destroy_group_member, @group_member) + + @group_member.destroy + + respond_to do |format| + format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } + format.js { render nothing: true } end end @@ -64,10 +62,11 @@ class Groups::GroupMembersController < Groups::ApplicationController end def leave - @group_member = @group.group_members.where(user_id: current_user.id).first + @group_member = @group.group_members.find_by(user_id: current_user) if can?(current_user, :destroy_group_member, @group_member) @group_member.destroy + redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else if @group.last_owner?(current_user) @@ -80,10 +79,6 @@ class Groups::GroupMembersController < Groups::ApplicationController protected - def group - @group ||= Group.find_by(path: params[:group_id]) - end - def member_params params.require(:group_member).permit(:access_level, :user_id) end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 669f7f3126d..0c2a350bc39 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,54 +1,55 @@ class Groups::MilestonesController < Groups::ApplicationController - before_action :authorize_group_milestone!, only: :update + include GlobalMilestones + + before_action :projects + before_action :milestones, only: [:index] + before_action :milestone, only: [:show, :update] + before_action :authorize_group_milestone!, only: [:create, :update] def index - project_milestones = case params[:state] - when 'all'; state - when 'closed'; state('closed') - else state('active') - end - @group_milestones = Milestones::GroupService.new(project_milestones).execute - @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE) end - def show - project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC") - @group_milestone = Milestones::GroupService.new(project_milestones).milestone(title) + def new + @milestone = Milestone.new end - def update - project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC") - @group_milestones = Milestones::GroupService.new(project_milestones).milestone(title) + def create + project_ids = params[:milestone][:project_ids] + title = milestone_params[:title] - @group_milestones.milestones.each do |milestone| - Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone) + @group.projects.where(id: project_ids).each do |project| + Milestones::CreateService.new(project, current_user, milestone_params).execute end - respond_to do |format| - format.js - format.html do - redirect_to group_milestones_path(group) - end + redirect_to milestone_path(title) + end + + def show + end + + def update + @milestone.milestones.each do |milestone| + Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone) end + + redirect_back_or_default(default: milestone_path(@milestone.title)) end private - def group - @group ||= Group.find_by(path: params[:group_id]) + def authorize_group_milestone! + return render_404 unless can?(current_user, :admin_milestones, group) end - def title - params[:title] + def milestone_params + params.require(:milestone).permit(:title, :description, :due_date, :state_event) end - def state(state = nil) - conditions = { project_id: group.projects } - conditions.reverse_merge!(state: state) if state - Milestone.where(conditions).order("title ASC") + def milestone_path(title) + group_milestone_path(@group, title.to_slug.to_s, title: title) end - def authorize_group_milestone! - return render_404 unless can?(current_user, :admin_group, group) + def projects + @projects ||= @group.projects end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 40fb15a5b36..fb26a4e6fc3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,15 +1,18 @@ class GroupsController < Groups::ApplicationController + include IssuesAction + include MergeRequestsAction + skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html before_action :group, except: [:new, :create] # Authorize - before_action :authorize_read_group!, except: [:show, :new, :create] + before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects] before_action :authorize_create_group!, only: [:new, :create] # Load group projects - before_action :load_projects, except: [:new, :create, :projects, :edit, :update] + before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete] before_action :event_filter, only: :show layout :determine_layout @@ -53,23 +56,6 @@ class GroupsController < Groups::ApplicationController end end - def merge_requests - @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) - @merge_requests = @merge_requests.preload(:author, :target_project) - end - - def issues - @issues = get_issues_collection - @issues = @issues.page(params[:page]).per(PER_PAGE) - @issues = @issues.preload(:author, :project) - - respond_to do |format| - format.html - format.atom { render layout: false } - end - end - def edit end @@ -133,7 +119,7 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit(:name, :description, :path, :avatar) + params.require(:group).permit(:name, :description, :path, :avatar, :public) end def load_events diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f809fa7500a..4cad98b8e98 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,6 +1,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController - protect_from_forgery except: [:kerberos, :saml] + protect_from_forgery except: [:kerberos, :saml, :cas3] Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do @@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController render 'errors/omniauth_error', layout: "errors", status: 422 end + def cas3 + ticket = params['ticket'] + if ticket + handle_service_ticket oauth['provider'], ticket + end + handle_omniauth + end + private def handle_omniauth @@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to new_user_session_path end + def handle_service_ticket provider, ticket + Gitlab::OAuth::Session.create provider, ticket + session[:service_tickets] ||= {} + session[:service_tickets][provider] = ticket + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 2025158d065..f74daff3bd0 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -40,7 +40,9 @@ class PasswordsController < Devise::PasswordsController def throttle_reset return unless resource && resource.recently_sent_password_reset? - redirect_to new_password_path(resource_name), - alert: I18n.t('devise.passwords.recently_reset') + # Throttle reset attempts, but return a normal message to + # avoid user enumeration attack. + redirect_to new_user_session_path, + notice: I18n.t('devise.passwords.send_paranoid_instructions') end end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index e6b99be37fb..6e91d9b4ad9 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,8 +1,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController + skip_before_action :check_2fa_requirement + def new unless current_user.otp_secret current_user.otp_secret = User.generate_otp_secret(32) - current_user.save! + end + + unless current_user.otp_grace_period_started_at && two_factor_grace_period + current_user.otp_grace_period_started_at = Time.current + end + + current_user.save! if current_user.changed? + + if two_factor_grace_period_expired? + flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.' + else + grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}." end @qr_code = build_qr_code @@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController redirect_to profile_account_path end + def skip + if two_factor_grace_period_expired? + redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' + else + session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + redirect_to root_path + end + end + private def build_qr_code diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 8da7b4d50ea..28803164fcf 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -70,6 +70,7 @@ class ProfilesController < Profiles::ApplicationController :email, :hide_no_password, :hide_no_ssh_key, + :hide_project_limit, :linkedin, :location, :name, diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 519d6d6127e..dd32d509191 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -21,18 +21,14 @@ class Projects::ApplicationController < ApplicationController unless @repository.branch_names.include?(@ref) redirect_to( namespace_project_tree_path(@project.namespace, @project, @ref), - notice: "This action is not allowed unless you are on top of a branch" + notice: "This action is not allowed unless you are on a branch" ) end end private - def ci_enabled - return render_404 unless @project.gitlab_ci? - end - - def ci_project - @ci_project ||= @project.ensure_gitlab_ci_project + def builds_enabled + return render_404 unless @project.builds_enabled? end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 8cc2f21d887..c56a3497bb2 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,6 +1,7 @@ # Controller for viewing a file's blame class Projects::BlobController < Projects::ApplicationController include ExtractsPath + include CreatesCommit include ActionView::Helpers::SanitizeHelper # Raised when given an invalid file path @@ -8,35 +9,23 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:destroy, :create] + before_action :authorize_edit_tree!, only: [:new, :create, :edit, :update, :destroy] before_action :assign_blob_vars before_action :commit, except: [:new, :create] before_action :blob, except: [:new, :create] before_action :from_merge_request, only: [:edit, :update] before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] - before_action :after_edit_path, only: [:edit, :update] def new commit unless @repository.empty? end def create - result = Files::CreateService.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "The changes have been successfully committed" - respond_to do |format| - format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } - format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } } - end - else - flash[:alert] = result[:message] - respond_to do |format| - format.html { render :new } - format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } } - end - end + create_commit(Files::CreateService, success_notice: "The file has been successfully created.", + success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)), + failure_view: :new, + failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) end def show @@ -47,21 +36,17 @@ class Projects::BlobController < Projects::ApplicationController end def update - result = Files::UpdateService.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - respond_to do |format| - format.html { redirect_to after_edit_path } - format.json { render json: { message: "success", filePath: after_edit_path } } - end - else - flash[:alert] = result[:message] - respond_to do |format| - format.html { render :edit } - format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } } + after_edit_path = + if from_merge_request && @target_branch == @ref + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + + "#file-path-#{hexdigest(@path)}" + else + namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) end - end + + create_commit(Files::UpdateService, success_path: after_edit_path, + failure_view: :edit, + failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end def preview @@ -73,15 +58,10 @@ class Projects::BlobController < Projects::ApplicationController end def destroy - result = Files::DeleteService.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) - else - flash[:alert] = result[:message] - render :show - end + create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", + success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch), + failure_view: :show, + failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end def diff @@ -131,37 +111,20 @@ class Projects::BlobController < Projects::ApplicationController render_404 end - def after_edit_path - @after_edit_path ||= - if from_merge_request - diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" - elsif @target_branch.present? - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) - else - namespace_project_blob_path(@project.namespace, @project, @id) - end - end - def from_merge_request # If blob edit was initiated from merge request page @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) end - def sanitized_new_branch_name - @new_branch ||= sanitize(strip_tags(params[:new_branch])) - end - def editor_variables - @current_branch = @ref - @target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref + @target_branch = params[:target_branch] @file_path = if action_name.to_s == 'create' if params[:file].present? params[:file_name] = params[:file].original_filename end - File.join(@path, File.basename(params[:file_name])) + File.join(@path, params[:file_name]) else @path end @@ -173,8 +136,6 @@ class Projects::BlobController < Projects::ApplicationController @commit_params = { file_path: @file_path, - current_branch: @current_branch, - target_branch: @target_branch, commit_message: params[:commit_message], file_content: params[:content], file_content_encoding: params[:encoding] diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 3ac0a75fa70..4db3b3bf23d 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -3,12 +3,17 @@ class Projects::BranchesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:create, :destroy] + before_action :authorize_push_code!, only: [:new, :create, :destroy] def index @sort = params[:sort] || 'name' @branches = @repository.branches_sorted_by(@sort) @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) + + @max_commits = @branches.reduce(0) do |memo, branch| + diverging_commit_counts = repository.diverging_commit_counts(branch) + [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + end end def recent diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 816012762ce..39d3ba26ba2 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,35 +1,36 @@ class Projects::BuildsController < Projects::ApplicationController - before_action :ci_project before_action :build, except: [:index, :cancel_all] - before_action :authorize_admin_project!, except: [:index, :show, :status] + before_action :authorize_manage_builds!, except: [:index, :show, :status] + before_action :authorize_download_build_artifacts!, only: [:download] layout "project" def index @scope = params[:scope] - @all_builds = project.ci_builds + @all_builds = project.builds + @builds = @all_builds.order('created_at DESC') @builds = case @scope - when 'all' - @all_builds + when 'running' + @builds.running_or_pending.reverse_order when 'finished' - @all_builds.finished + @builds.finished else - @all_builds.running_or_pending + @builds end - @builds = @builds.order('created_at DESC').page(params[:page]).per(30) + @builds = @builds.page(params[:page]).per(30) end def cancel_all - @project.ci_builds.running_or_pending.each(&:cancel) + @project.builds.running_or_pending.each(&:cancel) redirect_to namespace_project_builds_path(project.namespace, project) end def show - @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC') - @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) + @builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @builds.where("id not in (?)", @build.id) @commit = @build.commit respond_to do |format| @@ -41,17 +42,25 @@ class Projects::BuildsController < Projects::ApplicationController end def retry - if @build.commands.blank? + unless @build.retryable? return page_404 end build = Ci::Build.retry(@build) - if params[:return_to] - redirect_to URI.parse(params[:return_to]).path - else - redirect_to build_path(build) + redirect_to build_path(build) + end + + def download + unless artifacts_file.file_storage? + return redirect_to artifacts_file.url + end + + unless artifacts_file.exists? + return not_found! end + + send_file artifacts_file.path, disposition: 'attachment' end def status @@ -67,10 +76,30 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) + @build ||= project.builds.unscoped.find_by!(id: params[:id]) + end + + def artifacts_file + build.artifacts_file end def build_path(build) - namespace_project_build_path(build.gl_project.namespace, build.gl_project, build) + namespace_project_build_path(build.project.namespace, build.project, build) + end + + def authorize_manage_builds! + unless can?(current_user, :manage_builds, project) + return page_404 + end + end + + def authorize_download_build_artifacts! + unless can?(current_user, :download_build_artifacts, @project) + if current_user.nil? + return authenticate_user! + else + return render_404 + end + end end end diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb deleted file mode 100644 index 406f313ae79..00000000000 --- a/app/controllers/projects/ci_services_controller.rb +++ /dev/null @@ -1,49 +0,0 @@ -class Projects::CiServicesController < Projects::ApplicationController - before_action :ci_project - before_action :authorize_admin_project! - - layout "project_settings" - - def index - @ci_project.build_missing_services - @services = @ci_project.services.reload - end - - def edit - service - end - - def update - if @service.update_attributes(service_params) - redirect_to edit_namespace_project_ci_service_path(@project, @project.namespace, @service.to_param) - else - render 'edit' - end - end - - def test - last_build = @project.builds.last - - if @service.execute(last_build) - message = { notice: 'We successfully tested the service' } - else - message = { alert: 'We tried to test the service but error occurred' } - end - - redirect_back_or_default(options: message) - end - - private - - def service - @service ||= @ci_project.services.find { |service| service.to_param == params[:id] } - end - - def service_params - params.require(:service).permit( - :type, :active, :webhook, :notify_only_broken_builds, - :email_recipients, :email_only_broken_builds, :email_add_pusher, - :hipchat_token, :hipchat_room, :hipchat_server - ) - end -end diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb deleted file mode 100644 index a263242a850..00000000000 --- a/app/controllers/projects/ci_settings_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Projects::CiSettingsController < Projects::ApplicationController - before_action :ci_project - before_action :authorize_admin_project! - - layout "project_settings" - - def edit - end - - def update - if ci_project.update_attributes(project_params) - Ci::EventService.new.change_project_settings(current_user, ci_project) - - redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.' - else - render action: "edit" - end - end - - def destroy - ci_project.destroy - Ci::EventService.new.remove_project(current_user, ci_project) - project.gitlab_ci_service.update_attributes(active: false) - - redirect_to project_path(project), notice: "CI was disabled for this project" - end - - protected - - def project_params - params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, - :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, - :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, - { variables_attributes: [:id, :key, :value, :_destroy] }) - end -end diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb deleted file mode 100644 index a2d470d4a69..00000000000 --- a/app/controllers/projects/ci_web_hooks_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Projects::CiWebHooksController < Projects::ApplicationController - before_action :ci_project - before_action :authorize_admin_project! - - layout "project_settings" - - def index - @web_hooks = @ci_project.web_hooks - @web_hook = Ci::WebHook.new - end - - def create - @web_hook = @ci_project.web_hooks.new(web_hook_params) - @web_hook.save - - if @web_hook.valid? - redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project) - else - @web_hooks = @ci_project.web_hooks.select(&:persisted?) - render :index - end - end - - def test - Ci::TestHookService.new.execute(hook, current_user) - - redirect_back_or_default(default: { action: 'index' }) - end - - def destroy - hook.destroy - - redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project) - end - - private - - def hook - @web_hook ||= @ci_project.web_hooks.find(params[:id]) - end - - def web_hook_params - params.require(:web_hook).permit(:url) - end -end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 7886f3c6deb..0aaba3792bf 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -4,16 +4,17 @@ class Projects::CommitController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code! + before_action :authorize_download_code!, except: [:cancel_builds] + before_action :authorize_manage_builds!, only: [:cancel_builds] before_action :commit + before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds] + before_action :define_show_vars, only: [:show, :builds] def show return git_not_found! unless @commit @line_notes = commit.notes.inline - @diffs = @commit.diffs @note = @project.build_commit_note(commit) - @notes_count = commit.notes.count @notes = commit.notes.not_inline.fresh @noteable = @commit @comments_allowed = @reply_allowed = true @@ -22,8 +23,6 @@ class Projects::CommitController < Projects::ApplicationController commit_id: @commit.id } - @ci_commit = project.ci_commit(commit.sha) - respond_to do |format| format.html format.diff { render text: @commit.to_diff } @@ -31,20 +30,24 @@ class Projects::CommitController < Projects::ApplicationController end end - def ci - @ci_commit = @project.ci_commit(@commit.sha) - @builds = @ci_commit.builds if @ci_commit - @notes_count = @commit.notes.count - @ci_project = @project.gitlab_ci_project + def builds end def cancel_builds - @ci_commit = @project.ci_commit(@commit.sha) - @ci_commit.builds.running_or_pending.each(&:cancel) + ci_commit.builds.running_or_pending.each(&:cancel) - redirect_to ci_namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end + def retry_builds + ci_commit.builds.latest.failed.each do |build| + if build.retryable? + Ci::Build.retry(build) + end + end + + redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) + end def branches @branches = @project.repository.branch_names_contains(commit.id) @@ -52,7 +55,31 @@ class Projects::CommitController < Projects::ApplicationController render layout: false end + private + def commit @commit ||= @project.commit(params[:id]) end + + def ci_commit + @ci_commit ||= project.ci_commit(commit.sha) + end + + def define_show_vars + if params[:w].to_i == 1 + @diffs = commit.diffs({ ignore_whitespace_change: true }) + else + @diffs = commit.diffs + end + + @notes_count = commit.notes.count + + @statuses = ci_commit.statuses if ci_commit + end + + def authorize_manage_builds! + unless can?(current_user, :manage_builds, project) + return page_404 + end + end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index d1c15174aea..04a88990bf4 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -9,10 +9,10 @@ class Projects::CommitsController < Projects::ApplicationController def show @repo = @project.repository - @limit, @offset = (params[:limit] || 40), (params[:offset] || 0) + @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i @commits = @repo.commits(@ref, @path, @limit, @offset) - @note_counts = Note.where(commit_id: @commits.map(&:id)). + @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count respond_to do |format| diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 71aaad1fad6..5200d609cc9 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -12,15 +12,16 @@ class Projects::CompareController < Projects::ApplicationController def show base_ref = Addressable::URI.unescape(params[:from]) @ref = head_ref = Addressable::URI.unescape(params[:to]) + diff_options = { ignore_whitespace_change: true } if params[:w] == '1' compare_result = CompareService.new. - execute(@project, head_ref, @project, base_ref) + execute(@project, head_ref, @project, base_ref, diff_options) if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs - @commit = @commits.last - @first_commit = @commits.first + @commit = @project.commit(head_ref) + @first_commit = @project.commit(base_ref) @line_notes = [] end end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 8a785076bb7..750181f0c19 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -10,19 +10,35 @@ class Projects::ForksController < Projects::ApplicationController def create namespace = Namespace.find(params[:namespace_key]) - @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute + + @forked_project = namespace.projects.find_by(path: project.path) + @forked_project = nil unless @forked_project && @forked_project.forked_from_project == project + + @forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute if @forked_project.saved? && @forked_project.forked? if @forked_project.import_in_progress? - redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project) + redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project, continue: continue_params) else - redirect_to( - namespace_project_path(@forked_project.namespace, @forked_project), - notice: 'Project was successfully forked.' - ) + if continue_params + redirect_to continue_params[:to], notice: continue_params[:notice] + else + redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked." + end end else render :error end end + + private + + def continue_params + continue_params = params[:continue] + if continue_params + continue_params.permit(:to, :notice, :notice_now) + else + nil + end + end end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 418b92040bc..d13ea9f34b6 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -5,7 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController before_action :require_non_empty_project before_action :assign_ref_vars before_action :authorize_download_code! - before_action :ci_enabled, only: :ci + before_action :builds_enabled, only: :ci def show respond_to do |format| @@ -25,13 +25,31 @@ class Projects::GraphsController < Projects::ApplicationController end def ci - ci_project = @project.gitlab_ci_project - @charts = {} - @charts[:week] = Ci::Charts::WeekChart.new(ci_project) - @charts[:month] = Ci::Charts::MonthChart.new(ci_project) - @charts[:year] = Ci::Charts::YearChart.new(ci_project) - @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project) + @charts[:week] = Ci::Charts::WeekChart.new(project) + @charts[:month] = Ci::Charts::MonthChart.new(project) + @charts[:year] = Ci::Charts::YearChart.new(project) + @charts[:build_times] = Ci::Charts::BuildTime.new(project) + end + + def languages + @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages + total = @languages.map(&:last).sum + + @languages = @languages.map do |language| + name, share = language + color = Digest::SHA256.hexdigest(name)[0...6] + { + value: (share.to_f * 100 / total).round(2), + label: name, + color: "##{color}", + highlight: "##{color}" + } + end + + @languages.sort! do |x, y| + y[:value] <=> x[:value] + end end private diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index c7569541899..5fd4f855dec 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController def test if !@project.empty_repo? - status = TestHookService.new.execute(hook, current_user) + status, message = TestHookService.new.execute(hook, current_user) if status flash[:notice] = 'Hook successfully executed.' else - flash[:alert] = 'Hook execution failed. '\ - 'Ensure hook URL is correct and service is up.' + flash[:alert] = "Hook execution failed: #{message}" end else flash[:alert] = 'Hook execution failed. Ensure the project has commits.' @@ -54,6 +53,7 @@ class Projects::HooksController < Projects::ApplicationController def hook_params params.require(:hook).permit(:url, :push_events, :issues_events, - :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification) + :merge_requests_events, :tag_push_events, :note_events, + :build_events, :enable_ssl_verification) end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 066b66014f8..8d8035ef5ff 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -1,7 +1,7 @@ class Projects::ImportsController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! - before_action :require_no_repo + before_action :require_no_repo, except: :show before_action :redirect_if_progress, except: :show def new @@ -24,21 +24,36 @@ class Projects::ImportsController < Projects::ApplicationController end def show - unless @project.import_in_progress? - if @project.import_finished? - redirect_to(project_path(@project)) and return + if @project.repository_exists? || @project.import_finished? + if continue_params + redirect_to continue_params[:to], notice: continue_params[:notice] else - redirect_to new_namespace_project_import_path(@project.namespace, - @project) && return + redirect_to project_path(@project), notice: "The project was successfully forked." end + elsif @project.import_failed? + redirect_to new_namespace_project_import_path(@project.namespace, @project) + else + if continue_params && continue_params[:notice_now] + flash.now[:notice] = continue_params[:notice_now] + end + # Render end end private + def continue_params + continue_params = params[:continue] + if continue_params + continue_params.permit(:to, :notice, :notice_now) + else + nil + end + end + def require_no_repo if @project.repository_exists? && !@project.import_in_progress? - redirect_to(namespace_project_path(@project.namespace, @project)) and return + redirect_to(namespace_project_path(@project.namespace, @project)) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e767efbdc0c..b59b52291fb 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -58,10 +58,10 @@ class Projects::IssuesController < Projects::ApplicationController end def show - @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.with_associations.fresh + @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue + @merge_requests = @issue.referenced_merge_requests respond_with(@issue) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 16c42386623..ab5c953189c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,13 +1,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :merge, :merge_check, - :ci_status, :toggle_subscription + :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, + :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds ] - before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] - before_action :validates_merge_request, only: [:show, :diffs, :commits] - before_action :define_show_vars, only: [:show, :diffs, :commits] - before_action :ensure_ref_fetched, only: [:show, :commits, :diffs] + before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] + before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] + before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -31,6 +32,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) + @merge_requests = @merge_requests.preload(:target_project) respond_to do |format| format.html @@ -78,6 +80,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def builds + respond_to do |format| + format.html { render 'show' } + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } + end + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @@ -90,20 +99,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController @target_project = merge_request.target_project @source_project = merge_request.source_project - @commits = @merge_request.compare_commits + @commits = @merge_request.compare_commits.reverse @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs + + @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit + @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count end - def edit - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - @target_branches = @merge_request.target_project.repository.branch_names - end - def create @target_branches ||= [] @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute @@ -117,6 +124,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def edit + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + @target_branches = @merge_request.target_project.repository.branch_names + end + def update @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) @@ -140,24 +153,34 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_check - if @merge_request.unchecked? - @merge_request.check_if_can_be_merged - end - - closes_issues + @merge_request.check_if_can_be_merged if @merge_request.unchecked? render partial: "projects/merge_requests/widget/show.html.haml", layout: false end + def cancel_merge_when_build_succeeds + return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request) + end + def merge return access_denied! unless @merge_request.can_be_merged_by?(current_user) - if @merge_request.mergeable? - @merge_request.update(merge_error: nil) - MergeWorker.perform_async(@merge_request.id, current_user.id, params) - @status = true + unless @merge_request.mergeable? + @status = :failed + return + end + + @merge_request.update(merge_error: nil) + + if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) + .execute(@merge_request) + @status = :merge_when_build_succeeds else - @status = false + MergeWorker.perform_async(@merge_request.id, current_user.id, params) + @status = :success end end @@ -249,11 +272,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_show_vars - @participants = @merge_request.participants(current_user) - # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @notes = @merge_request.mr_and_commit_notes.inc_author.fresh + @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh @discussions = Note.discussions_from_notes(@notes) @noteable = @merge_request @@ -263,12 +284,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff + @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit + if @merge_request.locked_long_ago? @merge_request.unlock_mr @merge_request.close end end + def define_widget_vars + @ci_commit = @merge_request.ci_commit + closes_issues + end + def invalid_mr # Render special view for MR with removed source or target branch render 'invalid' @@ -282,6 +311,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def merge_params + params.permit(:should_remove_source_branch, :commit_message) + end + # Make sure merge requests created before 8.0 # have head file in refs/merge-requests/ def ensure_ref_fetched diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 41cd08c93c6..6f1e186d408 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] before_action :authorize_admin_note!, only: [:update, :destroy] - before_action :find_current_user_notes, except: [:destroy, :delete_attachment] + before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] def index current_fetched_at = Time.now.to_i @@ -13,7 +13,8 @@ class Projects::NotesController < Projects::ApplicationController @notes.each do |note| notes_json[:notes] << { id: note.id, - html: note_to_html(note) + html: note_to_html(note), + valid: note.valid? } end @@ -58,6 +59,30 @@ class Projects::NotesController < Projects::ApplicationController end end + def award_toggle + noteable = if note_params[:noteable_type] == "issue" + project.issues.find(note_params[:noteable_id]) + else + project.merge_requests.find(note_params[:noteable_id]) + end + + data = { + author: current_user, + is_award: true, + note: note_params[:note].delete(":") + } + + note = noteable.notes.find_by(data) + + if note + note.destroy + else + Notes::CreateService.new(project, current_user, note_params).execute + end + + render json: { ok: true } + end + private def note @@ -107,13 +132,24 @@ class Projects::NotesController < Projects::ApplicationController end def render_note_json(note) - render json: { - id: note.id, - discussion_id: note.discussion_id, - html: note_to_html(note), - discussion_html: note_to_discussion_html(note), - discussion_with_diff_html: note_to_discussion_with_diff_html(note) - } + if note.valid? + render json: { + valid: true, + id: note.id, + discussion_id: note.discussion_id, + html: note_to_html(note), + award: note.is_award, + note: note.note, + discussion_html: note_to_discussion_html(note), + discussion_with_diff_html: note_to_discussion_with_diff_html(note) + } + else + render json: { + valid: false, + award: note.is_award, + errors: note.errors + } + end end def authorize_admin_note! diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 9de5269cd25..8364fc293b7 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!, except: :leave + before_action :authorize_admin_project_member!, except: :leave def index @project_members = @project.project_members @@ -23,16 +23,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_members = @group_members.where(user_id: users) end - @group_members = @group_members.order('access_level DESC').limit(20) + @group_members = @group_members.order('access_level DESC') end @project_member = @project.project_members.new end - def new - @project_member = @project.project_members.new - end - def create @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) @@ -41,11 +37,17 @@ class Projects::ProjectMembersController < Projects::ApplicationController def update @project_member = @project.project_members.find(params[:id]) + + return render_403 unless can?(current_user, :update_project_member, @project_member) + @project_member.update_attributes(member_params) end def destroy @project_member = @project.project_members.find(params[:id]) + + return render_403 unless can?(current_user, :destroy_project_member, @project_member) + @project_member.destroy respond_to do |format| @@ -71,16 +73,22 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def leave - if @project.namespace == current_user.namespace - message = 'You can not leave your own project. Transfer or delete the project.' - return redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) - end + @project_member = @project.project_members.find_by(user_id: current_user) - @project.project_members.find_by(user_id: current_user).destroy + if can?(current_user, :destroy_project_member, @project_member) + @project_member.destroy - respond_to do |format| - format.html { redirect_to dashboard_projects_path } - format.js { render nothing: true } + respond_to do |format| + format.html { redirect_to dashboard_projects_path, notice: "You left the project." } + format.js { render nothing: true } + end + else + if current_user == @project.owner + message = 'You can not leave your own project. Transfer or delete the project.' + redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) + else + render_403 + end end end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 6b52eccebf7..e49259c34b6 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -21,7 +21,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController if protected_branch && protected_branch.update_attributes( - developers_can_push: params[:developers_can_push] + developers_can_push: params[:developers_can_push] ) respond_to do |format| diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index d5ee6ac8663..be7d5c187fe 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -10,15 +10,13 @@ class Projects::RawController < Projects::ApplicationController @blob = @repository.blob_at(@commit.id, @path) if @blob - type = get_blob_type - headers['X-Content-Type-Options'] = 'nosniff' - send_data( - @blob.data, - type: type, - disposition: 'inline' - ) + if @blob.lfs_pointer? + send_lfs_object + else + stream_data + end else render_404 end @@ -35,4 +33,33 @@ class Projects::RawController < Projects::ApplicationController 'application/octet-stream' end end + + def stream_data + type = get_blob_type + + send_data( + @blob.data, + type: type, + disposition: 'inline' + ) + end + + def send_lfs_object + lfs_object = find_lfs_object + + if lfs_object && lfs_object.project_allowed_access?(@project) + send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment' + else + render_404 + end + end + + def find_lfs_object + lfs_object = LfsObject.find_by_oid(@blob.lfs_oid) + if lfs_object && lfs_object.file.exists? + lfs_object + else + nil + end + end end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb new file mode 100644 index 00000000000..0825a4311cb --- /dev/null +++ b/app/controllers/projects/releases_controller.rb @@ -0,0 +1,31 @@ +class Projects::ReleasesController < Projects::ApplicationController + # Authorize + before_action :require_non_empty_project + before_action :authorize_download_code! + before_action :authorize_push_code! + before_action :tag + before_action :release + + def edit + end + + def update + release.update_attributes(release_params) + + redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) + end + + private + + def tag + @tag ||= @repository.find_tag(params[:tag_id]) + end + + def release + @release ||= @project.releases.find_or_initialize_by(tag: @tag.name) + end + + def release_params + params.require(:release).permit(:description) + end +end diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb new file mode 100644 index 00000000000..e2785caa2fb --- /dev/null +++ b/app/controllers/projects/runner_projects_controller.rb @@ -0,0 +1,26 @@ +class Projects::RunnerProjectsController < Projects::ApplicationController + before_action :authorize_admin_project! + + layout 'project_settings' + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + return head(403) unless current_user.ci_authorized_runners.include?(@runner) + + path = runners_path(project) + + if @runner.assign_to(project, current_user) + redirect_to path + else + redirect_to path, alert: 'Failed adding runner to project' + end + end + + def destroy + runner_project = project.runner_projects.find(params[:id]) + runner_project.destroy + + redirect_to runners_path(project) + end +end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index deb07a21416..4993b2648a5 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -1,16 +1,14 @@ class Projects::RunnersController < Projects::ApplicationController - before_action :ci_project before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] before_action :authorize_admin_project! layout 'project_settings' def index - @runners = @ci_project.runners.order('id DESC') - @specific_runners = - Ci::Runner.specific.includes(:runner_projects). - where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ). - where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20) + @runners = project.runners.ordered + @specific_runners = current_user.ci_authorized_runners. + where.not(id: project.runners). + ordered.page(params[:page]).per(20) @shared_runners = Ci::Runner.shared.active @shared_runners_count = @shared_runners.count(:all) end @@ -27,7 +25,7 @@ class Projects::RunnersController < Projects::ApplicationController end def destroy - if @runner.only_for?(@ci_project) + if @runner.only_for?(project) @runner.destroy end @@ -53,10 +51,16 @@ class Projects::RunnersController < Projects::ApplicationController def show end + def toggle_shared_runners + project.toggle!(:shared_runners_enabled) + + redirect_to namespace_project_runners_path(project.namespace, project) + end + protected def set_runner - @runner ||= @ci_project.runners.find(params[:id]) + @runner ||= project.runners.find(params[:id]) end def runner_params diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 42dbb497e01..8b2577aebe1 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -1,14 +1,17 @@ class Projects::ServicesController < Projects::ApplicationController - ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain, + ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :drone_url, :build_type, :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, :send_from_committer_email, :disable_diffs, :external_wiki_url, + :note_events, :build_events, + :notify_only_broken_builds, :add_pusher, + :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] + :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, + :jira_issue_transition_id] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index b07a2a8db2f..2104c7a7a71 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -21,6 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController filter: :by_project, project: @project }) + @snippets = @snippets.page(params[:page]).per(PER_PAGE) end def new diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index f565fbbbbc3..280fe12cc7c 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -2,21 +2,29 @@ class Projects::TagsController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:create] + before_action :authorize_push_code!, only: [:new, :create] before_action :authorize_admin_project!, only: [:destroy] def index sorted = VersionSorter.rsort(@repository.tag_names) @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) + @releases = project.releases.where(tag: @tags) + end + + def show + @tag = @repository.find_tag(params[:id]) + @release = @project.releases.find_or_initialize_by(tag: @tag.name) + @commit = @repository.commit(@tag.target) end def create result = CreateTagService.new(@project, current_user). - execute(params[:tag_name], params[:ref], params[:message]) + execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) if result[:status] == :success @tag = result[:tag] - redirect_to namespace_project_tags_path(@project.namespace, @project) + + redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name) else @error = result[:message] render action: 'new' @@ -26,12 +34,6 @@ class Projects::TagsController < Projects::ApplicationController def destroy DeleteTagService.new(project, current_user).execute(params[:id]) - respond_to do |format| - format.html do - redirect_to namespace_project_tags_path(@project.namespace, - @project) - end - format.js - end + redirect_to namespace_project_tags_path(@project.namespace, @project) end end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index bdcb1a3e297..cb3ed0f6f9c 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,13 +1,14 @@ # Controller for viewing a repository's file structure class Projects::TreeController < Projects::ApplicationController include ExtractsPath + include CreatesCommit include ActionView::Helpers::SanitizeHelper before_action :require_non_empty_project, except: [:new, :create] before_action :assign_ref_vars before_action :assign_dir_vars, only: [:create_dir] before_action :authorize_download_code! - before_action :authorize_push_code!, only: [:create_dir] + before_action :authorize_edit_tree!, only: [:create_dir] def show return render_404 unless @repository.commit(@ref) @@ -33,33 +34,19 @@ class Projects::TreeController < Projects::ApplicationController def create_dir return render_404 unless @commit_params.values.all? - begin - result = Files::CreateDirService.new(@project, current_user, @commit_params).execute - message = result[:message] - rescue => e - message = e.to_s - end - - if result && result[:status] == :success - flash[:notice] = "The directory has been successfully created" - respond_to do |format| - format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) } - end - else - flash[:alert] = message - respond_to do |format| - format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) } - end - end + create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", + success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)), + failure_path: namespace_project_tree_path(@project.namespace, @project, @ref)) end + private + def assign_dir_vars - @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref + @target_branch = params[:target_branch] + @dir_name = File.join(@path, params[:dir_name]) @commit_params = { file_path: @dir_name, - current_branch: @ref, - target_branch: @new_branch, commit_message: params[:commit_message], } end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index 782ebd01b05..30adfad1daa 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,22 +1,21 @@ class Projects::TriggersController < Projects::ApplicationController - before_action :ci_project before_action :authorize_admin_project! layout 'project_settings' def index - @triggers = @ci_project.triggers + @triggers = project.triggers @trigger = Ci::Trigger.new end def create - @trigger = @ci_project.triggers.new + @trigger = project.triggers.new @trigger.save if @trigger.valid? redirect_to namespace_project_triggers_path(@project.namespace, @project) else - @triggers = @ci_project.triggers.select(&:persisted?) + @triggers = project.triggers.select(&:persisted?) render :index end end @@ -30,6 +29,6 @@ class Projects::TriggersController < Projects::ApplicationController private def trigger - @trigger ||= @ci_project.triggers.find(params[:id]) + @trigger ||= project.triggers.find(params[:id]) end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index d6561a45a70..10efafea9db 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,5 +1,4 @@ class Projects::VariablesController < Projects::ApplicationController - before_action :ci_project before_action :authorize_admin_project! layout 'project_settings' @@ -8,9 +7,7 @@ class Projects::VariablesController < Projects::ApplicationController end def update - if ci_project.update_attributes(project_params) - Ci::EventService.new.change_project_settings(current_user, ci_project) - + if project.update_attributes(project_params) redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' else render action: 'show' diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ecaf4476246..935f7d75c6a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,7 @@ class ProjectsController < ApplicationController include ExtractsPath - prepend_before_filter :render_go_import, only: [:show] + prepend_before_action :render_go_import, only: [:show] skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] @@ -72,8 +72,7 @@ class ProjectsController < ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.forked? - @project.forked_project_link.destroy + if @project.unlink_fork flash[:notice] = 'The fork relationship has been removed.' end end @@ -124,11 +123,7 @@ class ProjectsController < ApplicationController ::Projects::DestroyService.new(@project, current_user, {}).execute flash[:alert] = "Project '#{@project.name}' was deleted." - if request.referer.include?('/admin') - redirect_to admin_namespaces_projects_path - else - redirect_to dashboard_projects_path - end + redirect_to dashboard_projects_path rescue Projects::DestroyService::DestroyError => ex redirect_to edit_project_path(@project), alert: ex.message end @@ -185,14 +180,14 @@ class ProjectsController < ApplicationController @project.reload render json: { - html: view_to_html_string("projects/buttons/_star") + star_count: @project.star_count } end def markdown_preview text = params[:text] - ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) ext.analyze(text) render json: { @@ -224,9 +219,10 @@ class ProjectsController < ApplicationController def project_params params.require(:project).permit( - :name, :path, :description, :issues_tracker, :tag_list, + :name, :path, :description, :issues_tracker, :tag_list, :runners_token, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, - :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar + :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, + :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, ) end @@ -255,7 +251,7 @@ class ProjectsController < ApplicationController project.repository_exists? && !project.empty_repo? end - # Override get_id from ExtractsPath, which returns the branch and file path + # Override get_id from ExtractsPath, which returns the branch and file path # for the blob/tree, which in this case is just the root of the default branch. def get_id project.repository.root_ref diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3b3dc86cb68..c48175a4c5a 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,10 +1,21 @@ class RegistrationsController < Devise::RegistrationsController before_action :signup_enabled? + include Recaptcha::Verify def new redirect_to(new_user_session_path) end + def create + if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha + super + else + flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." + flash.delete :recaptcha_error + render action: 'new' + end + end + def destroy DeleteUserService.new(current_user).execute(current_user) @@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController def sign_up_params params.require(:user).permit(:username, :email, :name, :password, :password_confirmation) end + + def resource_name + :user + end + + def resource + @resource ||= User.new(sign_up_params) + end + + def devise_mapping + @devise_mapping ||= Devise.mappings[:user] + end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index eb0408a95e5..9bb42ec86b3 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -23,8 +23,8 @@ class SearchController < ApplicationController @search_results = if @project - unless %w(blobs notes issues merge_requests milestones wiki_blobs). - include?(@scope) + unless %w(blobs notes issues merge_requests milestones wiki_blobs + commits).include?(@scope) @scope = 'blobs' end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1b60d3e27d0..825f85199be 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,9 +1,11 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor + include Recaptcha::ClientHelper prepend_before_action :authenticate_with_two_factor, only: [:create] prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] + before_action :load_recaptcha def new if Gitlab.config.ldap.enabled @@ -40,7 +42,7 @@ class SessionsController < Devise::SessionsController User.find(session[:otp_user_id]) end end - + def store_redirect_path redirect_path = if request.referer.present? && (params['redirect_to_referer'] == 'yes') @@ -87,14 +89,14 @@ class SessionsController < Devise::SessionsController provider = Gitlab.config.omniauth.auto_sign_in_with_provider return unless provider.present? - # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is - # registered or no alert at all. In case of another alert (such as a blocked user), it is safer + # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is + # registered or no alert at all. In case of another alert (such as a blocked user), it is safer # to do nothing to prevent redirection loops with certain Omniauth providers. return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated') - + # Prevent alert from popping up on the first page shown after authentication. - flash[:alert] = nil - + flash[:alert] = nil + redirect_to user_omniauth_authorize_path(provider.to_sym) end @@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController AuditEventService.new(user, user, options). for_authentication.security_event end + + def load_recaptcha + Gitlab::Recaptcha.load_configurations! + end end diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb new file mode 100644 index 00000000000..682ca5e3821 --- /dev/null +++ b/app/controllers/sherlock/application_controller.rb @@ -0,0 +1,12 @@ +module Sherlock + class ApplicationController < ::ApplicationController + before_action :find_transaction + + def find_transaction + if params[:transaction_id] + @transaction = Gitlab::Sherlock.collection. + find_transaction(params[:transaction_id]) + end + end + end +end diff --git a/app/controllers/sherlock/file_samples_controller.rb b/app/controllers/sherlock/file_samples_controller.rb new file mode 100644 index 00000000000..0c3bc100106 --- /dev/null +++ b/app/controllers/sherlock/file_samples_controller.rb @@ -0,0 +1,7 @@ +module Sherlock + class FileSamplesController < Sherlock::ApplicationController + def show + @file_sample = @transaction.find_file_sample(params[:id]) + end + end +end diff --git a/app/controllers/sherlock/queries_controller.rb b/app/controllers/sherlock/queries_controller.rb new file mode 100644 index 00000000000..63b26aab1a4 --- /dev/null +++ b/app/controllers/sherlock/queries_controller.rb @@ -0,0 +1,7 @@ +module Sherlock + class QueriesController < Sherlock::ApplicationController + def show + @query = @transaction.find_query(params[:id]) + end + end +end diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb new file mode 100644 index 00000000000..ccc739da879 --- /dev/null +++ b/app/controllers/sherlock/transactions_controller.rb @@ -0,0 +1,19 @@ +module Sherlock + class TransactionsController < Sherlock::ApplicationController + def index + @transactions = Gitlab::Sherlock.collection.newest_first + end + + def show + @transaction = Gitlab::Sherlock.collection.find_transaction(params[:id]) + + render_404 unless @transaction + end + + def destroy_all + Gitlab::Sherlock.collection.clear + + redirect_to(:back) + end + end +end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 9f9f9a92f11..c72df73af46 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,6 +1,9 @@ class SnippetsController < ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + # Allow read snippet + before_action :authorize_read_snippet!, only: [:show, :raw] + # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -79,10 +82,14 @@ class SnippetsController < ApplicationController [Snippet::PUBLIC, Snippet::INTERNAL]). find(params[:id]) else - PersonalSnippet.are_public.find(params[:id]) + PersonalSnippet.find(params[:id]) end end + def authorize_read_snippet! + authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) + end + def authorize_update_snippet! return render_404 unless can?(current_user, :update_personal_snippet, @snippet) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1484356a7f4..280228dbcc0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,14 +3,11 @@ class UsersController < ApplicationController before_action :set_user def show - @contributed_projects = contributed_projects.joined(@user). - reject(&:forked?) + @contributed_projects = contributed_projects.joined(@user).reject(&:forked?) - @projects = @user.personal_projects. - where(id: authorized_projects_ids).includes(:namespace) + @projects = PersonalProjectsFinder.new(@user).execute(current_user) - # Collect only groups common for both users - @groups = @user.groups & GroupsFinder.new.execute(current_user) + @groups = @user.groups.order_id_desc respond_to do |format| format.html @@ -53,16 +50,8 @@ class UsersController < ApplicationController @user = User.find_by_username!(params[:username]) end - def authorized_projects_ids - # Projects user can view - @authorized_projects_ids ||= - ProjectsFinder.new.execute(current_user).pluck(:id) - end - def contributed_projects - @contributed_projects = Project. - where(id: authorized_projects_ids & @user.contributed_projects_ids). - includes(:namespace) + ContributedProjectsFinder.new(@user).execute(current_user) end def contributions_calendar @@ -73,9 +62,13 @@ class UsersController < ApplicationController def load_events # Get user activity feed for projects common for both users @events = @user.recent_events. - where(project_id: authorized_projects_ids). - with_associations + merge(projects_for_current_user). + references(:project). + with_associations. + limit_recent(20, params[:offset]) + end - @events = @events.limit(20).offset(params[:offset] || 0) + def projects_for_current_user + ProjectsFinder.new.execute(current_user) end end diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb new file mode 100644 index 00000000000..0209649b017 --- /dev/null +++ b/app/finders/contributed_projects_finder.rb @@ -0,0 +1,37 @@ +class ContributedProjectsFinder + def initialize(user) + @user = user + end + + # Finds the projects "@user" contributed to, limited to either public projects + # or projects visible to the given user. + # + # current_user - When given the list of the projects is limited to those only + # visible by this user. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = projects_visible_to_user(current_user) + else + relation = public_projects + end + + relation.includes(:namespace).order_id_desc + end + + private + + def projects_visible_to_user(current_user) + authorized = @user.contributed_projects.visible_to_user(current_user) + + union = Gitlab::SQL::Union. + new([authorized.select(:id), public_projects.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + end + + def public_projects + @user.contributed_projects.public_only + end +end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb deleted file mode 100644 index d3597ef0901..00000000000 --- a/app/finders/groups_finder.rb +++ /dev/null @@ -1,38 +0,0 @@ -class GroupsFinder - def execute(current_user, options = {}) - all_groups(current_user) - end - - private - - def all_groups(current_user) - if current_user - if current_user.authorized_groups.any? - # User has access to groups - # - # Return only: - # groups with public projects - # groups with internal projects - # groups with joined projects - # - group_ids = Project.public_and_internal_only.pluck(:namespace_id) + - current_user.authorized_groups.pluck(:id) - Group.where(id: group_ids) - else - # User has no group membership - # - # Return only: - # groups with public projects - # groups with internal projects - # - Group.where(id: Project.public_and_internal_only.pluck(:namespace_id)) - end - else - # Not authenticated - # - # Return only: - # groups with public projects - Group.where(id: Project.public_only.pluck(:namespace_id)) - end - end -end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c407dfc163a..3d5e8b6fbe7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -62,10 +62,10 @@ class IssuableFinder if project? @project = Project.find(params[:project_id]) - + unless Ability.abilities.allowed?(current_user, :read_project, @project) @project = nil - end + end else @project = nil end @@ -77,11 +77,11 @@ class IssuableFinder return @projects if defined?(@projects) if project? - project + @projects = project elsif current_user && params[:authorized_only].presence && !current_user_related? - current_user.authorized_projects + @projects = current_user.authorized_projects else - ProjectsFinder.new.execute(current_user) + @projects = ProjectsFinder.new.execute(current_user) end end @@ -190,8 +190,10 @@ class IssuableFinder def by_project(items) items = - if projects - items.of_projects(projects).references(:project) + if project? + items.of_projects(projects).references_project + elsif projects + items.merge(projects.reorder(nil)).join_project else items.none end @@ -206,7 +208,9 @@ class IssuableFinder end def sort(items) - items.sort(params[:sort]) + # Ensure we always have an explicit sort order (instead of inheriting + # multiple orders when combining ActiveRecord::Relation objects). + params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) end def by_assignee(items) diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb new file mode 100644 index 00000000000..630c73c2a94 --- /dev/null +++ b/app/finders/milestones_finder.rb @@ -0,0 +1,12 @@ +class MilestonesFinder + def execute(projects, params) + milestones = Milestone.of_projects(projects) + milestones = milestones.reorder("due_date ASC") + + case params[:state] + when 'closed' then milestones.closed + when 'all' then milestones + else milestones.active + end + end +end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index ab252821b52..fa4c635f55c 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,9 +12,9 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).not_inline when "issue" - project.issues.find(target_id).notes.inc_author + project.issues.find(target_id).notes.nonawards.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author + project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb new file mode 100644 index 00000000000..a61ffa22990 --- /dev/null +++ b/app/finders/personal_projects_finder.rb @@ -0,0 +1,41 @@ +class PersonalProjectsFinder + def initialize(user) + @user = user + end + + # Finds the projects belonging to the user in "@user", limited to either + # public projects or projects visible to the given user. + # + # current_user - When given the list of projects is limited to those only + # visible by this user. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = projects_visible_to_user(current_user) + else + relation = public_projects + end + + relation.includes(:namespace).order_id_desc + end + + private + + def projects_visible_to_user(current_user) + authorized = @user.personal_projects.visible_to_user(current_user) + + union = Gitlab::SQL::Union. + new([authorized.select(:id), public_and_internal_projects.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + end + + def public_projects + @user.personal_projects.public_only + end + + def public_and_internal_projects + @user.personal_projects.public_and_internal_only + end +end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index c81bb51583a..3b4e0362e04 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,11 +1,39 @@ class ProjectsFinder - def execute(current_user, options = {}) + # Returns all projects, optionally including group projects a user has access + # to. + # + # ## Examples + # + # Retrieving all public projects: + # + # ProjectsFinder.new.execute + # + # Retrieving all public/internal projects and those the given user has access + # to: + # + # ProjectsFinder.new.execute(some_user) + # + # Retrieving all public/internal projects as well as the group's projects the + # user has access to: + # + # ProjectsFinder.new.execute(some_user, group: some_group) + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil, options = {}) group = options[:group] if group - group_projects(current_user, group) + segments = group_projects(current_user, group) else - all_projects(current_user) + segments = all_projects(current_user) + end + + if segments.length > 1 + union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) }) + + Project.where("projects.id IN (#{union.to_sql})") + else + segments.first end end @@ -13,77 +41,36 @@ class ProjectsFinder def group_projects(current_user, group) if current_user - if group.users.include?(current_user) - # User is group member - # - # Return ALL group projects - group.projects - else - projects_members = ProjectMember.in_projects(group.projects). - with_user(current_user) - - if projects_members.any? - # User is a project member - # - # Return only: - # public projects - # internal projects - # joined projects - # - group.projects.where( - "projects.id IN (?) OR projects.visibility_level IN (?)", - projects_members.pluck(:source_id), - Project.public_and_internal_levels - ) - else - # User has no access to group or group projects - # - # Return only: - # public projects - # internal projects - # - group.projects.public_and_internal_only - end - end + [ + group_projects_for_user(current_user, group), + group.projects.public_and_internal_only + ] else - # Not authenticated - # - # Return only: - # public projects - group.projects.public_only + [group.projects.public_only] end end def all_projects(current_user) if current_user - if current_user.authorized_projects.any? - # User has access to private projects - # - # Return only: - # public projects - # internal projects - # joined projects - # - Project.where( - "projects.id IN (?) OR projects.visibility_level IN (?)", - current_user.authorized_projects.pluck(:id), - Project.public_and_internal_levels - ) - else - # User has no access to private projects - # - # Return only: - # public projects - # internal projects - # - Project.public_and_internal_only - end + [current_user.authorized_projects, public_and_internal_projects] else - # Not authenticated - # - # Return only: - # public projects - Project.public_only + [Project.public_only] end end + + def group_projects_for_user(current_user, group) + if group.users.include?(current_user) + group.projects + else + group.projects.visible_to_user(current_user) + end + end + + def public_projects + Project.unscoped.public_only + end + + def public_and_internal_projects + Project.unscoped.public_and_internal_only + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8ecdeaf8e76..f7f7a1a02d3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -61,29 +61,29 @@ module ApplicationHelper options[:class] ||= '' options[:class] << ' identicon' bg_key = project.id % 7 - style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555" + style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555" content_tag(:div, class: options[:class], style: style) do project.name[0, 1].upcase end end - def avatar_icon(user_or_email = nil, size = nil) + def avatar_icon(user_or_email = nil, size = nil, scale = 2) if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email) + user = User.find_by(email: user_or_email.downcase) end if user user.avatar_url(size) || default_avatar else - gravatar_icon(user_or_email, size) + gravatar_icon(user_or_email, size, scale) end end - def gravatar_icon(user_email = '', size = nil) - GravatarService.new.execute(user_email, size) || + def gravatar_icon(user_email = '', size = nil, scale = 2) + GravatarService.new.execute(user_email, size, scale) || default_avatar end @@ -204,12 +204,16 @@ module ApplicationHelper # Returns an HTML-safe String def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago", + class: "#{html_class} js-timeago js-timeago-pending", datetime: time.getutc.iso8601, title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), data: { toggle: 'tooltip', placement: placement, container: 'body' } - element += javascript_tag "$('.js-timeago').timeago()" unless skip_js + unless skip_js + element << javascript_tag( + "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" + ) + end element end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index cd99a232403..0cfc0565e84 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? @@ -50,5 +50,17 @@ module AuthHelper current_user.identities.exists?(provider: provider.to_s) end + def two_factor_skippable? + current_application_settings.require_two_factor_authentication && + !current_user.two_factor_enabled && + current_application_settings.two_factor_grace_period && + !two_factor_grace_period_expired? + end + + def two_factor_grace_period_expired? + current_user.otp_grace_period_started_at && + (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current + end + extend self end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 77d99140c43..d31d4cde08f 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -22,36 +22,92 @@ module BlobHelper %w(credits changelog news copying copyright license authors) end - def edit_blob_link(project, ref, path, options = {}) - blob = - begin - project.repository.blob_at(ref, path) - rescue - nil - end - - if blob && blob.text? - text = 'Edit' - after = options[:after] || '' - from_mr = options[:from_merge_request_id] - link_opts = {} - link_opts[:from_merge_request_id] = from_mr if from_mr - cls = 'btn btn-small' - if allowed_tree_edit?(project, ref) - link_to(text, - namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - link_opts), - class: cls - ) - else - content_tag :span, text, class: cls + ' disabled' - end + after.html_safe - else - '' + def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) + return unless current_user + + blob = project.repository.blob_at(ref, path) rescue nil + + return unless blob && blob_text_viewable?(blob) + + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + + edit_path = namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts) + + if !on_top_of_branch? + button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } + elsif can_edit_blob?(blob) + link_to "Edit", edit_path, class: 'btn btn-small' + elsif can?(current_user, :fork_project, project) + continue_params = { + to: edit_path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to "Edit", fork_path, class: 'btn btn-small', method: :post + end + end + + def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) + return unless current_user + + blob = project.repository.blob_at(ref, path) rescue nil + + return unless blob + + if !on_top_of_branch? + button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } + elsif blob.lfs_pointer? + button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } + elsif can_edit_blob?(blob) + button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' + elsif can?(current_user, :fork_project, project) + continue_params = { + to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to #{action} this file again.", + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post end end + def replace_blob_link(project = @project, ref = @ref, path = @path) + modify_file_link( + project, + ref, + path, + label: "Replace", + action: "replace", + btn_class: "default", + modal_type: "upload" + ) + end + + def delete_blob_link(project = @project, ref = @ref, path = @path) + modify_file_link( + project, + ref, + path, + label: "Delete", + action: "delete", + btn_class: "remove", + modal_type: "remove" + ) + end + + def can_edit_blob?(blob, project = @project, ref = @ref) + !blob.lfs_pointer? && can_edit_tree?(project, ref) + end + def leave_edit_message "Leave edit mode?\nAll unsaved changes will be lost." end @@ -60,7 +116,7 @@ module BlobHelper if Gitlab::MarkupHelper.previewable?(filename) 'Preview' else - 'Preview changes' + 'Preview Changes' end end @@ -71,4 +127,16 @@ module BlobHelper def blob_icon(mode, name) icon("#{file_type_icon_class('file', mode, name)} fw") end + + def blob_text_viewable?(blob) + blob && blob.text? && !blob.lfs_pointer? + end + + def blob_size(blob) + if blob.lfs_pointer? + blob.lfs_size + else + blob.size + end + end end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index d6eaa7d57bc..e39548e17e1 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -11,7 +11,7 @@ module BranchesHelper def can_push_branch?(project, branch_name) return false unless project.repository.branch_names.include?(branch_name) - + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb deleted file mode 100644 index 1b5a2c31d74..00000000000 --- a/app/helpers/builds_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module BuildsHelper - def build_ref_link build - gitlab_ref_link build.project, build.ref - end - - def build_commit_link build - gitlab_commit_link build.project, build.short_sha - end - - def build_url(build) - namespace_project_build_path(build.gl_project, build.project, build) - end -end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb new file mode 100644 index 00000000000..ec0e3f409c1 --- /dev/null +++ b/app/helpers/button_helper.rb @@ -0,0 +1,58 @@ +module ButtonHelper + # Output a "Copy to Clipboard" button + # + # data - Data attributes passed to `content_tag` + # + # Examples: + # + # # Define the clipboard's text + # clipboard_button(clipboard_text: "Foo") + # # => "<button class='...' data-clipboard-text='Foo'>...</button>" + # + # # Define the target element + # clipboard_button(clipboard_target: "div#foo") + # # => "<button class='...' data-clipboard-target='div#foo'>...</button>" + # + # See http://clipboardjs.com/#usage + def clipboard_button(data = {}) + content_tag :button, + icon('clipboard'), + class: 'btn btn-xs btn-clipboard', + data: data, + type: :button + end + + def http_clone_button(project) + klass = 'btn js-protocol-switch' + klass << ' active' if default_clone_protocol == 'http' + klass << ' has_tooltip' if current_user.try(:require_password?) + + protocol = gitlab_config.protocol.upcase + + content_tag :button, protocol, + class: klass, + data: { + clone: project.http_url_to_repo, + container: 'body', + html: 'true', + title: "Set a password on your account<br>to pull or push via #{protocol}" + }, + type: :button + end + + def ssh_clone_button(project) + klass = 'btn js-protocol-switch' + klass << ' active' if default_clone_protocol == 'ssh' + klass << ' has_tooltip' if current_user.try(:require_ssh_key?) + + content_tag :button, 'SSH', + class: klass, + data: { + clone: project.ssh_url_to_repo, + container: 'body', + html: 'true', + title: 'Add an SSH key to your profile<br>to pull or push via SSH.' + }, + type: :button + end +end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb deleted file mode 100644 index baddbc806f2..00000000000 --- a/app/helpers/ci/gitlab_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - module GitlabHelper - def no_turbolink - { :"data-no-turbolink" => "data-no-turbolink" } - end - - def gitlab_ref_link project, ref - gitlab_url = project.gitlab_url.dup - gitlab_url << "/commits/#{ref}" - link_to ref, gitlab_url, no_turbolink - end - - def gitlab_compare_link project, before, after - gitlab_url = project.gitlab_url.dup - gitlab_url << "/compare/#{before}...#{after}" - - link_to "#{before}...#{after}", gitlab_url, no_turbolink - end - - def gitlab_commit_link project, sha - gitlab_url = project.gitlab_url.dup - gitlab_url << "/commit/#{sha}" - link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink - end - - def yaml_web_editor_link(project) - commits = project.commits - - if commits.any? && commits.last.ci_yaml_file - "#{project.gitlab_url}/edit/master/.gitlab-ci.yml" - else - "#{project.gitlab_url}/new/master" - end - end - end -end diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb deleted file mode 100644 index fd991a4165a..00000000000 --- a/app/helpers/ci/projects_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - module ProjectsHelper - def ref_tab_class ref = nil - 'active' if ref == @ref - end - - def success_ratio(success_builds, failed_builds) - failed_builds = failed_builds.count(:all) - success_builds = success_builds.count(:all) - - return 100 if failed_builds.zero? - - ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 - ratio.to_i - end - - def markdown_badge_code(project, ref) - url = status_ci_project_url(project, ref: ref, format: 'png') - "[![build status](#{url})](#{ci_project_url(project, ref: ref)})" - end - - def html_badge_code(project, ref) - url = status_ci_project_url(project, ref: ref, format: 'png') - "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>" - end - - def project_uses_specific_runner?(project) - project.runners.any? - end - - def no_runners_for_project?(project) - project.runners.blank? && - Ci::Runner.shared.blank? - end - end -end diff --git a/app/helpers/ci_badge_helper.rb b/app/helpers/ci_badge_helper.rb new file mode 100644 index 00000000000..27386133e36 --- /dev/null +++ b/app/helpers/ci_badge_helper.rb @@ -0,0 +1,13 @@ +module CiBadgeHelper + def markdown_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + link = namespace_project_commits_path(project.namespace, project, ref) + "[![build status](#{url})](#{link})" + end + + def html_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + link = namespace_project_commits_path(project.namespace, project, ref) + "<a href='#{link}'><img src='#{url}' /></a>" + end +end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index dbd1e26fa79..d8bee21c82e 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,29 +1,28 @@ module CiStatusHelper def ci_status_path(ci_commit) - project = ci_commit.gl_project - ci_namespace_project_commit_path(project.namespace, project, ci_commit.sha) + project = ci_commit.project + builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) end def ci_status_icon(ci_commit) ci_icon_for_status(ci_commit.status) end - def ci_status_color(ci_commit) - case ci_commit.status - when 'success' - 'green' - when 'failed' - 'red' - when 'running', 'pending' - 'yellow' - else - 'gray' - end + def ci_status_label(ci_commit) + ci_label_for_status(ci_commit.status) end def ci_status_with_icon(status) content_tag :span, class: "ci-status ci-#{status}" do - ci_icon_for_status(status) + ' '.html_safe + status + ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) + end + end + + def ci_label_for_status(status) + if status == 'success' + 'passed' + else + status end end @@ -40,6 +39,19 @@ module CiStatusHelper 'circle' end - icon(icon_name) + icon(icon_name + ' fw') + end + + def render_ci_status(ci_commit) + link_to ci_status_icon(ci_commit), + ci_status_path(ci_commit), + class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", + title: "Build #{ci_status_label(ci_commit)}", + data: { toggle: 'tooltip', placement: 'left' } + end + + def no_runners_for_project?(project) + project.runners.blank? && + Ci::Runner.shared.blank? end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 9df20c9fce5..590d20ac7b3 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -109,7 +109,7 @@ module CommitsHelper ) elsif @path.present? return link_to( - "Browse Dir »", + "Browse Directory »", namespace_project_tree_path(project.namespace, project, tree_join(commit.id, @path)), class: "pull-right" @@ -117,7 +117,7 @@ module CommitsHelper end end link_to( - "Browse Code »", + "Browse Files »", namespace_project_tree_path(project.namespace, project, commit), class: "pull-right" ) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e65e37211c4..24134310fc5 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,4 +1,8 @@ module DiffHelper + def diff_view + params[:view] == 'parallel' ? 'parallel' : 'inline' + end + def allowed_diff_size if diff_hard_limit_enabled? Commit::DIFF_HARD_LIMIT_FILES @@ -132,33 +136,19 @@ module DiffHelper end def inline_diff_btn - params_copy = params.dup - params_copy[:view] = 'inline' - # Always use HTML to handle case where JSON diff rendered this button - params_copy.delete(:format) - - link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do - 'Inline' - end + diff_btn('Inline', 'inline', diff_view == 'inline') end def parallel_diff_btn - params_copy = params.dup - params_copy[:view] = 'parallel' - # Always use HTML to handle case where JSON diff rendered this button - params_copy.delete(:format) - - link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do - 'Side-by-side' - end + diff_btn('Side-by-side', 'parallel', diff_view == 'parallel') end def submodule_link(blob, ref, repository = @repository) tree, commit = submodule_links(blob, ref, repository) commit_id = if commit.nil? - blob.id[0..10] + Commit.truncate_sha(blob.id) else - link_to "#{blob.id[0..10]}", commit + link_to Commit.truncate_sha(blob.id), commit end [ @@ -171,7 +161,7 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file first_commit = @first_commit || @commit - first_commit.parent + first_commit.parent || @first_commit else @commit end @@ -187,4 +177,18 @@ module DiffHelper def editable_diff?(diff) !diff.deleted_file && @merge_request && @merge_request.source_project end + + private + + def diff_btn(title, name, selected) + params_copy = params.dup + params_copy[:view] = name + + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) + + link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do + title + end + end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 45788ba95ac..41b5bd7be90 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -28,6 +28,8 @@ module EmailsHelper return "View #{action.humanize.singularize}" end end + + nil end def color_email_diff(diffcontent) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 6f69c2a9f32..dde83ff36b5 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -108,19 +108,23 @@ module EventsHelper end end elsif event.push? - if event.push_with_commits? && event.md_ref? - if event.commits_count > 1 - namespace_project_compare_url(event.project.namespace, event.project, - from: event.commit_from, to: - event.commit_to) - else - namespace_project_commit_url(event.project.namespace, event.project, - id: event.commit_to) - end + push_event_feed_url(event) + end + end + + def push_event_feed_url(event) + if event.push_with_commits? && event.md_ref? + if event.commits_count > 1 + namespace_project_compare_url(event.project.namespace, event.project, + from: event.commit_from, to: + event.commit_to) else - namespace_project_commits_url(event.project.namespace, event.project, - event.ref_name) + namespace_project_commit_url(event.project.namespace, event.project, + id: event.commit_to) end + else + namespace_project_commits_url(event.project.namespace, event.project, + event.ref_name) end end @@ -198,7 +202,7 @@ module EventsHelper xml.link href: event_link xml.title truncate(event_title, length: 80) xml.updated event.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) xml.author do |author| xml.name event.author_name xml.email event.author_email diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb index 838b85afdfe..1f3401f2906 100644 --- a/app/helpers/external_wiki_helper.rb +++ b/app/helpers/external_wiki_helper.rb @@ -1,7 +1,7 @@ module ExternalWikiHelper def get_project_wiki_path(project) external_wiki_service = project.services. - select { |service| service.to_param == 'external_wiki' }.first + find { |service| service.to_param == 'external_wiki' } if external_wiki_service.present? && external_wiki_service.active? external_wiki_service.properties['external_wiki_url'] else diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 65813482120..ca41657cec1 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -20,7 +20,7 @@ module GitlabMarkdownHelper end user = current_user if defined?(current_user) - gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user) + gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -48,47 +48,34 @@ module GitlabMarkdownHelper def markdown(text, context = {}) return "" unless text.present? - context.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref - ) + context[:project] ||= @project - user = current_user if defined?(current_user) + html = Banzai.render(text, context) - html = Gitlab::Markdown.render(text, context) - Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user) - end + context.merge!( + current_user: (current_user if defined?(current_user)), - # TODO (rspeicher): Remove all usages of this helper and just call `markdown` - # with a custom pipeline depending on the content being rendered - def gfm(text, options = {}) - return "" unless text.present? - - options.reverse_merge!( - path: @path, - pipeline: :default, - project: @project, - project_wiki: @project_wiki, - ref: @ref + # RelativeLinkFilter + requested_path: @path, + project_wiki: @project_wiki, + ref: @ref ) - user = current_user if defined?(current_user) - - html = Gitlab::Markdown.gfm(text, options) - Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user) + Banzai.post_process(html, context) end def asciidoc(text) - Gitlab::Asciidoc.render(text, { - commit: @commit, - project: @project, - project_wiki: @project_wiki, + Gitlab::Asciidoc.render( + text, + project: @project, + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + project_wiki: @project_wiki, requested_path: @path, - ref: @ref - }) + ref: @ref, + commit: @commit + ) end # Return the first line of +text+, up to +max_chars+, after parsing the line diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index b0b536d4649..f3fddef01cb 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -6,7 +6,7 @@ # # For example instead of this: # -# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request) +# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) # # We can simply use shortcut: # diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 1e372d5631d..c2ab80f2e0d 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -16,4 +16,14 @@ module GraphHelper ids = parents.map { |p| p.id } ids.zip(parent_spaces) end + + def success_ratio(success_builds, failed_builds) + failed_builds = failed_builds.count(:all) + success_builds = success_builds.count(:all) + + return 100 if failed_builds.zero? + + ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio.to_i + end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 1cf5b96481a..5724d3aabec 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -27,16 +27,20 @@ module IconsHelper end end - def public_icon - icon('globe fw') - end - - def internal_icon - icon('shield fw') - end + def visibility_level_icon(level, fw: true) + name = + case level + when Gitlab::VisibilityLevel::PRIVATE + 'lock' + when Gitlab::VisibilityLevel::INTERNAL + 'shield' + else # Gitlab::VisibilityLevel::PUBLIC + 'globe' + end + + name << " fw" if fw - def private_icon - icon('lock fw') + icon(name) end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index fda18e7b316..c12456a187f 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -44,28 +44,35 @@ module IssuesHelper end def bulk_update_milestone_options - options_for_select([['None (backlog)', -1]]) + - options_from_collection_for_select(project_active_milestones, 'id', - 'title', params[:milestone_id]) + milestones = project_active_milestones.to_a + milestones.unshift(Milestone::None) + + options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) end def milestone_options(object) - options_from_collection_for_select(object.project.milestones.active, - 'id', 'title', object.milestone_id) + milestones = object.project.milestones.active.to_a + milestones.unshift(Milestone::None) + + options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) end - def issue_box_class(item) + def status_box_class(item) if item.respond_to?(:expired?) && item.expired? - 'issue-box-expired' + 'status-box-expired' elsif item.respond_to?(:merged?) && item.merged? - 'issue-box-merged' + 'status-box-merged' elsif item.closed? - 'issue-box-closed' + 'status-box-closed' else - 'issue-box-open' + 'status-box-open' end end + def issue_button_visibility(issue, closed) + return 'hidden' if issue.closed? == closed + end + def issue_to_atom(xml, issue) xml.entry do xml.id namespace_project_issue_url(issue.project.namespace, @@ -74,7 +81,7 @@ module IssuesHelper issue.project, issue) xml.title truncate(issue.title, length: 80) xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email) + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.author do |author| xml.name issue.author_name xml.email issue.author_email @@ -84,9 +91,51 @@ module IssuesHelper end def merge_requests_sentence(merge_requests) - merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') + # Sorting based on the `!123` or `group/project!123` reference will sort + # local merge requests first. + merge_requests.map do |merge_request| + merge_request.to_reference(@project) + end.sort.to_sentence(last_word_connector: ', or ') + end + + def emoji_icon(name, unicode = nil, aliases = []) + unicode ||= Emoji.emoji_filename(name) rescue "" + + content_tag :div, "", + class: "icon emoji-icon emoji-#{unicode}", + "data-emoji" => name, + "data-aliases" => aliases.join(" "), + "data-unicode-name" => unicode + end + + def emoji_author_list(notes, current_user) + list = notes.map do |note| + note.author == current_user ? "me" : note.author.name + end + + list.join(", ") + end + + def note_active_class(notes, current_user) + if current_user && notes.pluck(:author_id).include?(current_user.id) + "active" + else + "" + end + end + + def awards_sort(awards) + awards.sort_by do |award, notes| + if award == "thumbsup" + 0 + elsif award == "thumbsdown" + 1 + else + 2 + end + end.to_h end - # Required for Gitlab::Markdown::IssueReferenceFilter + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index ee04ace35d0..a2c3d4d2f32 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -100,13 +100,13 @@ module LabelsHelper Label.where(project_id: @projects) end - grouped_labels = Labels::GroupService.new(labels).execute + grouped_labels = GlobalLabel.build_collection(labels) grouped_labels.unshift(Label::None) grouped_labels.unshift(Label::Any) options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) end - # Required for Gitlab::Markdown::LabelReferenceFilter + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 728d877ace2..1dd07a2a220 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -8,14 +8,6 @@ module MergeRequestsHelper ) end - def new_mr_path_for_fork_from_push_event(event) - new_namespace_project_merge_request_path( - event.project.namespace, - event.project, - new_mr_from_push_event(event, event.project.forked_from_project) - ) - end - def new_mr_from_push_event(event, target_project) { merge_request: { @@ -35,7 +27,16 @@ module MergeRequestsHelper end def ci_build_details_path(merge_request) - merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + return nil unless build_url + + parsed_url = URI.parse(build_url) + + unless parsed_url.userinfo.blank? + parsed_url.userinfo = '' + end + + parsed_url.to_s end def merge_path_description(merge_request, separator) @@ -47,7 +48,11 @@ module MergeRequestsHelper end def issues_sentence(issues) - issues.map(&:to_reference).to_sentence + # Sorting based on the `#123` or `group/project#123` reference will sort + # local issues first. + issues.map do |issue| + issue.to_reference(@project) + end.sort.to_sentence end def mr_change_branches_path(merge_request) @@ -57,18 +62,21 @@ module MergeRequestsHelper source_project_id: @merge_request.source_project_id, target_project_id: @merge_request.target_project_id, source_branch: @merge_request.source_branch, - target_branch: nil - } + target_branch: @merge_request.target_branch, + }, + change_branches: true ) end def source_branch_with_namespace(merge_request) + branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) + if merge_request.for_fork? namespace = link_to(merge_request.source_project_namespace, project_path(merge_request.source_project)) - namespace + ":#{merge_request.source_branch}" + namespace + ":" + branch else - merge_request.source_branch + branch end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 37a5b58cce8..a42cbcff182 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -28,7 +28,9 @@ module MilestonesHelper Milestone.where(project_id: @projects) end.active - grouped_milestones = Milestones::GroupService.new(milestones).execute + epoch = DateTime.parse('1970-01-01') + grouped_milestones = GlobalMilestone.build_collection(milestones) + grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::Any) diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index b3132a1f3ba..faba418c4db 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -1,10 +1,10 @@ module NamespacesHelper - def namespaces_options(selected = :current_user, scope = :default) + def namespaces_options(selected = :current_user, display_path: false) groups = current_user.owned_groups + current_user.masters_groups users = [current_user.namespace] - group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ] - users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ] + group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ] + users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ] options = [] options << group_opts @@ -17,15 +17,6 @@ module NamespacesHelper grouped_options_for_select(options, selected) end - def namespace_select_tag(id, opts = {}) - css_class = "ajax-namespace-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - - hidden_field_tag(id, value, class: css_class) - end - def namespace_icon(namespace, size = 40) if namespace.kind_of?(Group) group_icon(namespace) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 9b1dd8b8e54..e6fb8670e57 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -5,6 +5,14 @@ module NavHelper def nav_sidebar_class if nav_menu_collapsed? + "sidebar-collapsed" + else + "sidebar-expanded" + end + end + + def page_sidebar_class + if nav_menu_collapsed? "page-sidebar-collapsed" else "page-sidebar-expanded" diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index cf11f8e5320..499c655d2bf 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -16,40 +16,28 @@ module NotificationsHelper def notification_list_item(notification_level, user_membership) case notification_level when Notification::N_DISABLED - content_tag(:li, class: active_level_for(user_membership, Notification::N_DISABLED)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_DISABLED } do - icon('microphone-slash fw', text: 'Disabled') - end - end + update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash') when Notification::N_PARTICIPATING - content_tag(:li, class: active_level_for(user_membership, Notification::N_PARTICIPATING)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_PARTICIPATING } do - icon('volume-up fw', text: 'Participate') - end - end + update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up') when Notification::N_WATCH - content_tag(:li, class: active_level_for(user_membership, Notification::N_WATCH)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_WATCH } do - icon('eye fw', text: 'Watch') - end - end + update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye') when Notification::N_MENTION - content_tag(:li, class: active_level_for(user_membership, Notification::N_MENTION)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_MENTION } do - icon('at fw', text: 'On mention') - end - end + update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at') when Notification::N_GLOBAL - content_tag(:li, class: active_level_for(user_membership, Notification::N_GLOBAL)) do - link_to '#', class: 'update-notification', data: { notification_level: Notification::N_GLOBAL } do - icon('globe fw', text: 'Global') - end - end + update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe') else # do nothing end end + def update_notification_link(notification_level, user_membership, title, icon) + content_tag(:li, class: active_level_for(user_membership, notification_level)) do + link_to '#', class: 'update-notification', data: { notification_level: notification_level } do + icon("#{icon} fw", text: title) + end + end + end + def notification_label(user_membership) Notification.new(user_membership).to_s end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 775cf5a3dd4..791cb9e50bd 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -4,7 +4,82 @@ module PageLayoutHelper @page_title.push(*titles.compact) if titles.any? - @page_title.join(" | ") + # Segments are seperated by middot + @page_title.join(" \u00b7 ") + end + + # Define or get a description for the current page + # + # description - String (default: nil) + # + # If this helper is called multiple times with an argument, only the last + # description will be returned when called without an argument. Descriptions + # have newlines replaced with spaces and all HTML tags are sanitized. + # + # Examples: + # + # page_description # => "GitLab Community Edition" + # page_description("Foo") + # page_description # => "Foo" + # + # page_description("<b>Bar</b>\nBaz") + # page_description # => "Bar Baz" + # + # Returns an HTML-safe String. + def page_description(description = nil) + @page_description ||= page_description_default + + if description.present? + @page_description = description.squish + else + sanitize(@page_description, tags: []).truncate_words(30) + end + end + + # Default value for page_description when one hasn't been defined manually by + # a view + def page_description_default + if @project + @project.description || brand_title + else + brand_title + end + end + + def page_image + default = image_url('gitlab_logo.png') + + if @project + @project.avatar_url || default + elsif @user + avatar_icon(@user) + else + default + end + end + + # Define or get attributes to be used as Twitter card metadata + # + # map - Hash of label => data pairs. Keys become labels, values become data + # + # Raises ArgumentError if given more than two attributes + def page_card_attributes(map = {}) + raise ArgumentError, 'cannot provide more than two attributes' if map.length > 2 + + @page_card_attributes ||= {} + @page_card_attributes = map.reject { |_,v| v.blank? } if map.present? + @page_card_attributes + end + + def page_card_meta_tags + tags = '' + + page_card_attributes.each_with_index do |pair, i| + tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0]) + tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1]) + end + + tags.html_safe end def header_title(title = nil, title_url = nil) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5301c2ccf76..77ba612548a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -21,7 +21,7 @@ module ProjectsHelper end def link_to_member(project, author, opts = {}) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author' } + default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } opts = default_opts.merge(opts) return "(deleted)" unless author @@ -39,7 +39,8 @@ module ProjectsHelper if opts[:name] link_to(author_html, user_path(author), class: "author_link").html_safe else - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe + title = opts[:title].sub(":name", sanitize(author.name)) + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe end end @@ -104,6 +105,14 @@ module ProjectsHelper end end + def user_max_access_in_project(user_id, project) + level = project.team.max_member_access(user_id) + + if level + Gitlab::Access.options_with_owner.key(level) + end + end + private def get_project_nav_tabs(project, current_user) @@ -117,7 +126,7 @@ module ProjectsHelper nav_tabs << :merge_requests end - if project.gitlab_ci? && can?(current_user, :read_build, project) + if project.builds_enabled? && can?(current_user, :read_build, project) nav_tabs << :builds end @@ -173,13 +182,20 @@ module ProjectsHelper 'unknown' end - def default_url_to_repo(project = nil) - project = project || @project - current_user ? project.url_to_repo : project.http_url_to_repo + def default_url_to_repo(project = @project) + if default_clone_protocol == "ssh" + project.ssh_url_to_repo + else + project.http_url_to_repo + end end def default_clone_protocol - current_user ? "ssh" : "http" + if !current_user || current_user.require_ssh_key? + "http" + else + "ssh" + end end def project_last_activity(project) @@ -253,14 +269,6 @@ module ProjectsHelper filename_path(project, :version) end - def hidden_pass_url(original_url) - result = URI(original_url) - result.password = '*****' unless result.password.nil? - result - rescue - original_url - end - def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } namespace_project_wiki_path(proj.namespace, proj, page, url_params) @@ -277,14 +285,6 @@ module ProjectsHelper end end - def user_max_access_in_project(user, project) - level = project.team.max_member_access(user) - - if level - Gitlab::Access.options_with_owner.key(level) - end - end - def leave_project_message(project) "Are you sure you want to leave \"#{project.name}\" project?" end @@ -330,10 +330,9 @@ module ProjectsHelper def filename_path(project, filename) if project && blob = project.repository.send(filename) namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - blob.name) + project.namespace, + project, + tree_join(project.default_branch, blob.name) ) end end diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index 46eb82a354f..9fb42487a75 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -19,7 +19,7 @@ module RunnersHelper id = "\##{runner.id}" if current_user && current_user.admin - link_to ci_admin_runner_path(runner) do + link_to admin_runner_path(runner) do display_name + id end else diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index c31a556ff7b..d4f78258626 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -70,7 +70,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - current_user.authorized_groups.search(term).limit(limit).map do |group| + Group.search(term).limit(limit).map do |group| { label: "group: #{search_result_sanitize(group.name)}", url: group_path(group) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 12fce8db701..05386d790ca 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -15,12 +15,14 @@ module SelectsHelper html = { class: css_class, - 'data-placeholder' => placeholder, - 'data-null-user' => null_user, - 'data-any-user' => any_user, - 'data-email-user' => email_user, - 'data-first-user' => first_user, - 'data-current-user' => current_user + data: { + placeholder: placeholder, + null_user: null_user, + any_user: any_user, + email_user: email_user, + first_user: first_user, + current_user: current_user + } } unless opts[:scope] == :all @@ -35,8 +37,33 @@ module SelectsHelper end def groups_select_tag(id, opts = {}) - css_class = "ajax-groups-select " - css_class << "multiselect " if opts[:multiple] + opts[:class] ||= '' + opts[:class] << ' ajax-groups-select' + select2_tag(id, opts) + end + + def namespace_select_tag(id, opts = {}) + opts[:class] ||= '' + opts[:class] << ' ajax-namespace-select' + select2_tag(id, opts) + end + + def project_select_tag(id, opts = {}) + opts[:class] ||= '' + opts[:class] << ' ajax-project-select' + + unless opts.delete(:scope) == :all + if @group + opts['data-group-id'] = @group.id + end + end + + hidden_field_tag(id, opts[:selected], opts) + end + + def select2_tag(id, opts = {}) + css_class = '' + css_class << 'multiselect ' if opts[:multiple] css_class << (opts[:class] || '') value = opts[:selected] || '' diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 0e7d8065ac7..04e53fe7c61 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -110,22 +110,4 @@ module TabHelper 'active' end end - - # Use nav_tab for save controller/action but different params - def nav_tab(key, value, &block) - o = {} - o[:class] = "" - - if value.nil? - o[:class] << " active" if params[key].blank? - else - o[:class] << " active" if params[key] == value - end - - if block_given? - content_tag(:li, capture(&block), o) - else - content_tag(:li, nil, o) - end - end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 03a49e119b8..2ad7c80dae0 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -46,12 +46,51 @@ module TreeHelper File.join(*args) end - def allowed_tree_edit?(project = nil, ref = nil) + def on_top_of_branch?(project = @project, ref = @ref) + project.repository.branch_names.include?(ref) + end + + def can_edit_tree?(project = nil, ref = nil) project ||= @project ref ||= @ref - return false unless project.repository.branch_names.include?(ref) - ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + return false unless on_top_of_branch?(project, ref) + + can?(current_user, :push_code, project) || + (current_user && current_user.already_forked?(project)) + end + + def tree_edit_branch(project = @project, ref = @ref) + return unless can_edit_tree?(project, ref) + + if can_push_branch?(project, ref) + ref + else + project = tree_edit_project(project) + project.repository.next_patch_branch + end + end + + def tree_edit_project(project = @project) + if can?(current_user, :push_code, project) + project + elsif current_user && current_user.already_forked?(project) + current_user.fork_of(project) + end + end + + def edit_in_new_fork_notice_now + "You're not allowed to make changes to this project directly." + + " A fork of this project is being created that you can make changes in, so you can submit a merge request." + end + + def edit_in_new_fork_notice + "You're not allowed to make changes to this project directly." + + " A fork of this project has been created that you can make changes in, so you can submit a merge request." + end + + def commit_in_fork_help + "A new branch will be created in your fork and a new merge request will be started." end def tree_breadcrumbs(tree, max_links = 2) @@ -65,7 +104,7 @@ module TreeHelper part_path = File.join(part_path, part) unless part_path.empty? part_path = part if part_path.empty? - next unless parts.last(2).include?(part) if parts.count > max_links + next if parts.count > max_links && !parts.last(2).include?(part) yield(part, tree_join(@ref, part_path)) end end diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index 2a3a7e80fca..8cad994d10f 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,5 +1,5 @@ module TriggersHelper - def ci_build_trigger_url(project_id, ref_name) - "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" + def builds_trigger_url(project_id) + "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds" end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index b52cd23aba2..71d33b445c2 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -12,61 +12,41 @@ module VisibilityLevelHelper # Return the description for the +level+ argument. # - # +level+ One of the Gitlab::VisibilityLevel constants - # +form_model+ Either a model object (Project, Snippet, etc.) or the name of - # a Project or Snippet class. + # +level+ One of the Gitlab::VisibilityLevel constants + # +form_model+ Either a model object (Project, Snippet, etc.) or the name of + # a Project or Snippet class. def visibility_level_description(level, form_model) - case form_model.is_a?(String) ? form_model : form_model.class.name - when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' - snippet_visibility_level_description(level) - when 'Project' + case form_model + when Project project_visibility_level_description(level) + when Snippet + snippet_visibility_level_description(level, form_model) end end def project_visibility_level_description(level) - capture_haml do - haml_tag :span do - case level - when Gitlab::VisibilityLevel::PRIVATE - haml_concat "Project access must be granted explicitly for each user." - when Gitlab::VisibilityLevel::INTERNAL - haml_concat "The project can be cloned by" - haml_concat "any logged in user." - when Gitlab::VisibilityLevel::PUBLIC - haml_concat "The project can be cloned" - haml_concat "without any" - haml_concat "authentication." - end - end - end - end - - def snippet_visibility_level_description(level) - capture_haml do - haml_tag :span do - case level - when Gitlab::VisibilityLevel::PRIVATE - haml_concat "The snippet is visible only for me." - when Gitlab::VisibilityLevel::INTERNAL - haml_concat "The snippet is visible for any logged in user." - when Gitlab::VisibilityLevel::PUBLIC - haml_concat "The snippet can be accessed" - haml_concat "without any" - haml_concat "authentication." - end - end + case level + when Gitlab::VisibilityLevel::PRIVATE + "Project access must be granted explicitly to each user." + when Gitlab::VisibilityLevel::INTERNAL + "The project can be cloned by any logged in user." + when Gitlab::VisibilityLevel::PUBLIC + "The project can be cloned without any authentication." end end - def visibility_level_icon(level) + def snippet_visibility_level_description(level, snippet = nil) case level when Gitlab::VisibilityLevel::PRIVATE - private_icon + if snippet.is_a? ProjectSnippet + "The snippet is visible only to project members." + else + "The snippet is visible only to me." + end when Gitlab::VisibilityLevel::INTERNAL - internal_icon + "The snippet is visible to any logged in user." when Gitlab::VisibilityLevel::PUBLIC - public_icon + "The snippet can be accessed without any authentication." end end @@ -89,7 +69,6 @@ module VisibilityLevelHelper def skip_level?(form_model, level) form_model.is_a?(Project) && - form_model.forked? && - !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) + !form_model.visibility_level_allowed?(level) end end diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index f0c41f69a5c..d0ce827a595 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -2,11 +2,19 @@ class AbuseReportMailer < BaseMailer include Gitlab::CurrentSettings def notify(abuse_report_id) + return unless deliverable? + @abuse_report = AbuseReport.find(abuse_report_id) mail( - to: current_application_settings.admin_notification_email, + to: current_application_settings.admin_notification_email, subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse" ) end + + private + + def deliverable? + current_application_settings.admin_notification_email.present? + end end diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index aedb0889185..8b83bbd93b7 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -8,10 +8,6 @@ class BaseMailer < ActionMailer::Base default from: Proc.new { default_sender_address.format } default reply_to: Proc.new { default_reply_to_address.format } - def self.delay - delay_for(2.seconds) - end - def can? Ability.abilities.allowed?(current_user, action, subject) end diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb deleted file mode 100644 index 6fb4fba85e5..00000000000 --- a/app/mailers/ci/emails/builds.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Ci - module Emails - module Builds - def build_fail_email(build_id, to) - @build = Ci::Build.find(build_id) - @project = @build.project - mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) - end - - def build_success_email(build_id, to) - @build = Ci::Build.find(build_id) - @project = @build.project - mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) - end - end - end -end diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb deleted file mode 100644 index 404842cf213..00000000000 --- a/app/mailers/ci/notify.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Ci - class Notify < ActionMailer::Base - include Ci::Emails::Builds - - add_template_helper Ci::GitlabHelper - - default_url_options[:host] = Gitlab.config.gitlab.host - default_url_options[:protocol] = Gitlab.config.gitlab.protocol - default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? - default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root - - default from: Gitlab.config.gitlab.email_from - - # Just send email with 3 seconds delay - def self.delay - delay_for(2.seconds) - end - - private - - # Formats arguments into a String suitable for use as an email subject - # - # extra - Extra Strings to be inserted into the subject - # - # Examples - # - # >> subject('Lorem ipsum') - # => "GitLab-CI | Lorem ipsum" - # - # # Automatically inserts Project name when @project is set - # >> @project = Project.last - # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> - # >> subject('Lorem ipsum') - # => "GitLab-CI | Ruby on Rails | Lorem ipsum " - # - # # Accepts multiple arguments - # >> subject('Lorem ipsum', 'Dolor sit amet') - # => "GitLab-CI | Lorem ipsum | Dolor sit amet" - def subject(*extra) - subject = "GitLab-CI" - subject << (@project ? " | #{@project.name}" : "") - subject << " | " + extra.join(' | ') if extra.present? - subject - end - end -end diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb new file mode 100644 index 00000000000..d58609a2de5 --- /dev/null +++ b/app/mailers/emails/builds.rb @@ -0,0 +1,15 @@ +module Emails + module Builds + def build_fail_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) + end + + def build_success_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) + end + end +end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 2c035fbb70b..abdeefed5ef 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,53 +1,49 @@ module Emails module Issues def new_issue_email(recipient_id, issue_id) - @issue = Issue.find(issue_id) - @project = @issue.project - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_new_thread(@issue, - from: sender(@issue.author_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record(@issue, recipient_id, reply_key) + issue_mail_with_notification(issue_id, recipient_id) do + mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id)) + end end def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id) - @issue = Issue.find(issue_id) - @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - @project = @issue.project - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_answer_thread(@issue, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record(@issue, recipient_id, reply_key) + issue_mail_with_notification(issue_id, recipient_id) do + @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) + end end def closed_issue_email(recipient_id, issue_id, updated_by_user_id) - @issue = Issue.find issue_id - @project = @issue.project - @updated_by = User.find updated_by_user_id - @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_answer_thread(@issue, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record(@issue, recipient_id, reply_key) + issue_mail_with_notification(issue_id, recipient_id) do + @updated_by = User.find updated_by_user_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) + end end def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) - @issue = Issue.find issue_id - @issue_status = status + issue_mail_with_notification(issue_id, recipient_id) do + @issue_status = status + @updated_by = User.find updated_by_user_id + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) + end + end + + private + + def issue_thread_options(sender_id, recipient_id) + { + from: sender(sender_id), + to: recipient(recipient_id), + subject: subject("#{@issue.title} (##{@issue.iid})") + } + end + + def issue_mail_with_notification(issue_id, recipient_id) + @issue = Issue.find(issue_id) @project = @issue.project - @updated_by = User.find updated_by_user_id @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) - mail_answer_thread(@issue, - from: sender(updated_by_user_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) + + yield SentNotification.record(@issue, recipient_id, reply_key) end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 87ba94a583d..e1382d2da12 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,47 +1,52 @@ module Emails module Notes def note_commit_email(recipient_id, note_id) - @note = Note.find(note_id) - @commit = @note.noteable - @project = @note.project - @target_url = namespace_project_commit_url(@project.namespace, @project, - @commit, anchor: - "note_#{@note.id}") - mail_answer_thread(@commit, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@commit.title} (#{@commit.short_id})")) + note_mail_with_notification(note_id, recipient_id) do + @commit = @note.noteable + @target_url = namespace_project_commit_url(*note_target_url_options) - SentNotification.record_note(@note, recipient_id, reply_key) + mail_answer_thread(@commit, + from: sender(@note.author_id), + to: recipient(recipient_id), + subject: subject("#{@commit.title} (#{@commit.short_id})")) + end end def note_issue_email(recipient_id, note_id) - @note = Note.find(note_id) - @issue = @note.noteable - @project = @note.project - @target_url = namespace_project_issue_url(@project.namespace, @project, - @issue, anchor: - "note_#{@note.id}") - mail_answer_thread(@issue, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@issue.title} (##{@issue.iid})")) - - SentNotification.record_note(@note, recipient_id, reply_key) + note_mail_with_notification(note_id, recipient_id) do + @issue = @note.noteable + @target_url = namespace_project_issue_url(*note_target_url_options) + mail_answer_thread(@issue, note_thread_options(recipient_id)) + end end def note_merge_request_email(recipient_id, note_id) + note_mail_with_notification(note_id, recipient_id) do + @merge_request = @note.noteable + @target_url = namespace_project_merge_request_url(*note_target_url_options) + mail_answer_thread(@merge_request, note_thread_options(recipient_id)) + end + end + + private + + def note_target_url_options + [@project.namespace, @project, @note.noteable, anchor: "note_#{@note.id}"] + end + + def note_thread_options(recipient_id) + { + from: sender(@note.author_id), + to: recipient(recipient_id), + subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})") + } + end + + def note_mail_with_notification(note_id, recipient_id) @note = Note.find(note_id) - @merge_request = @note.noteable @project = @note.project - @target_url = namespace_project_merge_request_url(@project.namespace, - @project, - @merge_request, anchor: - "note_#{@note.id}") - mail_answer_thread(@merge_request, - from: sender(@note.author_id), - to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + yield SentNotification.record_note(@note, recipient_id, reply_key) end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index caba63006da..b96418679bd 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -59,85 +59,17 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id: nil, - ref: nil, - action: nil, - compare: nil, - reverse_compare: false, - send_from_committer_email: false, - disable_diffs: false) - unless author_id && ref && action - raise ArgumentError, "missing keywords: author_id, ref, action" - end + def repository_push_email(project_id, recipient, opts = {}) + @message = + Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) - @project = Project.find(project_id) - @current_user = @author = User.find(author_id) - @reverse_compare = reverse_compare - @compare = compare - @ref_name = Gitlab::Git.ref_name(ref) - @ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch" - @action = action - @disable_diffs = disable_diffs - - if @compare - @commits = Commit.decorate(compare.commits, @project) - @diffs = compare.diffs - end - - @action_name = - case action - when :create - "pushed new" - when :delete - "deleted" - else - "pushed to" - end - - @subject = "[Git]" - @subject << "[#{@project.path_with_namespace}]" - @subject << "[#{@ref_name}]" if action == :push - @subject << " " - - if action == :push - if @commits.length > 1 - @target_url = namespace_project_compare_url(@project.namespace, - @project, - from: Commit.new(@compare.base, @project), - to: Commit.new(@compare.head, @project)) - @subject << "Deleted " if @reverse_compare - @subject << "#{@commits.length} commits: #{@commits.first.title}" - else - @target_url = namespace_project_commit_url(@project.namespace, - @project, @commits.first) - - @subject << "Deleted 1 commit: " if @reverse_compare - @subject << @commits.first.title - end - else - unless action == :delete - @target_url = namespace_project_tree_url(@project.namespace, - @project, @ref_name) - end - - subject_action = @action_name.dup - subject_action[0] = subject_action[0].capitalize - @subject << "#{subject_action} #{@ref_type} #{@ref_name}" - end - - @disable_footer = true - - reply_to = - if send_from_committer_email && can_send_from_user_email?(@author) - @author.email - else - Gitlab.config.gitlab.email_reply_to - end - - mail(from: sender(author_id, send_from_committer_email), - reply_to: reply_to, - to: recipient, - subject: @subject) + # used in notify layout + @target_url = @message.target_url + + mail(from: sender(@message.author_id, @message.send_from_committer_email?), + reply_to: @message.reply_to, + to: @message.recipient, + subject: @message.subject) end end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 50a409c3754..3bbdd9cee76 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -7,6 +7,7 @@ class Notify < BaseMailer include Emails::Projects include Emails::Profile include Emails::Groups + include Emails::Builds add_template_helper MergeRequestsHelper add_template_helper EmailsHelper @@ -16,7 +17,7 @@ class Notify < BaseMailer subject: subject, body: body.html_safe, content_type: 'text/html' - ) + ) end # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com", @@ -33,13 +34,13 @@ class Notify < BaseMailer allowed_domains end - private - def can_send_from_user_email?(sender) sender_domain = sender.email.split("@").last self.class.allowed_email_domains.include?(sender_domain) end + private + # Return an email address that displays the name of the sender. # Only the displayed name changes; the actual email address is always the same. def sender(sender_id, send_from_user_email = false) diff --git a/app/models/ability.rb b/app/models/ability.rb index b72178fa126..5a1a67db8e1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,8 +1,8 @@ class Ability class << self def allowed(user, subject) - return not_auth_abilities(user, subject) if user.nil? - return [] unless user.kind_of?(User) + return anonymous_abilities(user, subject) if user.nil? + return [] unless user.is_a?(User) return [] if user.blocked? case subject.class.name @@ -15,19 +15,30 @@ class Ability when "Group" then group_abilities(user, subject) when "Namespace" then namespace_abilities(user, subject) when "GroupMember" then group_member_abilities(user, subject) + when "ProjectMember" then project_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end - # List of possible abilities - # for non-authenticated user - def not_auth_abilities(user, subject) - project = if subject.kind_of?(Project) + # List of possible abilities for anonymous user + def anonymous_abilities(user, subject) + case true + when subject.is_a?(PersonalSnippet) + anonymous_personal_snippet_abilities(subject) + when subject.is_a?(Project) || subject.respond_to?(:project) + anonymous_project_abilities(subject) + when subject.is_a?(Group) || subject.respond_to?(:group) + anonymous_group_abilities(subject) + else + [] + end + end + + def anonymous_project_abilities(subject) + project = if subject.is_a?(Project) subject - elsif subject.respond_to?(:project) - subject.project else - nil + subject.project end if project && project.public? @@ -47,19 +58,29 @@ class Ability rules - project_disabled_features_rules(project) else - group = if subject.kind_of?(Group) - subject - elsif subject.respond_to?(:group) - subject.group - else - nil - end + [] + end + end - if group && group.public_profile? - [:read_group] - else - [] - end + def anonymous_group_abilities(subject) + group = if subject.is_a?(Group) + subject + else + subject.group + end + + if group && group.projects.public_only.any? + [:read_group] + else + [] + end + end + + def anonymous_personal_snippet_abilities(snippet) + if snippet.public? + [:read_personal_snippet] + else + [] end end @@ -111,14 +132,14 @@ class Ability end def public_project_rules - project_guest_rules + [ + @public_project_rules ||= project_guest_rules + [ :download_code, :fork_project ] end def project_guest_rules - [ + @project_guest_rules ||= [ :read_project, :read_wiki, :read_issue, @@ -136,7 +157,7 @@ class Ability end def project_report_rules - project_guest_rules + [ + @project_report_rules ||= project_guest_rules + [ :create_commit_status, :read_commit_statuses, :download_code, @@ -149,17 +170,18 @@ class Ability end def project_dev_rules - project_report_rules + [ + @project_dev_rules ||= project_report_rules + [ :admin_merge_request, :create_merge_request, :create_wiki, :manage_builds, + :download_build_artifacts, :push_code ] end def project_archived_rules - [ + @project_archived_rules ||= [ :create_merge_request, :push_code, :push_code_to_protected_branches, @@ -169,7 +191,7 @@ class Ability end def project_master_rules - project_dev_rules + [ + @project_master_rules ||= project_dev_rules + [ :push_code_to_protected_branches, :update_project_snippet, :update_merge_request, @@ -184,7 +206,7 @@ class Ability end def project_admin_rules - project_master_rules + [ + @project_admin_rules ||= project_master_rules + [ :change_namespace, :change_visibility_level, :rename_project, @@ -230,18 +252,19 @@ class Ability # Only group masters and group owners can create new projects in group if group.has_master?(user) || group.has_owner?(user) || user.admin? - rules.push(*[ + rules += [ :create_projects, - ]) + :admin_milestones + ] end # Only group owner and administrators can admin group if group.has_owner?(user) || user.admin? - rules.push(*[ + rules += [ :admin_group, :admin_namespace, :admin_group_member - ]) + ] end rules.flatten @@ -252,16 +275,15 @@ class Ability # Only namespace owner and administrators can admin it if namespace.owner == user || user.admin? - rules.push(*[ + rules += [ :create_projects, :admin_namespace - ]) + ] end rules.flatten end - [:issue, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| rules = [] @@ -278,7 +300,7 @@ class Ability end end - [:note, :project_snippet, :personal_snippet].each do |name| + [:note, :project_snippet].each do |name| define_method "#{name}_abilities" do |user, subject| rules = [] @@ -298,19 +320,57 @@ class Ability end end + def personal_snippet_abilities(user, snippet) + rules = [] + + if snippet.author == user + rules += [ + :read_personal_snippet, + :update_personal_snippet, + :admin_personal_snippet + ] + end + + if snippet.public? || snippet.internal? + rules << :read_personal_snippet + end + + rules + end + def group_member_abilities(user, subject) rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:admin_group_member) - if can_manage && (user != target_user) - rules << :update_group_member - rules << :destroy_group_member + unless group.last_owner?(target_user) + can_manage = group_abilities(user, group).include?(:admin_group_member) + + if can_manage + rules << :update_group_member + rules << :destroy_group_member + elsif user == target_user + rules << :destroy_group_member + end end - if !group.last_owner?(user) && (can_manage || (user == target_user)) - rules << :destroy_group_member + rules + end + + def project_member_abilities(user, subject) + rules = [] + target_user = subject.user + project = subject.project + + unless target_user == project.owner + can_manage = project_abilities(user, project).include?(:admin_project_member) + + if can_manage + rules << :update_project_member + rules << :destroy_project_member + elsif user == target_user + rules << :destroy_project_member + end end rules @@ -318,10 +378,10 @@ class Ability def abilities @abilities ||= begin - abilities = Six.new - abilities << self - abilities - end + abilities = Six.new + abilities << self + abilities + end end private diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 89b3116b9f2..55864236b2f 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -18,4 +18,10 @@ class AbuseReport < ActiveRecord::Base validates :user, presence: true validates :message, presence: true validates :user_id, uniqueness: true + + def notify + return unless self.persisted? + + AbuseReportMailer.notify(self.id).deliver_later + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 05430c2ee18..be69d317d73 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -2,51 +2,74 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null +# runners_registration_token :string(255) +# require_two_factor_authentication :boolean default(TRUE) +# two_factor_grace_period :integer default(48) # class ApplicationSetting < ActiveRecord::Base + include TokenAuthenticatable + add_authentication_token_field :runners_registration_token + + CACHE_KEY = 'application_setting.last' + serialize :restricted_visibility_levels serialize :import_sources serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw validates :session_expire_delay, - presence: true, - numericality: { only_integer: true, greater_than_or_equal_to: 0 } + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :home_page_url, - allow_blank: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, - if: :home_page_url_column_exist + allow_blank: true, + url: true, + if: :home_page_url_column_exist validates :after_sign_out_path, - allow_blank: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + allow_blank: true, + url: true validates :admin_notification_email, - allow_blank: true, - email: true + allow_blank: true, + email: true + + validates :two_factor_grace_period, + numericality: { greater_than_or_equal_to: 0 } + + validates :recaptcha_site_key, + presence: true, + if: :recaptcha_enabled + + validates :recaptcha_private_key, + presence: true, + if: :recaptcha_enabled validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? @@ -68,8 +91,20 @@ class ApplicationSetting < ActiveRecord::Base end end + before_save :ensure_runners_registration_token + + after_commit do + Rails.cache.write(CACHE_KEY, self) + end + def self.current - ApplicationSetting.last + Rails.cache.fetch(CACHE_KEY) do + ApplicationSetting.last + end + end + + def self.expire + Rails.cache.delete(CACHE_KEY) end def self.create_from_defaults @@ -87,7 +122,11 @@ class ApplicationSetting < ActiveRecord::Base default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], - import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], + shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], + max_artifacts_size: Settings.artifacts['max_size'], + require_two_factor_authentication: false, + two_factor_grace_period: 48 ) end @@ -102,13 +141,16 @@ class ApplicationSetting < ActiveRecord::Base def restricted_signup_domains_raw=(values) self.restricted_signup_domains = [] self.restricted_signup_domains = values.split( - /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace - | # or - \s # any whitespace character - | # or - [\r\n] # any number of newline characters - /x) + /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace + | # or + \s # any whitespace character + | # or + [\r\n] # any number of newline characters + /x) self.restricted_signup_domains.reject! { |d| d.empty? } end + def runners_registration_token + ensure_runners_registration_token! + end end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 05f5e979695..ad514706160 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -16,12 +16,12 @@ class BroadcastMessage < ActiveRecord::Base include Sortable - validates :message, presence: true + validates :message, presence: true validates :starts_at, presence: true - validates :ends_at, presence: true + validates :ends_at, presence: true - validates :color, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true - validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true + validates :color, allow_blank: true, color: true + validates :font, allow_blank: true, color: true def self.current where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb deleted file mode 100644 index 0cf496f7d81..00000000000 --- a/app/models/ci/application_setting.rb +++ /dev/null @@ -1,27 +0,0 @@ -# == Schema Information -# -# Table name: application_settings -# -# id :integer not null, primary key -# all_broken_builds :boolean -# add_pusher :boolean -# created_at :datetime -# updated_at :datetime -# - -module Ci - class ApplicationSetting < ActiveRecord::Base - extend Ci::Model - - def self.current - Ci::ApplicationSetting.last - end - - def self.create_from_defaults - create( - all_broken_builds: Settings.gitlab_ci['all_broken_builds'], - add_pusher: Settings.gitlab_ci['add_pusher'], - ) - end - end -end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b19e2ac1363..d7fccb2197d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: builds +# Table name: ci_builds # # id :integer not null, primary key # project_id :integer @@ -11,16 +11,24 @@ # updated_at :datetime # started_at :datetime # runner_id :integer -# commit_id :integer # coverage :float +# commit_id :integer # commands :text # job_id :integer # name :string(255) +# deploy :boolean default(FALSE) # options :text # allow_failure :boolean default(FALSE), not null # stage :string(255) -# deploy :boolean default(FALSE) # trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text # module Ci @@ -39,11 +47,15 @@ module Ci scope :ignore_failures, ->() { where(allow_failure: false) } scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } + mount_uploader :artifacts_file, ArtifactUploader + acts_as_taggable # To prevent db load megabytes of data from trace default_scope -> { select(Ci::Build.columns_without_lazy) } + before_destroy { project } + class << self def columns_without_lazy (column_names - LAZY_ATTRIBUTES).map do |column_name| @@ -74,6 +86,7 @@ module Ci new_build.options = build.options new_build.commands = build.commands new_build.tag_list = build.tag_list + new_build.gl_project_id = build.gl_project_id new_build.commit_id = build.commit_id new_build.name = build.name new_build.allow_failure = build.allow_failure @@ -86,19 +99,16 @@ module Ci end state_machine :status, initial: :pending do - after_transition any => [:success, :failed, :canceled] do |build, transition| - project = build.project + after_transition pending: :running do |build, transition| + build.execute_hooks + end - if project.web_hooks? - Ci::WebHookService.new.build_end(build) - end + after_transition any => [:success, :failed, :canceled] do |build, transition| + return unless build.project + build.update_coverage build.commit.create_next_builds(build) - project.execute_services(build) - - if project.coverage_enabled? - build.update_coverage - end + build.execute_hooks end end @@ -106,21 +116,35 @@ module Ci failed? && allow_failure? end + def retryable? + project.builds_enabled? && commands.present? + end + + def retried? + !self.commit.latest_builds_for_ref(self.ref).include?(self) + end + def trace_html html = Ci::Ansi2html::convert(trace) if trace.present? html || '' end def timeout - project.timeout + project.build_timeout end def variables predefined_variables + yaml_variables + project_variables + trigger_variables end - def project - commit.project + def merge_request + merge_requests = MergeRequest.includes(:merge_request_diff) + .where(source_branch: ref, source_project_id: commit.gl_project_id) + .reorder(iid: :asc) + + merge_requests.find do |merge_request| + merge_request.commits.any? { |ci| ci.id == commit.sha } + end end def project_id @@ -131,26 +155,21 @@ module Ci project.name end - def project_recipients - recipients = project.email_recipients.split(' ') - - if project.email_add_pusher? && user.present? && user.notification_email.present? - recipients << user.notification_email - end - - recipients.uniq - end - def repo_url - project.repo_url_with_auth + auth = "gitlab-ci-token:#{token}@" + project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| + prefix + auth + end end def allow_git_fetch - project.allow_git_fetch + project.build_allow_git_fetch end def update_coverage - coverage = extract_coverage(trace, project.coverage_regex) + coverage_regex = project.build_coverage_regex + return unless coverage_regex + coverage = extract_coverage(trace, coverage_regex) if coverage.is_a? Numeric update_attributes(coverage: coverage) @@ -159,7 +178,8 @@ module Ci def extract_coverage(text, regex) begin - matches = text.gsub(Regexp.new(regex)).to_a.last + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) coverage = matches.gsub(/\d+(\.\d+)?/).first if coverage.present? @@ -172,8 +192,11 @@ module Ci end def raw_trace - if File.exist?(path_to_trace) + if File.file?(path_to_trace) File.read(path_to_trace) + elsif project.ci_id && File.file?(old_path_to_trace) + # Temporary fix for build trace data integrity + File.read(old_path_to_trace) else # backward compatibility read_attribute :trace @@ -183,15 +206,15 @@ module Ci def trace trace = raw_trace if project && trace.present? - trace.gsub(project.token, 'xxxxxx') + trace.gsub(project.runners_token, 'xxxxxx') else trace end end def trace=(trace) - unless Dir.exists? dir_to_trace - FileUtils.mkdir_p dir_to_trace + unless Dir.exists?(dir_to_trace) + FileUtils.mkdir_p(dir_to_trace) end File.write(path_to_trace, trace) @@ -209,22 +232,79 @@ module Ci "#{dir_to_trace}/#{id}.log" end + ## + # Deprecated + # + # This is a hotfix for CI build data integrity, see #4246 + # Should be removed in 8.4, after CI files migration has been done. + # + def old_dir_to_trace + File.join( + Settings.gitlab_ci.builds_path, + created_at.utc.strftime("%Y_%m"), + project.ci_id.to_s + ) + end + + ## + # Deprecated + # + # This is a hotfix for CI build data integrity, see #4246 + # Should be removed in 8.4, after CI files migration has been done. + # + def old_path_to_trace + "#{old_dir_to_trace}/#{id}.log" + end + + ## + # Deprecated + # + # This contains a hotfix for CI build data integrity, see #4246 + # + # This method is used by `ArtifactUploader` to create a store_dir. + # Warning: Uploader uses it after AND before file has been stored. + # + # This method returns old path to artifacts only if it already exists. + # + def artifacts_path + old = File.join(created_at.utc.strftime('%Y_%m'), + project.ci_id.to_s, + id.to_s) + + old_store = File.join(ArtifactUploader.artifacts_path, old) + return old if project.ci_id && File.directory?(old_store) + + File.join( + created_at.utc.strftime('%Y_%m'), + project.id.to_s, + id.to_s + ) + end + + def token + project.runners_token + end + + def valid_token? token + project.valid_runners_token? token + end + def target_url Gitlab::Application.routes.url_helpers. - namespace_project_build_url(gl_project.namespace, gl_project, self) + namespace_project_build_url(project.namespace, project, self) end def cancel_url if active? Gitlab::Application.routes.url_helpers. - cancel_namespace_project_build_path(gl_project.namespace, gl_project, self) + cancel_namespace_project_build_path(project.namespace, project, self) end end def retry_url - if commands.present? + if retryable? Gitlab::Application.routes.url_helpers. - retry_namespace_project_build_path(gl_project.namespace, gl_project, self) + retry_namespace_project_build_path(project.namespace, project, self) end end @@ -240,6 +320,21 @@ module Ci pending? && !any_runners_online? end + def download_url + if artifacts_file.exists? + Gitlab::Application.routes.url_helpers. + download_namespace_project_build_path(project.namespace, project, self) + end + end + + def execute_hooks + build_data = Gitlab::BuildDataBuilder.build(self) + project.execute_hooks(build_data.dup, :build_hooks) + project.execute_services(build_data.dup, :build_hooks) + end + + + private def yaml_variables diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 13437b2483f..d2a29236942 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -1,26 +1,27 @@ # == Schema Information # -# Table name: commits +# Table name: ci_commits # -# id :integer not null, primary key -# project_id :integer -# ref :string(255) -# sha :string(255) -# before_sha :string(255) -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# gl_project_id :integer # module Ci class Commit < ActiveRecord::Base extend Ci::Model - belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id - has_many :statuses, dependent: :destroy, class_name: 'CommitStatus' + 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' @@ -37,10 +38,6 @@ module Ci sha end - def project - @project ||= gl_project.ensure_gitlab_ci_project - end - def project_id project.id end @@ -56,7 +53,7 @@ module Ci end def valid_commit_sha - if self.sha == Ci::Git::BLANK_SHA + if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") end end @@ -78,7 +75,7 @@ module Ci end def commit_data - @commit ||= gl_project.commit(sha) + @commit ||= project.commit(sha) rescue nil end @@ -164,21 +161,31 @@ module Ci status == 'canceled' end + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + def duration duration_array = latest_statuses.map(&:duration).compact duration_array.reduce(:+).to_i end + def started_at + @started_at ||= statuses.order('started_at ASC').first.try(:started_at) + end + def finished_at @finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at) end def coverage - if project.coverage_enabled? - coverage_array = latest_builds.map(&:coverage).compact - if coverage_array.size >= 1 - '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) - end + coverage_array = latest_builds.map(&:coverage).compact + if coverage_array.size >= 1 + '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end end @@ -187,18 +194,18 @@ module Ci end def config_processor - @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file) - rescue Ci::GitlabCiYamlProcessor::ValidationError => e + return nil unless ci_yaml_file + @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) + rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e save_yaml_error(e.message) nil - rescue Exception => e - logger.error e.message + "\n" + e.backtrace.join("\n") - save_yaml_error("Undefined yaml error") + rescue + save_yaml_error("Undefined error") nil end def ci_yaml_file - gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data + @ci_yaml_file ||= project.repository.blob_at(sha, '.gitlab-ci.yml').data rescue nil end diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb deleted file mode 100644 index cac3a7a49c1..00000000000 --- a/app/models/ci/event.rb +++ /dev/null @@ -1,27 +0,0 @@ -# == Schema Information -# -# Table name: events -# -# id :integer not null, primary key -# project_id :integer -# user_id :integer -# is_admin :integer -# description :text -# created_at :datetime -# updated_at :datetime -# - -module Ci - class Event < ActiveRecord::Base - extend Ci::Model - - belongs_to :project, class_name: 'Ci::Project' - - validates :description, - presence: true, - length: { in: 5..200 } - - scope :admin, ->(){ where(is_admin: true) } - scope :project_wide, ->(){ where(is_admin: false) } - end -end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb deleted file mode 100644 index eb65c773570..00000000000 --- a/app/models/ci/project.rb +++ /dev/null @@ -1,215 +0,0 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string(255) not null -# timeout :integer default(3600), not null -# created_at :datetime -# updated_at :datetime -# token :string(255) -# default_ref :string(255) -# path :string(255) -# always_build :boolean default(FALSE), not null -# polling_interval :integer -# public :boolean default(FALSE), not null -# ssh_url_to_repo :string(255) -# gitlab_id :integer -# allow_git_fetch :boolean default(TRUE), not null -# email_recipients :string(255) default(""), not null -# email_add_pusher :boolean default(TRUE), not null -# email_only_broken_builds :boolean default(TRUE), not null -# skip_refs :string(255) -# coverage_regex :string(255) -# shared_runners_enabled :boolean default(FALSE) -# generated_yaml_config :text -# - -module Ci - class Project < ActiveRecord::Base - extend Ci::Model - - include Ci::ProjectStatus - - belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id - - has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' - has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' - has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook' - has_many :events, dependent: :destroy, class_name: 'Ci::Event' - has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' - has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' - - # Project services - has_many :services, dependent: :destroy, class_name: 'Ci::Service' - has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService' - has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService' - has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService' - - accepts_nested_attributes_for :variables, allow_destroy: true - - delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project - - # - # Validations - # - validates_presence_of :timeout, :token, :default_ref, :gitlab_id - - validates_uniqueness_of :gitlab_id - - validates :polling_interval, - presence: true, - if: ->(project) { project.always_build.present? } - - before_validation :set_default_values - - class << self - include Ci::CurrentSettings - - def base_build_script - <<-eos - git submodule update --init - ls -la - eos - end - - def parse(project) - params = { - gitlab_id: project.id, - default_ref: project.default_branch || 'master', - email_add_pusher: current_application_settings.add_pusher, - email_only_broken_builds: current_application_settings.all_broken_builds, - } - - project = Ci::Project.new(params) - project.build_missing_services - project - end - - def already_added?(project) - where(gitlab_id: project.id).any? - end - - def unassigned(runner) - joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ - "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). - where("#{Ci::RunnerProject.table_name}.project_id" => nil) - end - - def ordered_by_last_commit_date - last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)" - joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id"). - order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") - end - end - - def name - name_with_namespace - end - - def path - path_with_namespace - end - - def gitlab_url - web_url - end - - def any_runners?(&block) - if runners.active.any?(&block) - return true - end - - shared_runners_enabled && Ci::Runner.shared.active.any?(&block) - end - - def set_default_values - self.token = SecureRandom.hex(15) if self.token.blank? - self.default_ref ||= 'master' - end - - def tracked_refs - @tracked_refs ||= default_ref.split(",").map { |ref| ref.strip } - end - - def valid_token? token - self.token && self.token == token - end - - def no_running_builds? - # Get running builds not later than 3 days ago to ignore hangs - builds.running.where("updated_at > ?", 3.days.ago).empty? - end - - def email_notification? - email_add_pusher || email_recipients.present? - end - - def web_hooks? - web_hooks.any? - end - - def services? - services.any? - end - - def timeout_in_minutes - timeout / 60 - end - - def timeout_in_minutes=(value) - self.timeout = value.to_i * 60 - end - - def coverage_enabled? - coverage_regex.present? - end - - # Build a clone-able repo url - # using http and basic auth - def repo_url_with_auth - auth = "gitlab-ci-token:#{token}@" - http_url_to_repo.sub(/^https?:\/\//) do |prefix| - prefix + auth - end - end - - def available_services_names - %w(slack mail hip_chat) - end - - def build_missing_services - available_services_names.each do |service_name| - service = services.find { |service| service.to_param == service_name } - - # If service is available but missing in db - # we should create an instance. Ex `create_gitlab_ci_service` - self.send :"create_#{service_name}_service" if service.nil? - end - end - - def execute_services(data) - services.each do |service| - - # Call service hook only if it is active - begin - service.execute(data) if service.active && service.can_execute?(data) - rescue => e - logger.error(e) - end - end - end - - def setup_finished? - commits.any? - end - - def commits - gl_project.ci_commits.ordered - end - - def builds - gl_project.ci_builds - end - end -end diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb deleted file mode 100644 index b66f1212f23..00000000000 --- a/app/models/ci/project_status.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Ci - module ProjectStatus - def status - last_commit.status if last_commit - end - - def broken? - last_commit.failed? if last_commit - end - - def success? - last_commit.success? if last_commit - end - - def broken_or_success? - broken? || success? - end - - def last_commit - @last_commit ||= commits.last if commits.any? - end - - def last_commit_date - last_commit.try(:created_at) - end - - def human_status - status - end - - def last_commit_for_ref(ref) - commits.where(ref: ref).last - end - end -end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 1b3669f1b7a..38b20cd7faa 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: runners +# Table name: ci_runners # # id :integer not null, primary key # token :string(255) @@ -25,7 +25,7 @@ module Ci has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' - has_many :projects, through: :runner_projects, class_name: 'Ci::Project' + has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' @@ -36,6 +36,7 @@ module Ci scope :active, ->() { where(active: true) } scope :paused, ->() { where(active: false) } scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } + scope :ordered, ->() { order(id: :desc) } acts_as_taggable @@ -44,10 +45,6 @@ module Ci query: "%#{query.try(:downcase)}%") end - def gl_projects_ids - projects.select(:gitlab_id) - end - def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 44453ee4b41..93d9be144e8 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: runner_projects +# Table name: ci_runner_projects # # id :integer not null, primary key # runner_id :integer not null @@ -14,8 +14,8 @@ module Ci extend Ci::Model belongs_to :runner, class_name: 'Ci::Runner' - belongs_to :project, class_name: 'Ci::Project' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - validates_uniqueness_of :runner_id, scope: :project_id + validates_uniqueness_of :runner_id, scope: :gl_project_id end end diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb deleted file mode 100644 index ed5e3f940b6..00000000000 --- a/app/models/ci/service.rb +++ /dev/null @@ -1,105 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -# To add new service you should build a class inherited from Service -# and implement a set of methods -module Ci - class Service < ActiveRecord::Base - extend Ci::Model - - serialize :properties, JSON - - default_value_for :active, false - - after_initialize :initialize_properties - - belongs_to :project, class_name: 'Ci::Project' - - validates :project_id, presence: true - - def activated? - active - end - - def category - :common - end - - def initialize_properties - self.properties = {} if properties.nil? - end - - def title - # implement inside child - end - - def description - # implement inside child - end - - def help - # implement inside child - end - - def to_param - # implement inside child - end - - def fields - # implement inside child - [] - end - - def can_test? - project.builds.any? - end - - def can_execute?(build) - true - end - - def execute(build) - # implement inside child - end - - # Provide convenient accessor methods - # for each serialized property. - def self.prop_accessor(*args) - args.each do |arg| - class_eval %{ - def #{arg} - (properties || {})['#{arg}'] - end - - def #{arg}=(value) - self.properties ||= {} - self.properties['#{arg}'] = value - end - } - end - end - - def self.boolean_accessor(*args) - self.prop_accessor(*args) - - args.each do |arg| - class_eval %{ - def #{arg}? - ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) - end - } - end - end - end -end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index fe224b7dc70..23516709a41 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: triggers +# Table name: ci_triggers # # id :integer not null, primary key # token :string(255) @@ -16,7 +16,7 @@ module Ci acts_as_paranoid - belongs_to :project, class_name: 'Ci::Project' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' validates_presence_of :token diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 29cd9553394..9973d2e5ade 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: trigger_requests +# Table name: ci_trigger_requests # # id :integer not null, primary key # trigger_id :integer not null diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 7a542802fa6..56759d3e50f 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: variables +# Table name: ci_variables # # id :integer not null, primary key # project_id :integer not null @@ -15,10 +15,10 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model - belongs_to :project, class_name: 'Ci::Project' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id validates_presence_of :key - validates_uniqueness_of :key, scope: :project_id + validates_uniqueness_of :key, scope: :gl_project_id attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base end diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb deleted file mode 100644 index 8f03b0625da..00000000000 --- a/app/models/ci/web_hook.rb +++ /dev/null @@ -1,44 +0,0 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(255) not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - -module Ci - class WebHook < ActiveRecord::Base - extend Ci::Model - - include HTTParty - - belongs_to :project, class_name: 'Ci::Project' - - # HTTParty timeout - default_timeout 10 - - validates :url, presence: true, - format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } - - def execute(data) - parsed_url = URI.parse(url) - if parsed_url.userinfo.blank? - Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) - else - post_url = url.gsub("#{parsed_url.userinfo}@", "") - auth = { - username: URI.decode(parsed_url.user), - password: URI.decode(parsed_url.password), - } - Ci::WebHook.post(post_url, - body: data.to_json, - headers: { "Content-Type" => "application/json" }, - verify: false, - basic_auth: auth) - end - end - end -end diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce3..0ba7b584d91 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -7,7 +7,7 @@ class Commit include Referable include StaticModel - attr_mentionable :safe_message + attr_mentionable :safe_message, pipeline: :single_line participant :author, :committer, :notes attr_accessor :project @@ -78,11 +78,23 @@ class Commit }x end + def self.link_reference_pattern + super("commit", /(?<commit>\h{6,40})/) + end + def to_reference(from_project = nil) if cross_project_reference?(from_project) - "#{project.to_reference}@#{id}" + project.to_reference + self.class.reference_prefix + self.id else - id + self.id + end + end + + def reference_link_text(from_project = nil) + if cross_project_reference?(from_project) + project.to_reference + self.class.reference_prefix + self.short_id + else + self.short_id end end @@ -135,10 +147,10 @@ class Commit description.present? end - def hook_attrs + def hook_attrs(with_changed_files: false) path_with_namespace = project.path_with_namespace - { + data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, @@ -148,6 +160,12 @@ class Commit email: author_email } } + + if with_changed_files + data.merge!(repo_changes) + end + + data end # Discover issues should be closed when this commit is pushed to a project's @@ -157,11 +175,11 @@ class Commit end def author - @author ||= User.find_by_any_email(author_email) + @author ||= User.find_by_any_email(author_email.downcase) end def committer - @committer ||= User.find_by_any_email(committer_email) + @committer ||= User.find_by_any_email(committer_email.downcase) end def parents @@ -196,4 +214,22 @@ class Commit def status ci_commit.try(:status) || :not_found end + + private + + def repo_changes + changes = { added: [], modified: [], removed: [] } + + diffs.each do |diff| + if diff.deleted_file + changes[:removed] << diff.old_path + elsif diff.renamed_file || diff.new_file + changes[:added] << diff.new_path + else + changes[:modified] << diff.new_path + end + end + + changes + end end diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 86fc9eb01a3..14e7971fa06 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -2,36 +2,38 @@ # # Examples: # -# range = CommitRange.new('f3f85602...e86e1013') +# range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false # range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # -# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae') +# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true # range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # -# # Assuming `project` is a Project with a repository containing both commits: -# range.project = project +# # Assuming the specified project has a repository containing both commits: # range.valid_commits? # => true # class CommitRange include ActiveModel::Conversion include Referable - attr_reader :sha_from, :notation, :sha_to + attr_reader :commit_from, :notation, :commit_to + attr_reader :ref_from, :ref_to # Optional Project model attr_accessor :project - # See `exclude_start?` - attr_reader :exclude_start - - # The beginning and ending SHAs can be between 6 and 40 hex characters, and + # The beginning and ending refs can be named or SHAs, and # the range notation can be double- or triple-dot. - PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ + REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/ + PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ + + # In text references, the beginning and ending refs can only be SHAs + # between 6 and 40 hex characters. + STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/ def self.reference_prefix '@' @@ -43,27 +45,40 @@ class CommitRange def self.reference_pattern %r{ (?:#{Project.reference_pattern}#{reference_prefix})? - (?<commit_range>#{PATTERN}) + (?<commit_range>#{STRICT_PATTERN}) }x end + def self.link_reference_pattern + super("compare", /(?<commit_range>#{PATTERN})/) + end + # Initialize a CommitRange # # range_string - The String commit range. # project - An optional Project model. # # Raises ArgumentError if `range_string` does not match `PATTERN`. - def initialize(range_string, project = nil) + def initialize(range_string, project) + @project = project + range_string.strip! - unless range_string.match(/\A#{PATTERN}\z/) + unless range_string =~ /\A#{PATTERN}\z/ raise ArgumentError, "invalid CommitRange string format: #{range_string}" end - @exclude_start = !range_string.include?('...') - @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2) + @ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2) - @project = project + if project.valid_repo? + @commit_from = project.commit(@ref_from) + @commit_to = project.commit(@ref_to) + end + + if valid_commits? + @ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from) + @ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to) + end end def inspect @@ -71,15 +86,24 @@ class CommitRange end def to_s - "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}" + sha_from + notation + sha_to end + alias_method :id, :to_s + def to_reference(from_project = nil) - # Not using to_s because we want the full SHAs - reference = sha_from + notation + sha_to + if cross_project_reference?(from_project) + project.to_reference + self.class.reference_prefix + self.id + else + self.id + end + end + + def reference_link_text(from_project = nil) + reference = ref_from + notation + ref_to if cross_project_reference?(from_project) - reference = project.to_reference + '@' + reference + reference = project.to_reference + self.class.reference_prefix + reference end reference @@ -87,46 +111,58 @@ class CommitRange # Returns a String for use in a link's title attribute def reference_title - "Commits #{suffixed_sha_from} through #{sha_to}" + "Commits #{sha_start} through #{sha_to}" end # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` def to_param - { from: suffixed_sha_from, to: sha_to } + { from: sha_start, to: sha_to } end def exclude_start? - exclude_start + @notation == '..' end # Check if both the starting and ending commit IDs exist in a project's # repository - # - # project - An optional Project to check (default: `project`) - def valid_commits?(project = project) - return nil unless project.present? - return false unless project.valid_repo? - - commit_from.present? && commit_to.present? + def valid_commits? + commit_start.present? && commit_end.present? end def persisted? true end - def commit_from - @commit_from ||= project.repository.commit(suffixed_sha_from) + def sha_from + return nil unless @commit_from + + @commit_from.id + end + + def sha_to + return nil unless @commit_to + + @commit_to.id end - def commit_to - @commit_to ||= project.repository.commit(sha_to) + def sha_start + return nil unless sha_from + + exclude_start? ? sha_from + '^' : sha_from end - private + def commit_start + return nil unless sha_start - def suffixed_sha_from - sha_from + (exclude_start? ? '^' : '') + if exclude_start? + @commit_start ||= project.commit(sha_start) + else + commit_from + end end + + alias_method :sha_end, :sha_to + alias_method :commit_end, :commit_to end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8188ba3a28e..21c5c87bc3d 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,6 +1,36 @@ +# == Schema Information +# +# project_id integer +# status string +# finished_at datetime +# trace text +# created_at datetime +# updated_at datetime +# started_at datetime +# runner_id integer +# coverage float +# commit_id integer +# commands text +# job_id integer +# name string +# deploy boolean default: false +# options text +# allow_failure boolean default: false, null: false +# stage string +# trigger_request_id integer +# stage_idx integer +# tag boolean +# ref string +# user_id integer +# type string +# target_url string +# description string +# + class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id belongs_to :commit, class_name: 'Ci::Commit' belongs_to :user @@ -15,12 +45,11 @@ class CommitStatus < ActiveRecord::Base scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } scope :failed, -> { where(status: 'failed') } - scope :running_or_pending, -> { where(status:[:running, :pending]) } - scope :finished, -> { where(status:[:success, :failed, :canceled]) } + scope :running_or_pending, -> { where(status: [:running, :pending]) } + scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :for_ref, ->(ref) { where(ref: ref) } - scope :running_or_pending, -> { where(status: [:running, :pending]) } state_machine :status, initial: :pending do event :run do @@ -47,6 +76,10 @@ class CommitStatus < ActiveRecord::Base build.update_attributes finished_at: Time.now end + after_transition [:pending, :running] => :success do |build, transition| + MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build) + end + state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' @@ -54,8 +87,7 @@ class CommitStatus < ActiveRecord::Base state :canceled, value: 'canceled' end - delegate :sha, :short_sha, :gl_project, - to: :commit, prefix: false + delegate :sha, :short_sha, to: :commit, prefix: false # TODO: this should be removed with all references def before_sha @@ -93,4 +125,8 @@ class CommitStatus < ActiveRecord::Base def show_warning? false end + + def download_url + nil + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5e964f04ef5..18a00f95b48 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -8,6 +8,7 @@ module Issuable extend ActiveSupport::Concern include Participable include Mentionable + include StripAttribute included do belongs_to :author, class_name: "User" @@ -24,7 +25,7 @@ module Issuable scope :authored, ->(user) { where(author_id: user) } scope :assigned_to, ->(u) { where(assignee_id: u.id)} - scope :recent, -> { order("created_at DESC") } + scope :recent, -> { reorder(id: :desc) } scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } @@ -35,6 +36,9 @@ module Issuable 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 :join_project, -> { joins(:project) } + scope :references_project, -> { references(:project) } + delegate :name, :email, to: :author, @@ -46,8 +50,10 @@ module Issuable allow_nil: true, prefix: true - attr_mentionable :title, :description + attr_mentionable :title, pipeline: :single_line + attr_mentionable :description, cache: true participant :author, :assignee, :notes_with_associations + strip_attributes :title end module ClassMethods @@ -89,39 +95,12 @@ module Issuable opened? || reopened? end - # - # Votes - # - - # Return the number of -1 comments (downvotes) def downvotes - filter_superceded_votes(notes.select(&:downvote?), notes).size + notes.awards.where(note: "thumbsdown").count end - def downvotes_in_percent - if votes_count.zero? - 0 - else - 100.0 - upvotes_in_percent - end - end - - # Return the number of +1 comments (upvotes) def upvotes - filter_superceded_votes(notes.select(&:upvote?), notes).size - end - - def upvotes_in_percent - if votes_count.zero? - 0 - else - 100.0 / votes_count * upvotes - end - end - - # Return the total number of votes - def votes_count - upvotes + downvotes + notes.awards.where(note: "thumbsup").count end def subscribed?(user) @@ -180,21 +159,20 @@ module Issuable self.class.to_s.underscore end + # Returns a Hash of attributes to be used for Twitter card metadata + def card_attributes + { + 'Author' => author.try(:name), + 'Assignee' => assignee.try(:name) + } + end + def notes_with_associations notes.includes(:author, :project) end - private - - def filter_superceded_votes(votes, notes) - filteredvotes = [] + votes - - votes.each do |vote| - if vote.superceded?(notes) - filteredvotes.delete(vote) - end - end - - filteredvotes + def updated_tasks + Taskable.get_updated_tasks(old_content: previous_changes['description'].first, + new_content: description) end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 193c91f1742..6316ee208b5 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -10,8 +10,9 @@ module Mentionable module ClassMethods # Indicate which attributes of the Mentionable to search for GFM references. - def attr_mentionable(*attrs) - mentionable_attrs.concat(attrs.map(&:to_s)) + def attr_mentionable(attr, options = {}) + attr = attr.to_s + mentionable_attrs << [attr, options] end # Accessor for attributes marked mentionable. @@ -22,7 +23,7 @@ module Mentionable included do if self < Participable - participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) } + participant ->(current_user) { mentioned_users(current_user) } end end @@ -37,38 +38,46 @@ module Mentionable "#{friendly_name} #{to_reference(from_project)}" end - # Construct a String that contains possible GFM references. - def mentionable_text - self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n") - end - # The GFM reference to this Mentionable, which shouldn't be included in its #references. def local_reference self end - def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true) - ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references) - ext.analyze(text) + def all_references(current_user = self.author, text = nil) + ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author) + + if text + ext.analyze(text) + else + self.class.mentionable_attrs.each do |attr, options| + text = send(attr) + options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted? + ext.analyze(text, options) + end + end + ext end - def mentioned_users(current_user = nil, load_lazy_references: true) - all_references(current_user, load_lazy_references: load_lazy_references).users + def mentioned_users(current_user = nil) + all_references(current_user).users end # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. - def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true) - return [] if text.blank? + def referenced_mentionables(current_user = self.author, text = nil) + refs = all_references(current_user, text) + refs = (refs.issues + refs.merge_requests + refs.commits) - refs = all_references(current_user, text, load_lazy_references: load_lazy_references) - (refs.issues + refs.merge_requests + refs.commits) - [local_reference] + # We're using this method instead of Array diffing because that requires + # both of the object's `hash` values to be the same, which may not be the + # case for otherwise identical Commit objects. + refs.reject { |ref| ref == local_reference } end - # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. - def create_cross_references!(author = self.author, without = [], text = self.mentionable_text) + # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+. + def create_cross_references!(author = self.author, without = [], text = nil) refs = referenced_mentionables(author, text) - + # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the # case for otherwise identical Commit objects. @@ -106,12 +115,12 @@ module Mentionable def detect_mentionable_changes source = (changes.present? ? changes : previous_changes).dup - mentionable = self.class.mentionable_attrs + mentionable = self.class.mentionable_attrs.map { |attr, options| attr } # Only include changed fields that are mentionable source.select { |key, val| mentionable.include?(key) } end - + # Determine whether or not a cross-reference Note has already been created between this Mentionable and # the specified target. def cross_reference_exists?(target) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 85367f89f4f..fc6f83b918b 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -37,21 +37,22 @@ module Participable # Be aware that this method makes a lot of sql queries. # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author, load_lazy_references: true) - participants = self.class.participant_attrs.flat_map do |attr| - value = - if attr.respond_to?(:call) - instance_exec(current_user, &attr) - else - send(attr) - end + def participants(current_user = self.author) + participants = + Gitlab::ReferenceExtractor.lazily do + self.class.participant_attrs.flat_map do |attr| + value = + if attr.respond_to?(:call) + instance_exec(current_user, &attr) + else + send(attr) + end - participants_for(value, current_user) - end.compact.uniq - - if load_lazy_references - participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq + participants_for(value, current_user) + end.compact.uniq + end + unless Gitlab::ReferenceExtractor.lazy? participants.select! do |user| user.can?(:read_project, project) end @@ -64,12 +65,12 @@ module Participable def participants_for(value, current_user = nil) case value - when User, Gitlab::Markdown::ReferenceFilter::LazyReference + when User, Banzai::LazyReference [value] when Enumerable, ActiveRecord::Relation value.flat_map { |v| participants_for(v, current_user) } when Participable - value.participants(current_user, load_lazy_references: false) + value.participants(current_user) end end end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index cced66cc1e4..ce064f675ae 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -21,6 +21,10 @@ module Referable '' end + def reference_link_text(from_project = nil) + to_reference(from_project) + end + module ClassMethods # The character that prefixes the actual reference identifier # @@ -44,6 +48,25 @@ module Referable def reference_pattern raise NotImplementedError, "#{self} does not implement #{__method__}" end + + def link_reference_pattern(route, pattern) + %r{ + (?<url> + #{Regexp.escape(Gitlab.config.gitlab.url)} + \/#{Project.reference_pattern} + \/#{Regexp.escape(route)} + \/#{pattern} + (?<path> + (\/[a-z0-9_=-]+)* + )? + (?<query> + \?[a-z0-9_=-]+ + (&[a-z0-9_=-]+)* + )? + (?<anchor>\#[a-z0-9_-]+)? + ) + }x + end end private diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 0ad2654867d..7391a77383c 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -8,12 +8,13 @@ module Sortable included do # By default all models should be ordered # by created_at field starting from newest - default_scope { order(created_at: :desc, id: :desc) } + default_scope { order_id_desc } - scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) } - scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) } - scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) } - scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) } + scope :order_id_desc, -> { reorder(id: :desc) } + scope :order_created_desc, -> { reorder(created_at: :desc) } + scope :order_created_asc, -> { reorder(created_at: :asc) } + scope :order_updated_desc, -> { reorder(updated_at: :desc) } + scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_desc, -> { reorder(name: :desc) } end diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb new file mode 100644 index 00000000000..8806ebe897a --- /dev/null +++ b/app/models/concerns/strip_attribute.rb @@ -0,0 +1,34 @@ +# == Strip Attribute module +# +# Contains functionality to clean attributes before validation +# +# Usage: +# +# class Milestone < ActiveRecord::Base +# strip_attributes :title +# end +# +# +module StripAttribute + extend ActiveSupport::Concern + + module ClassMethods + def strip_attributes(*attrs) + strip_attrs.concat(attrs) + end + + def strip_attrs + @strip_attrs ||= [] + end + end + + included do + before_validation :strip_attributes + end + + def strip_attributes + self.class.strip_attrs.each do |attr| + self[attr].strip! if self[attr] && self[attr].respond_to?(:strip!) + end + end +end diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index 660e58b876d..df2a9e3e84b 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -7,14 +7,39 @@ require 'task_list/filter' # # Used by MergeRequest and Issue module Taskable + COMPLETED = 'completed'.freeze + INCOMPLETE = 'incomplete'.freeze + ITEM_PATTERN = / + ^ + (?:\s*[-+*]|(?:\d+\.))? # optional list prefix + \s* # optional whitespace prefix + (\[\s\]|\[[xX]\]) # checkbox + (\s.+) # followed by whitespace and some text. + /x + + def self.get_tasks(content) + content.to_s.scan(ITEM_PATTERN).map do |checkbox, label| + # ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble. + TaskList::Item.new("- #{checkbox}", label.strip) + end + end + + def self.get_updated_tasks(old_content:, new_content:) + old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content) + + new_tasks.select.with_index do |new_task, i| + old_task = old_tasks[i] + next unless old_task + + new_task.source == old_task.source && new_task.complete? != old_task.complete? + end + end + # Called by `TaskList::Summary` def task_list_items return [] if description.blank? - @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item| - # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble. - TaskList::Item.new("- #{item}") - end + @task_list_items ||= Taskable.get_tasks(description) end def tasks diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 9b88ec1cc38..885deaf78d2 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -1,31 +1,49 @@ module TokenAuthenticatable extend ActiveSupport::Concern - module ClassMethods - def find_by_authentication_token(authentication_token = nil) - if authentication_token - where(authentication_token: authentication_token).first - end + class_methods do + def authentication_token_fields + @token_fields || [] end - end - def ensure_authentication_token - if authentication_token.blank? - self.authentication_token = generate_authentication_token - end - end + private - def reset_authentication_token! - self.authentication_token = generate_authentication_token - save + def add_authentication_token_field(token_field) + @token_fields = [] unless @token_fields + @token_fields << token_field + + define_singleton_method("find_by_#{token_field}") do |token| + find_by(token_field => token) if token + end + + define_method("ensure_#{token_field}") do + current_token = read_attribute(token_field) + current_token.blank? ? write_new_token(token_field) : current_token + end + + define_method("ensure_#{token_field}!") do + send("reset_#{token_field}!") if read_attribute(token_field).blank? + read_attribute(token_field) + end + + define_method("reset_#{token_field}!") do + write_new_token(token_field) + save! + end + end end private - def generate_authentication_token + def write_new_token(token_field) + new_token = generate_token(token_field) + write_attribute(token_field, new_token) + end + + def generate_token(token_field) loop do token = Devise.friendly_token - break token unless self.class.unscoped.where(authentication_token: token).first + break token unless self.class.unscoped.find_by(token_field => token) end end end diff --git a/app/models/event.rb b/app/models/event.rb index 47600c57e35..01d008035a5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -45,7 +45,7 @@ class Event < ActiveRecord::Base after_create :reset_project_activity # Scopes - scope :recent, -> { order(created_at: :desc) } + scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } scope :with_associations, -> { includes(project: :namespace) } @@ -63,6 +63,16 @@ class Event < ActiveRecord::Base Event::PUSHED, ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED]) end + + def latest_update_time + row = select(:updated_at, :project_id).reorder(id: :desc).take + + row ? row.updated_at : nil + end + + def limit_recent(limit = 20, offset = nil) + recent.limit(limit).offset(offset) + end end def proper? @@ -191,7 +201,7 @@ class Event < ActiveRecord::Base elsif commented? "commented on" elsif created_project? - if project.import? + if project.external_import? "imported" else "created" diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index fa54e3540d0..12c934e2494 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,3 +1,36 @@ +# == Schema Information +# +# Table name: ci_builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# coverage :float +# commit_id :integer +# commands :text +# job_id :integer +# name :string(255) +# deploy :boolean default(FALSE) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# trigger_request_id :integer +# stage_idx :integer +# tag :boolean +# ref :string(255) +# user_id :integer +# type :string(255) +# target_url :string(255) +# description :string(255) +# artifacts_file :text +# + class GenericCommitStatus < CommitStatus before_validation :set_default_values diff --git a/app/models/global_label.rb b/app/models/global_label.rb new file mode 100644 index 00000000000..0171f7d54b7 --- /dev/null +++ b/app/models/global_label.rb @@ -0,0 +1,17 @@ +class GlobalLabel + attr_accessor :title, :labels + alias_attribute :name, :title + + def self.build_collection(labels) + labels = labels.group_by(&:title) + + labels.map do |title, label| + new(title, label) + end + end + + def initialize(title, labels) + @title = title + @labels = labels + end +end diff --git a/app/models/group_milestone.rb b/app/models/global_milestone.rb index 91844da62e2..af1d7562ebe 100644 --- a/app/models/group_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,16 +1,32 @@ -class GroupMilestone +class GlobalMilestone attr_accessor :title, :milestones alias_attribute :name, :title + def self.build_collection(milestones) + milestones = milestones.group_by(&:title) + + milestones.map do |title, milestones| + new(title, milestones) + end + end + def initialize(title, milestones) @title = title @milestones = milestones end def safe_title - @title.parameterize + @title.to_slug.normalize.to_s end - + + def expired? + if due_date + due_date.past? + else + false + end + end + def projects milestones.map { |milestone| milestone.project } end @@ -60,15 +76,15 @@ class GroupMilestone end def issues - @group_issues ||= milestones.map(&:issues).flatten.group_by(&:state) + @issues ||= milestones.map(&:issues).flatten.group_by(&:state) end def merge_requests - @group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) + @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) end def participants - @group_participants ||= milestones.map(&:participants).flatten.compact.uniq + @participants ||= milestones.map(&:participants).flatten.compact.uniq end def opened_issues @@ -86,4 +102,29 @@ class GroupMilestone def closed_merge_requests merge_requests.values_at("closed", "merged", "locked").compact.flatten end + + def complete? + total_items_count == closed_items_count + end + + def due_date + return @due_date if defined?(@due_date) + + @due_date = + if @milestones.all? { |x| x.due_date == @milestones.first.due_date } + @milestones.first.due_date + else + nil + end + end + + def expires_at + if due_date + if due_date.past? + "expired at #{due_date.stamp("Aug 21, 2011")}" + else + "expires at #{due_date.stamp("Aug 21, 2011")}" + end + end + end end diff --git a/app/models/group.rb b/app/models/group.rb index 465c22d23ac..b8f2ab6ae5d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,6 +11,7 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) +# public :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -19,8 +20,9 @@ require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper include Referable - + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' + alias_method :members, :group_members has_many :users, through: :group_members validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } @@ -47,6 +49,10 @@ class Group < Namespace def reference_pattern User.reference_pattern end + + def visible_to_user(user) + where(id: user.authorized_groups.select(:id).reorder(nil)) + end end def to_reference(_from_project = nil) @@ -109,20 +115,12 @@ class Group < Namespace has_owner?(user) && owners.size == 1 end - def members - group_members - end - def avatar_type unless self.avatar.image? self.errors.add :avatar, "only images allowed" end end - def public_profile? - projects.public_only.any? - end - def post_create_hook Gitlab::AppLogger.info("Group \"#{name}\" was created") diff --git a/app/models/group_label.rb b/app/models/group_label.rb deleted file mode 100644 index 0fc39cb8771..00000000000 --- a/app/models/group_label.rb +++ /dev/null @@ -1,9 +0,0 @@ -class GroupLabel - attr_accessor :title, :labels - alias_attribute :name, :title - - def initialize(title, labels) - @title = title - @labels = labels - end -end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index ca7066b959a..22638057773 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class ProjectHook < WebHook @@ -24,4 +25,5 @@ class ProjectHook < WebHook 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) } end diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index b55e217975f..09bb3ee52a2 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class ServiceHook < WebHook diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 6fb2d421026..2f63c59b07e 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index a078accbdbd..40eb0e20b4b 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -2,18 +2,19 @@ # # Table name: web_hooks # -# id :integer not null, primary key -# url :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null +# id :integer not null, primary key +# url :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# type :string(255) default("ProjectHook") +# service_id :integer +# push_events :boolean default(TRUE), not null +# issues_events :boolean default(FALSE), not null +# merge_requests_events :boolean default(FALSE), not null +# tag_push_events :boolean default(FALSE) +# note_events :boolean default(FALSE), not null +# enable_ssl_verification :boolean default(TRUE) # class WebHook < ActiveRecord::Base @@ -25,42 +26,44 @@ class WebHook < ActiveRecord::Base default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false + default_value_for :build_events, false default_value_for :enable_ssl_verification, true # HTTParty timeout default_timeout Gitlab.config.gitlab.webhook_timeout - validates :url, presence: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" } + validates :url, presence: true, url: true def execute(data, hook_name) parsed_url = URI.parse(url) if parsed_url.userinfo.blank? - WebHook.post(url, - body: data.to_json, - headers: { - "Content-Type" => "application/json", - "X-Gitlab-Event" => hook_name.singularize.titleize - }, - verify: enable_ssl_verification) + response = WebHook.post(url, + body: data.to_json, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, + verify: enable_ssl_verification) else post_url = url.gsub("#{parsed_url.userinfo}@", "") auth = { username: URI.decode(parsed_url.user), password: URI.decode(parsed_url.password), } - WebHook.post(post_url, - body: data.to_json, - headers: { - "Content-Type" => "application/json", - "X-Gitlab-Event" => hook_name.singularize.titleize - }, - verify: enable_ssl_verification, - basic_auth: auth) + response = WebHook.post(post_url, + body: data.to_json, + headers: { + "Content-Type" => "application/json", + "X-Gitlab-Event" => hook_name.singularize.titleize + }, + verify: enable_ssl_verification, + basic_auth: auth) end - rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e + + [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)] + rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") - false + [false, e.to_s] end def async_execute(data, hook_name) diff --git a/app/models/identity.rb b/app/models/identity.rb index ad60154be71..8bcdc194953 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -12,6 +12,7 @@ class Identity < ActiveRecord::Base include Sortable + include CaseSensitivity belongs_to :user validates :provider, presence: true diff --git a/app/models/issue.rb b/app/models/issue.rb index 72183108033..80ecd15077f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -69,6 +69,10 @@ class Issue < ActiveRecord::Base }x end + def self.link_reference_pattern + super("issues", /(?<issue>\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -79,6 +83,14 @@ class Issue < ActiveRecord::Base reference end + def referenced_merge_requests + Gitlab::ReferenceExtractor.lazily do + [self, *notes].flat_map do |note| + note.all_references.merge_requests + end + end.sort_by(&:iid) + end + # Reset issue events cache # # Since we do cache @event we need to reset cache in special cases: diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb new file mode 100644 index 00000000000..5b21aac5e43 --- /dev/null +++ b/app/models/jira_issue.rb @@ -0,0 +1,2 @@ +class JiraIssue < ExternalIssue +end diff --git a/app/models/label.rb b/app/models/label.rb index 1bb4b5f55cf..220da10a6ab 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -8,6 +8,7 @@ # project_id :integer # created_at :datetime # updated_at :datetime +# template :boolean default(FALSE) # class Label < ActiveRecord::Base @@ -16,7 +17,7 @@ class Label < ActiveRecord::Base # Requests that have no label assigned. LabelStruct = Struct.new(:title, :name) None = LabelStruct.new('No Label', 'No Label') - Any = LabelStruct.new('Any', '') + Any = LabelStruct.new('Any Label', '') DEFAULT_COLOR = '#428BCA' @@ -26,9 +27,7 @@ class Label < ActiveRecord::Base has_many :label_links, dependent: :destroy has_many :issues, through: :label_links, source: :target, source_type: 'Issue' - validates :color, - format: { with: /\A#[0-9A-Fa-f]{6}\Z/ }, - allow_blank: false + validates :color, color: true, allow_blank: false validates :project, presence: true, unless: Proc.new { |service| service.template? } # Don't allow '?', '&', and ',' for label titles diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb new file mode 100644 index 00000000000..86b1b7e2f99 --- /dev/null +++ b/app/models/lfs_object.rb @@ -0,0 +1,32 @@ +# == Schema Information +# +# Table name: lfs_objects +# +# id :integer not null, primary key +# oid :string(255) not null +# size :integer not null +# created_at :datetime +# updated_at :datetime +# file :string(255) +# + +class LfsObject < ActiveRecord::Base + has_many :lfs_objects_projects, dependent: :destroy + has_many :projects, through: :lfs_objects_projects + + validates :oid, presence: true, uniqueness: true + + mount_uploader :file, LfsObjectUploader + + def storage_project(project) + if project && project.forked? + storage_project(project.forked_from_project) + else + project + end + end + + def project_allowed_access?(project) + projects.exists?(storage_project(project).id) + end +end diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb new file mode 100644 index 00000000000..890736bfc80 --- /dev/null +++ b/app/models/lfs_objects_project.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: lfs_objects_projects +# +# id :integer not null, primary key +# lfs_object_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +class LfsObjectsProject < ActiveRecord::Base + belongs_to :project + belongs_to :lfs_object + + validates :lfs_object_id, presence: true + validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } + validates :project_id, presence: true +end diff --git a/app/models/member.rb b/app/models/member.rb index cae8caa23fb..28aee2e3799 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -30,13 +30,22 @@ class Member < ActiveRecord::Base validates :user, presence: true, unless: :invite? validates :source, presence: true - validates :user_id, uniqueness: { scope: [:source_type, :source_id], + validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source", allow_nil: true } validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true - validates :invite_email, presence: { if: :invite? }, - email: { strict_mode: true, allow_nil: true }, - uniqueness: { scope: [:source_type, :source_id], allow_nil: true } + validates :invite_email, + presence: { + if: :invite? + }, + email: { + strict_mode: true, + allow_nil: true + }, + uniqueness: { + scope: [:source_type, :source_id], + allow_nil: true + } scope :invite, -> { where(user_id: nil) } scope :non_invite, -> { where("user_id IS NOT NULL") } @@ -73,7 +82,7 @@ class Member < ActiveRecord::Base def add_user(members, user_id, access_level, current_user = nil) user = user_for_id(user_id) - + # `user` can be either a User object or an email to be invited if user.is_a?(User) member = members.find_or_initialize_by(user_id: user.id) @@ -82,10 +91,21 @@ class Member < ActiveRecord::Base member.invite_email = user end - member.created_by ||= current_user - member.access_level = access_level + if can_update_member?(current_user, member) + member.created_by ||= current_user + member.access_level = access_level + + member.save + end + end + + private - member.save + def can_update_member?(current_user, member) + # There is no current user for bulk actions, in which case anything is allowed + !current_user || + current_user.can?(:update_group_member, member) || + current_user.can?(:update_project_member, member) end end @@ -95,7 +115,7 @@ class Member < ActiveRecord::Base def accept_invite!(new_user) return false unless invite? - + self.invite_token = nil self.invite_accepted_at = Time.now.utc diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 21861a46a84..ac25d38eb63 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2,24 +2,28 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text (serialized to hash) +# merge_when_build_succeeds :boolean default(false), not null +# merge_user_id :integer # require Rails.root.join("app/models/commit") @@ -34,13 +38,16 @@ class MergeRequest < ActiveRecord::Base belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" + belongs_to :merge_user, class_name: "User" has_one :merge_request_diff, dependent: :destroy + serialize :merge_params, Hash + after_create :create_merge_request_diff after_update :update_merge_request_diff - delegate :commits, :diffs, to: :merge_request_diff, prefix: nil + delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -120,6 +127,7 @@ class MergeRequest < ActiveRecord::Base validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true + validates :merge_user, presence: true, if: :merge_when_build_succeeds? validate :validate_branches validate :validate_fork @@ -133,6 +141,9 @@ class MergeRequest < ActiveRecord::Base scope :closed, -> { with_state(:closed) } scope :closed_and_merged, -> { with_states(:closed, :merged) } + scope :join_project, -> { joins(:target_project) } + scope :references_project, -> { references(:target_project) } + def self.reference_prefix '!' end @@ -147,6 +158,10 @@ class MergeRequest < ActiveRecord::Base }x end + def self.link_reference_pattern + super("merge_requests", /(?<merge_request>\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -159,11 +174,11 @@ class MergeRequest < ActiveRecord::Base def last_commit merge_request_diff ? merge_request_diff.last_commit : compare_commits.last - end + end def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first - end + end def last_commit_short_sha last_commit.short_id @@ -179,9 +194,7 @@ class MergeRequest < ActiveRecord::Base similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, - "Cannot Create: This merge request already exists: #{ - similar_mrs.pluck(:title) - }" + "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}" end end end @@ -250,6 +263,16 @@ class MergeRequest < ActiveRecord::Base end end + def can_cancel_merge_when_build_succeeds?(current_user) + can_be_merged_by?(current_user) || self.author == current_user + end + + def can_remove_source_branch?(current_user) + !source_project.protected_branch?(source_branch) && + !source_project.root_ref?(source_branch) && + Ability.abilities.allowed?(current_user, :push_code, source_project) + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 @@ -257,7 +280,7 @@ class MergeRequest < ActiveRecord::Base Note.where( "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + - "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", + "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", mr_id: id, commit_ids: commit_ids, target_project_id: target_project_id, @@ -287,7 +310,7 @@ class MergeRequest < ActiveRecord::Base work_in_progress: work_in_progress? } - unless last_commit.nil? + if last_commit attrs.merge!(last_commit: last_commit.hook_attrs) end @@ -312,7 +335,7 @@ class MergeRequest < ActiveRecord::Base issues = commits.flat_map { |c| c.closes_issues(current_user) } issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(description)) - issues.uniq.sort_by(&:id) + issues.uniq(&:id) else [] end @@ -385,6 +408,16 @@ class MergeRequest < ActiveRecord::Base message end + def reset_merge_when_build_succeeds + return unless merge_when_build_succeeds? + + self.merge_when_build_succeeds = false + self.merge_user = nil + self.merge_params = nil + + self.save + end + # Return array of possible target branches # depends on target project of MR def target_branches @@ -470,4 +503,12 @@ class MergeRequest < ActiveRecord::Base unlock_mr if locked? end end + + def ci_commit + @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project + end + + def broken? + self.commits.blank? || branch_missing? || cannot_be_merged? + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 6575d0bc81f..c499a4b5b4c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -19,7 +19,7 @@ class MergeRequestDiff < ActiveRecord::Base # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 500 - attr_reader :commits, :diffs + attr_reader :commits, :diffs, :diffs_no_whitespace belongs_to :merge_request @@ -47,6 +47,20 @@ class MergeRequestDiff < ActiveRecord::Base @diffs ||= (load_diffs(st_diffs) || []) end + def diffs_no_whitespace + # Get latest sha of branch from source project + source_sha = merge_request.source_project.commit(source_branch).sha + + compare_result = Gitlab::CompareResult.new( + Gitlab::Git::Compare.new( + merge_request.target_project.repository.raw_repository, + merge_request.target_branch, + source_sha, + ), { ignore_whitespace_change: true } + ) + @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs)) + end + def commits @commits ||= load_commits(st_commits || []) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 2ff16e2825c..d8c7536cd31 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -16,12 +16,13 @@ class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. - MilestoneStruct = Struct.new(:title, :name) - None = MilestoneStruct.new('No Milestone', 'No Milestone') - Any = MilestoneStruct.new('Any', '') + MilestoneStruct = Struct.new(:title, :name, :id) + None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) + Any = MilestoneStruct.new('Any Milestone', '', -1) include InternalId include Sortable + include StripAttribute belongs_to :project has_many :issues @@ -35,6 +36,8 @@ class Milestone < ActiveRecord::Base validates :title, presence: true validates :project, presence: true + strip_attributes :title + state_machine :state, initial: :active do event :close do transition active: :closed diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 5782e649f8b..adafabbec07 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,6 +11,7 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) +# public :boolean default(FALSE) # class Namespace < ActiveRecord::Base @@ -22,19 +23,17 @@ class Namespace < ActiveRecord::Base validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, - presence: true, uniqueness: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.namespace_name_regex, - message: Gitlab::Regex.namespace_name_regex_message } + namespace_name: true, + presence: true, + uniqueness: true validates :description, length: { within: 0..255 } validates :path, - uniqueness: { case_sensitive: false }, - presence: true, length: { within: 1..255 }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.namespace_regex, - message: Gitlab::Regex.namespace_regex_message } + namespace: true, + presence: true, + uniqueness: { case_sensitive: false } delegate :name, to: :owner, allow_nil: true, prefix: true @@ -46,7 +45,7 @@ class Namespace < ActiveRecord::Base class << self def by_path(path) - where('lower(path) = :value', value: path.downcase).first + find_by('lower(path) = :value', value: path.downcase) end # Case insensetive search for namespace by path or name @@ -149,6 +148,6 @@ class Namespace < ActiveRecord::Base end def find_fork_of(project) - projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first + projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) end end diff --git a/app/models/note.rb b/app/models/note.rb index 0b3aa30abd7..3d5b663c99f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -16,6 +16,7 @@ # system :boolean default(FALSE), not null # st_diff :text # updated_by_id :integer +# is_award :boolean default(FALSE), not null # require 'carrierwave/orm/activerecord' @@ -28,7 +29,7 @@ class Note < ActiveRecord::Base default_value_for :system, false - attr_mentionable :note + attr_mentionable :note, cache: true, pipeline: :note participant :author belongs_to :project @@ -39,17 +40,24 @@ class Note < ActiveRecord::Base delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true + before_validation :set_award! + validates :note, :project, presence: true - validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true + validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } + validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } + validates :line_code, line_code: true, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } + validates :author, presence: true mount_uploader :attachment, AttachmentUploader # Scopes + scope :awards, ->{ where(is_award: true) } + scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } scope :not_inline, ->{ where(line_code: [nil, '']) } @@ -97,6 +105,19 @@ class Note < ActiveRecord::Base def search(query) where("LOWER(note) like :query", query: "%#{query.downcase}%") end + + def grouped_awards + notes = {} + + awards.select(:note).distinct.map do |note| + notes[note.note] = where(note: note.note) + end + + notes["thumbsup"] ||= Note.none + notes["thumbsdown"] ||= Note.none + + notes + end end def cross_reference? @@ -288,44 +309,6 @@ class Note < ActiveRecord::Base nil end - DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:) - - # Check if the note is a downvote - def downvote? - votable? && note.start_with?(*DOWNVOTES) - end - - UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:) - - # Check if the note is an upvote - def upvote? - votable? && note.start_with?(*UPVOTES) - end - - def superceded?(notes) - return false unless vote? - - notes.each do |note| - next if note == self - - if note.vote? && - self[:author_id] == note[:author_id] && - self[:created_at] <= note[:created_at] - return true - end - end - - false - end - - def vote? - upvote? || downvote? - end - - def votable? - for_issue? || (for_merge_request? && !for_diff_line?) - end - # Mentionable override. def gfm_reference(from_project = nil) noteable.gfm_reference(from_project) @@ -363,7 +346,43 @@ class Note < ActiveRecord::Base read_attribute(:system) end + def downvote? + is_award && note == "thumbsdown" + end + + def upvote? + is_award && note == "thumbsup" + end + def editable? - !system? + !system? && !is_award + end + + # Checks if note is an award added as a comment + # + # If note is an award, this method sets is_award to true + # and changes content of the note to award name. + # + # Method is executed as a before_validation callback. + # + def set_award! + return unless awards_supported? && contains_emoji_only? + self.is_award = true + self.note = award_emoji_name + end + + private + + def awards_supported? + noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest) + end + + def contains_emoji_only? + note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/ + end + + def award_emoji_name + original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] + AwardEmoji.normilize_emoji_name(original_name) end end diff --git a/app/models/project.rb b/app/models/project.rb index 88cd88dcb5a..b1a6cfa86af 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,6 +28,7 @@ # import_type :string(255) # import_source :string(255) # commit_count :integer default(0) +# import_error :text # require 'carrierwave/orm/activerecord' @@ -37,13 +38,13 @@ class Project < ActiveRecord::Base include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel + include Gitlab::CurrentSettings include Referable include Sortable include AfterCommitQueue include CaseSensitivity extend Gitlab::ConfigHelper - extend Enumerize UNKNOWN_IMPORT_URL = 'http://unknown.git' @@ -51,9 +52,11 @@ class Project < ActiveRecord::Base default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :issues_enabled, gitlab_config_features.issues default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests + default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at after_create :set_last_activity_at @@ -61,10 +64,24 @@ class Project < ActiveRecord::Base update_column(:last_activity_at, self.created_at) end + # update visibility_levet of forks + after_update :update_forks_visibility_level + def update_forks_visibility_level + return unless visibility_level < visibility_level_was + + forks.each do |forked_project| + if forked_project.visibility_level > visibility_level + forked_project.visibility_level = visibility_level + forked_project.save! + end + end + end + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags attr_accessor :new_default_branch + attr_accessor :old_path_with_namespace # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' @@ -75,10 +92,10 @@ class Project < ActiveRecord::Base # Project services has_many :services - has_one :gitlab_ci_service, dependent: :destroy has_one :campfire_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy + has_one :builds_email_service, dependent: :destroy has_one :irker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy @@ -97,9 +114,12 @@ class Project < ActiveRecord::Base has_one :gitlab_issue_tracker_service, dependent: :destroy has_one :external_wiki_service, dependent: :destroy - has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_from_project, through: :forked_project_link + + has_many :forked_project_links, foreign_key: "forked_from_project_id" + has_many :forks, through: :forked_project_links, source: :forked_to_project - has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed @@ -119,11 +139,21 @@ class Project < ActiveRecord::Base has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects, dependent: :destroy has_many :starrers, through: :users_star_projects, source: :user - has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id - has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :releases, dependent: :destroy + has_many :lfs_objects_projects, dependent: :destroy + has_many :lfs_objects, through: :lfs_objects_projects has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" - has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id + + has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id + has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id + has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id + has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' + has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id + + accepts_nested_attributes_for :variables, allow_destroy: true delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -148,7 +178,7 @@ class Project < ActiveRecord::Base validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id validates :import_url, - format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' }, + url: { protocols: %w(ssh git http https) }, if: :external_import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create @@ -156,6 +186,11 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + before_validation :set_runners_token_token + def set_runners_token_token + self.runners_token = SecureRandom.hex(15) if self.runners_token.blank? + end + mount_uploader :avatar, AvatarUploader # Scopes @@ -243,11 +278,16 @@ class Project < ActiveRecord::Base # Use of unscoped ensures we're not secretly adding any ORDER BYs, which # have a negative impact on performance (and aren't needed for this # query). - unscoped. + projects = unscoped. joins(:namespace). - iwhere('namespaces.path' => namespace_path). - iwhere('projects.path' => project_path). - take + iwhere('namespaces.path' => namespace_path) + + projects.find_by('projects.path' => project_path) || + projects.iwhere('projects.path' => project_path).take + end + + def find_by_ci_id(id) + find_by(ci_id: id.to_i) end def visibility_levels @@ -280,6 +320,10 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end + + def visible_to_user(user) + where(id: user.authorized_projects.select(:id).reorder(nil)) + end end def team @@ -304,15 +348,17 @@ class Project < ActiveRecord::Base def add_import_job if forked? - unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path) - import_fail - end + RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(id) + RepositoryImportWorker.perform_async(self.id) end end def clear_import_data + update(import_error: nil) + + ProjectCacheWorker.perform_async(self.id) + self.import_data.destroy if self.import_data end @@ -340,6 +386,14 @@ class Project < ActiveRecord::Base import_status == 'finished' end + def safe_import_url + result = URI.parse(self.import_url) + result.password = '*****' unless result.password.nil? + result.to_s + rescue + original_url + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it") @@ -413,7 +467,7 @@ class Project < ActiveRecord::Base end def external_issue_tracker - @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first + @external_issues_tracker ||= external_issues_trackers.find(&:activated?) end def can_have_issues_tracker_id? @@ -454,16 +508,16 @@ class Project < ActiveRecord::Base list.find { |service| service.to_param == name } end - def gitlab_ci? - gitlab_ci_service && gitlab_ci_service.active && gitlab_ci_project.present? - end - def ci_services services.select { |service| service.category == :ci } end def ci_service - @ci_service ||= ci_services.select(&:activated?).first + @ci_service ||= ci_services.find(&:activated?) + end + + def jira_tracker? + issues_tracker.to_param == 'jira' end def avatar_type @@ -502,7 +556,9 @@ class Project < ActiveRecord::Base end def send_move_instructions(old_path_with_namespace) - NotificationService.new.project_was_moved(self, old_path_with_namespace) + # New project path needs to be committed to the DB or notification will + # retrieve stale information + run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) } end def owner @@ -514,7 +570,7 @@ class Project < ActiveRecord::Base end def project_member_by_name_or_email(name = nil, email = nil) - user = users.where('name like ? or email like ?', name, email).first + user = users.find_by('name like ? or email like ?', name, email) project_members.where(user: user) if user end @@ -567,7 +623,7 @@ class Project < ActiveRecord::Base end def empty_repo? - !repository.exists? || repository.empty? + !repository.exists? || !repository.has_visible_content? end def repo @@ -646,6 +702,12 @@ class Project < ActiveRecord::Base gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) reset_events_cache + + @old_path_with_namespace = old_path_with_namespace + + SystemHooksService.new.execute_hooks_for(self, :rename) + + @repository = nil rescue # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks @@ -688,7 +750,7 @@ class Project < ActiveRecord::Base end def project_member(user) - project_members.where(user_id: user).first + project_members.find_by(user_id: user) end def default_branch @@ -713,6 +775,8 @@ class Project < ActiveRecord::Base end def change_head(branch) + # Cached divergent commit counts are based on repository head + repository.expire_branch_cache gitlab_shell.update_repository_head(self.path_with_namespace, branch) reload_default_branch end @@ -730,7 +794,7 @@ class Project < ActiveRecord::Base end def forks_count - ForkedProjectLink.where(forked_from_project_id: self.id).count + forks.count end def find_label(name) @@ -765,6 +829,10 @@ class Project < ActiveRecord::Base false end + def jira_tracker_active? + jira_tracker? && jira_service.active + end + def ci_commit(sha) ci_commits.find_by(sha: sha) end @@ -773,13 +841,56 @@ class Project < ActiveRecord::Base ci_commit(sha) || ci_commits.create(sha: sha) end - def ensure_gitlab_ci_project - gitlab_ci_project || create_gitlab_ci_project + def enable_ci + self.builds_enabled = true end - def enable_ci - service = gitlab_ci_service || create_gitlab_ci_service - service.active = true - service.save + def unlink_fork + if forked? + forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << self + end + + forked_project_link.destroy + end + end + + def any_runners?(&block) + if runners.active.any?(&block) + return true + end + + shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) + end + + def valid_runners_token? token + self.runners_token && self.runners_token == token + end + + # TODO (ayufan): For now we use runners_token (backward compatibility) + # In 8.4 every build will have its own individual token valid for time of build + def valid_build_token? token + self.builds_enabled? && self.runners_token && self.runners_token == token + end + + def build_coverage_enabled? + build_coverage_regex.present? + end + + def build_timeout_in_minutes + build_timeout / 60 + end + + def build_timeout_in_minutes=(value) + self.build_timeout = value.to_i * 60 + end + + def open_issues_count + issues.opened.count + end + + def visibility_level_allowed?(level) + return true unless forked? + Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i) end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index e6e16058d41..7d367e40037 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -40,8 +40,8 @@ get the commit comment added to it. You can also close a task with a message containing: `fix #123456`. -You can find your Api Keys here: -http://developer.asana.com/documentation/#api_keys' +You can create a Personal Access Token here: +http://app.asana.com/-/account_api' end def to_param @@ -53,14 +53,12 @@ http://developer.asana.com/documentation/#api_keys' { type: 'text', name: 'api_key', - placeholder: 'User API token. User must have access to task, -all comments will be attributed to this user.' + placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.' }, { type: 'text', name: 'restrict_to_branch', - placeholder: 'Comma-separated list of branches which will be -automatically inspected. Leave blank to include all branches.' + placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ] end @@ -69,58 +67,58 @@ automatically inspected. Leave blank to include all branches.' %w(push) end + def client + @_client ||= begin + Asana::Client.new do |c| + c.authentication :access_token, api_key + end + end + end + def execute(data) return unless supported_events.include?(data[:object_kind]) - Asana.configure do |client| - client.api_key = api_key - end - - user = data[:user_name] + # check the branch restriction is poplulated and branch is not included branch = Gitlab::Git.ref_name(data[:ref]) - branch_restriction = restrict_to_branch.to_s - - # check the branch restriction is poplulated and branch is not included if branch_restriction.length > 0 && branch_restriction.index(branch).nil? return end + user = data[:user_name] project_name = project.name_with_namespace - push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name data[:commits].each do |commit| - check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg) + push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):" + check_commit(commit[:message], push_msg) end end def check_commit(message, push_msg) - task_list = [] - close_list = [] - - message.split("\n").each do |line| - # look for a task ID or a full Asana url - task_list.concat(line.scan(/#(\d+)/)) - task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/)) - # look for a word starting with 'fix' followed by a task ID - close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i)) - end - - # post commit to every taskid found - task_list.each do |taskid| - task = Asana::Task.find(taskid[0]) - - if task - task.create_story(text: push_msg + ' ' + message) - end - end - - # close all tasks that had 'fix(ed/es/ing) #:id' in them - close_list.each do |taskid| - task = Asana::Task.find(taskid.last) - - if task - task.modify(completed: true) + # matches either: + # - #1234 + # - https://app.asana.com/0/0/1234 + # optionally preceded with: + # - fix/ed/es/ing + # - close/s/d + # - closing + issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i + + message.scan(issue_finder).each do |tuple| + # tuple will be + # [ 'fix', 'id_from_url', 'id_from_pound' ] + taskid = tuple[2] || tuple[1] + + begin + task = Asana::Task.find_by_id(client, taskid) + task.add_comment(text: "#{push_msg} #{message}") + + if tuple[0] + task.update(completed: true) + end + rescue => e + Rails.logger.error(e.message) + next end end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index d31b12f539e..aa8746beb80 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -23,19 +23,14 @@ class BambooService < CiService prop_accessor :bamboo_url, :build_key, :username, :password - validates :bamboo_url, - presence: true, - format: { with: /\A#{URI.regexp}\z/ }, - if: :activated? + validates :bamboo_url, presence: true, url: true, if: :activated? validates :build_key, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, - if: :activated? + if: ->(service) { service.activated? && service.password } validates :password, presence: true, - if: ->(service) { service.username? }, - if: :activated? + if: ->(service) { service.activated? && service.username } attr_accessor :response @@ -84,7 +79,7 @@ class BambooService < CiService def supported_events %w(push) end - + def build_info(sha) url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 40058b53df5..199ee3a9d0d 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -37,7 +37,7 @@ class BuildkiteService < CiService def compose_service_hook hook = service_hook || build_service_hook hook.url = webhook_url - hook.enable_ssl_verification = enable_ssl_verification + hook.enable_ssl_verification = !!enable_ssl_verification hook.save end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb new file mode 100644 index 00000000000..8247c79fc33 --- /dev/null +++ b/app/models/project_services/builds_email_service.rb @@ -0,0 +1,90 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +class BuildsEmailService < Service + prop_accessor :recipients + boolean_accessor :add_pusher + boolean_accessor :notify_only_broken_builds + validates :recipients, presence: true, if: :activated? + + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end + + def title + 'Builds emails' + end + + def description + 'Email the builds status to a list of recipients.' + end + + def to_param + 'builds_email' + end + + def supported_events + %w(build) + end + + def execute(push_data) + return unless supported_events.include?(push_data[:object_kind]) + + if should_build_be_notified?(push_data) + BuildEmailWorker.perform_async( + push_data[:build_id], + all_recipients(push_data), + push_data, + ) + end + end + + def fields + [ + { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' }, + { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + ] + end + + def should_build_be_notified?(data) + case data[:build_status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end + + def all_recipients(data) + all_recipients = recipients.split(',') + + if add_pusher? && data[:user][:email] + all_recipients << "#{data[:user][:email]}" + end + + all_recipients + end +end diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb deleted file mode 100644 index cbf325cc525..00000000000 --- a/app/models/project_services/ci/hip_chat_message.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Ci - class HipChatMessage - include Gitlab::Application.routes.url_helpers - - attr_reader :build - - def initialize(build) - @build = build - end - - def to_s - lines = Array.new - lines.push("<a href=\"#{ci_project_url(project)}\">#{project.name}</a> - ") - lines.push("<a href=\"#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}\">Commit ##{commit.id}</a></br>") - lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>") - lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).") - lines.join('') - end - - def status_color(build_or_commit=nil) - build_or_commit ||= commit_status - case build_or_commit - when :success - 'green' - when :failed, :canceled - 'red' - else # :pending, :running or unknown - 'yellow' - end - end - - def notify? - [:failed, :canceled].include?(commit_status) - end - - - private - - def commit - build.commit - end - - def project - commit.project - end - - def build_status - build.status.to_sym - end - - def commit_status - commit.status.to_sym - end - - def humanized_status(build_or_commit=nil) - build_or_commit ||= commit_status - case build_or_commit - when :pending - "Pending" - when :running - "Running" - when :failed - "Failed" - when :success - "Successful" - when :canceled - "Canceled" - else - "Unknown" - end - end - end -end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb deleted file mode 100644 index f17993d9f3b..00000000000 --- a/app/models/project_services/ci/hip_chat_service.rb +++ /dev/null @@ -1,93 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -module Ci - class HipChatService < Ci::Service - prop_accessor :hipchat_token, :hipchat_room, :hipchat_server - boolean_accessor :notify_only_broken_builds - validates :hipchat_token, presence: true, if: :activated? - validates :hipchat_room, presence: true, if: :activated? - default_value_for :notify_only_broken_builds, true - - def title - "HipChat" - end - - def description - "Private group chat, video chat, instant messaging for teams" - end - - def help - end - - def to_param - 'hip_chat' - end - - def fields - [ - { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' }, - { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' }, - { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' }, - { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } - ] - end - - def can_execute?(build) - return if build.allow_failure? - - commit = build.commit - return unless commit - return unless commit.latest_builds.include? build - - case commit.status.to_sym - when :failed - true - when :success - true unless notify_only_broken_builds? - else - false - end - end - - def execute(build) - msg = Ci::HipChatMessage.new(build) - opts = default_options.merge( - token: hipchat_token, - room: hipchat_room, - server: server_url, - color: msg.status_color, - notify: msg.notify? - ) - Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts) - end - - private - - def default_options - { - service_name: 'GitLab CI', - message_format: 'html' - } - end - - def server_url - if hipchat_server.blank? - 'https://api.hipchat.com' - else - hipchat_server - end - end - end -end diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb deleted file mode 100644 index fd193301001..00000000000 --- a/app/models/project_services/ci/mail_service.rb +++ /dev/null @@ -1,84 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -module Ci - class MailService < Ci::Service - delegate :email_recipients, :email_recipients=, - :email_add_pusher, :email_add_pusher=, - :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false - - before_save :update_project - - default_value_for :active, true - - def title - 'Mail' - end - - def description - 'Email notification' - end - - def to_param - 'mail' - end - - def fields - [ - { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' }, - { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' }, - { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' } - ] - end - - def can_execute?(build) - return if build.allow_failure? - - # it doesn't make sense to send emails for retried builds - commit = build.commit - return unless commit - return unless commit.latest_builds.include?(build) - - case build.status.to_sym - when :failed - true - when :success - true unless email_only_broken_builds - else - false - end - end - - def execute(build) - build.project_recipients.each do |recipient| - case build.status.to_sym - when :success - mailer.build_success_email(build.id, recipient) - when :failed - mailer.build_fail_email(build.id, recipient) - end - end - end - - private - - def update_project - project.save! - end - - def mailer - Ci::Notify.delay - end - end -end diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb deleted file mode 100644 index dc050a3fc59..00000000000 --- a/app/models/project_services/ci/slack_message.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'slack-notifier' - -module Ci - class SlackMessage - include Gitlab::Application.routes.url_helpers - - def initialize(commit) - @commit = commit - end - - def pretext - '' - end - - def color - attachment_color - end - - def fallback - format(attachment_message) - end - - def attachments - fields = [] - - commit.latest_builds.each do |build| - next if build.allow_failure? - next unless build.failed? - fields << { - title: build.name, - value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." - } - end - - [{ - text: attachment_message, - color: attachment_color, - fields: fields - }] - end - - private - - attr_reader :commit - - def attachment_message - out = "<#{ci_project_url(project)}|#{project_name}>: " - out << "Commit <#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> " - out << "(<#{commit_sha_link}|#{commit.short_sha}>) " - out << "of <#{commit_ref_link}|#{commit.ref}> " - out << "by #{commit.git_author_name} " if commit.git_author_name - out << "#{commit_status} in " - out << "#{commit.duration} second(s)" - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def project - commit.project - end - - def project_name - project.name - end - - def commit_sha_link - "#{project.gitlab_url}/commit/#{commit.sha}" - end - - def commit_ref_link - "#{project.gitlab_url}/commits/#{commit.ref}" - end - - def attachment_color - if commit.success? - 'good' - else - 'danger' - end - end - - def commit_status - if commit.success? - 'succeeded' - else - 'failed' - end - end - end -end diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb deleted file mode 100644 index ee8e4988826..00000000000 --- a/app/models/project_services/ci/slack_service.rb +++ /dev/null @@ -1,81 +0,0 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# - -module Ci - class SlackService < Ci::Service - prop_accessor :webhook - boolean_accessor :notify_only_broken_builds - validates :webhook, presence: true, if: :activated? - - default_value_for :notify_only_broken_builds, true - - def title - 'Slack' - end - - def description - 'A team communication tool for the 21st century' - end - - def to_param - 'slack' - end - - def help - 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present? - end - - def fields - [ - { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' }, - { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } - ] - end - - def can_execute?(build) - return if build.allow_failure? - - commit = build.commit - return unless commit - return unless commit.latest_builds.include?(build) - - case commit.status.to_sym - when :failed - true - when :success - true unless notify_only_broken_builds? - else - false - end - end - - def execute(build) - message = Ci::SlackMessage.new(build.commit) - options = default_options.merge( - color: message.color, - fallback: message.fallback, - attachments: message.attachments - ) - Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options) - end - - private - - def default_options - { - username: 'GitLab CI' - } - end - end -end diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index c73c4b058a1..08e5ccb3855 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -19,21 +19,19 @@ # class DroneCiService < CiService - + prop_accessor :drone_url, :token, :enable_ssl_verification - validates :drone_url, - presence: true, - format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated? - validates :token, - presence: true, - if: :activated? + + validates :drone_url, presence: true, url: true, if: :activated? + validates :token, presence: true, if: :activated? after_save :compose_service_hook, if: :activated? def compose_service_hook hook = service_hook || build_service_hook - hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join - hook.enable_ssl_verification = enable_ssl_verification + # If using a service template, project may not be available + hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project + hook.enable_ssl_verification = !!enable_ssl_verification hook.save end @@ -57,16 +55,16 @@ class DroneCiService < CiService end def merge_request_status_path(iid, sha = nil, ref = nil) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", + url = [drone_url, + "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", "?access_token=#{token}"] URI.join(*url).to_s end def commit_status_path(sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", + url = [drone_url, + "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"] URI.join(*url).to_s @@ -113,15 +111,15 @@ class DroneCiService < CiService end def merge_request_page(iid, sha, ref) - url = [drone_url, + url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] URI.join(*url).to_s end def commit_page(sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", + url = [drone_url, + "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}"] URI.join(*url).to_s @@ -162,10 +160,10 @@ class DroneCiService < CiService end def push_valid?(data) - opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id, + opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id, source_branch: Gitlab::Git.ref_name(data[:ref])) - opened_merge_requests.empty? && data[:total_commits_count] > 0 && + opened_merge_requests.empty? && data[:total_commits_count] > 0 && !Gitlab::Git.blank_ref?(data[:after]) end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 9c46af7e721..74c57949b4d 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -22,10 +22,8 @@ class ExternalWikiService < Service include HTTParty prop_accessor :external_wiki_url - validates :external_wiki_url, - presence: true, - format: { with: /\A#{URI.regexp}\z/ }, - if: :activated? + + validates :external_wiki_url, presence: true, url: true, if: :activated? def title 'External Wiki' diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 27fc19379f1..15c7c907f7e 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -58,6 +58,6 @@ class FlowdockService < Service repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s", - ) + ) end end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 91ef267ad79..202fee042e3 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -57,6 +57,6 @@ class GemnasiumService < Service token: token, api_key: api_key, repo: project.repository.path_to_repo - ) + ) end end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 4dcd16ede3a..b64d97ce75d 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -18,76 +18,11 @@ # note_events :boolean default(TRUE), not null # +# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed class GitlabCiService < CiService - include Gitlab::Application.routes.url_helpers - - after_save :compose_service_hook, if: :activated? - after_save :ensure_gitlab_ci_project, if: :activated? - - def compose_service_hook - hook = service_hook || build_service_hook - hook.save - end - - def ensure_gitlab_ci_project - project.ensure_gitlab_ci_project - end - - def supported_events - %w(push tag_push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - ci_project = project.gitlab_ci_project - if ci_project - current_user = User.find_by(id: data[:user_id]) - Ci::CreateCommitService.new.execute(ci_project, current_user, data) - end - end - - def token - if project.gitlab_ci_project.present? - project.gitlab_ci_project.token - end - end - - def get_ci_commit(sha, ref) - Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha!(sha) - end - - def commit_status(sha, ref) - get_ci_commit(sha, ref).status - rescue ActiveRecord::RecordNotFound - :error - end - - def commit_coverage(sha, ref) - get_ci_commit(sha, ref).coverage - rescue ActiveRecord::RecordNotFound - :error - end - - def build_page(sha, ref) - if project.gitlab_ci_project.present? - ci_namespace_project_commit_url(project.namespace, project, sha) - end - end - - def title - 'GitLab CI' - end - - def description - 'Continuous integration server from GitLab' - end - - def to_param - 'gitlab_ci' - end - - def fields - [] + # We override the active accessor to always make GitLabCiService disabled + # Otherwise the GitLabCiService can be picked, but should never be since it's deprecated + def active + false end end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index af2840a57f0..1e1686a11c6 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -22,8 +22,16 @@ class HipchatService < Service MAX_COMMITS = 3 prop_accessor :token, :room, :server, :notify, :color, :api_version + boolean_accessor :notify_only_broken_builds validates :token, presence: true, if: :activated? + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end + def title 'HipChat' end @@ -45,12 +53,13 @@ class HipchatService < Service { type: 'text', name: 'api_version', placeholder: 'Leave blank for default (v2)' }, { type: 'text', name: 'server', - placeholder: 'Leave blank for default. https://hipchat.example.com' } + placeholder: 'Leave blank for default. https://hipchat.example.com' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, ] end def supported_events - %w(push issue merge_request note tag_push) + %w(push issue merge_request note tag_push build) end def execute(data) @@ -94,6 +103,8 @@ class HipchatService < Service create_merge_request_message(data) unless is_update?(data) when "note" create_note_message(data) + when "build" + create_build_message(data) if should_build_be_notified?(data) end end @@ -235,6 +246,20 @@ class HipchatService < Service message end + def create_build_message(data) + ref_type = data[:tag] ? 'tag' : 'branch' + ref = data[:ref] + sha = data[:sha] + user_name = data[:commit][:author_name] + status = data[:commit][:status] + duration = data[:commit][:duration] + + branch_link = "<a href=\"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>" + commit_link = "<a href=\"#{project_url}/commit/#{URI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>" + + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" + end + def project_name project.name_with_namespace.gsub(/\s/, '') end @@ -250,4 +275,24 @@ class HipchatService < Service def is_update?(data) data[:object_attributes][:action] == 'update' end + + def humanized_status(status) + case status + when 'success' + 'passed' + else + status + end + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 35e30b1cb0b..e216f406e1c 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,9 +19,24 @@ # class JiraService < IssueTrackerService + include HTTParty include Gitlab::Application.routes.url_helpers - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + DEFAULT_API_VERSION = 2 + + prop_accessor :username, :password, :api_url, :jira_issue_transition_id, + :title, :description, :project_url, :issues_url, :new_issue_url + + before_validation :set_api_url, :set_jira_issue_transition_id + + before_update :reset_password + + def reset_password + # don't reset the password if a new one is provided + if api_url_changed? && !password_touched? + self.password = nil + end + end def help line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ @@ -54,4 +69,228 @@ class JiraService < IssueTrackerService def to_param 'jira' end + + def fields + super.push( + { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' }, + { type: 'text', name: 'username', placeholder: '' }, + { type: 'password', name: 'password', placeholder: '' }, + { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } + ) + end + + def execute(push, issue = nil) + if issue.nil? + # No specific issue, that means + # we just want to test settings + test_settings + else + close_issue(push, issue) + end + end + + def create_cross_reference_note(mentioned, noteable, author) + issue_name = mentioned.id + project = self.project + noteable_name = noteable.class.name.underscore.downcase + noteable_id = if noteable.is_a?(Commit) + noteable.id + else + noteable.iid + end + + entity_url = build_entity_url(noteable_name.to_sym, noteable_id) + + data = { + user: { + name: author.name, + url: resource_url(user_path(author)), + }, + project: { + name: project.path_with_namespace, + url: resource_url(namespace_project_path(project.namespace, project)) + }, + entity: { + name: noteable_name.humanize.downcase, + url: entity_url + } + } + + add_comment(data, issue_name) + end + + def test_settings + result = JiraService.get( + jira_api_test_url, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + case result.code + when 201, 200 + Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.") + true + else + Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") + false + end + rescue Errno::ECONNREFUSED => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}." + false + end + + private + + def build_api_url_from_project_url + server = URI(project_url) + default_ports = [["http",80],["https",443]].include?([server.scheme,server.port]) + server_url = "#{server.scheme}://#{server.host}" + server_url.concat(":#{server.port}") unless default_ports + "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" + rescue + "" # looks like project URL was not valid + end + + def set_api_url + self.api_url = build_api_url_from_project_url if self.api_url.blank? + end + + def set_jira_issue_transition_id + self.jira_issue_transition_id ||= "2" + end + + def close_issue(entity, issue) + commit_id = if entity.is_a?(Commit) + entity.id + elsif entity.is_a?(MergeRequest) + entity.last_commit.id + end + commit_url = build_entity_url(:commit, commit_id) + + # Depending on the JIRA project's workflow, a comment during transition + # may or may not be allowed. Split the operation in to two calls so the + # comment always works. + transition_issue(issue) + add_issue_solved_comment(issue, commit_id, commit_url) + end + + def transition_issue(issue) + message = { + transition: { + id: jira_issue_transition_id + } + } + send_message(close_issue_url(issue.iid), message.to_json) + end + + def add_issue_solved_comment(issue, commit_id, commit_url) + comment = { + body: "Issue solved with [#{commit_id}|#{commit_url}]." + } + + send_message(comment_url(issue.iid), comment.to_json) + end + + def add_comment(data, issue_name) + url = comment_url(issue_name) + user_name = data[:user][:name] + user_url = data[:user][:url] + entity_name = data[:entity][:name] + entity_url = data[:entity][:url] + project_name = data[:project][:name] + + message = { + body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]." + } + + unless existing_comment?(issue_name, message[:body]) + send_message(url, message.to_json) + end + end + + + def auth + require 'base64' + Base64.urlsafe_encode64("#{self.username}:#{self.password}") + end + + def send_message(url, message) + result = JiraService.post( + url, + body: message, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + message = case result.code + when 201, 200, 204 + "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." + when 401 + "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." + else + "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" + end + + Rails.logger.info(message) + message + rescue URI::InvalidURIError, Errno::ECONNREFUSED => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}." + end + + def existing_comment?(issue_name, new_comment) + result = JiraService.get( + comment_url(issue_name), + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Basic #{auth}" + } + ) + + case result.code + when 201, 200 + existing_comments = JSON.parse(result.body)['comments'] + + if existing_comments.present? + return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any? + end + end + + false + rescue JSON::ParserError + false + end + + def resource_url(resource) + "#{Settings.gitlab['url'].chomp("/")}#{resource}" + end + + def build_entity_url(entity_name, entity_id) + resource_url( + polymorphic_url( + [ + self.project.namespace.becomes(Namespace), + self.project, + entity_name + ], + id: entity_id, + routing_type: :path + ) + ) + end + + def close_issue_url(issue_name) + "#{self.api_url}/issue/#{issue_name}/transitions" + end + + def comment_url(issue_name) + "#{self.api_url}/issue/#{issue_name}/comment" + end + + def jira_api_test_url + "#{self.api_url}/myself" + end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 7cd5e892507..375b4534d07 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -20,8 +20,16 @@ class SlackService < Service prop_accessor :webhook, :username, :channel + boolean_accessor :notify_only_broken_builds validates :webhook, presence: true, if: :activated? + def initialize_properties + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end + def title 'Slack' end @@ -45,12 +53,13 @@ class SlackService < Service { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, { type: 'text', name: 'username', placeholder: 'username' }, - { type: 'text', name: 'channel', placeholder: '#channel' } + { type: 'text', name: 'channel', placeholder: '#channel' }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, ] end def supported_events - %w(push issue merge_request note tag_push) + %w(push issue merge_request note tag_push build) end def execute(data) @@ -78,6 +87,8 @@ class SlackService < Service MergeMessage.new(data) unless is_update?(data) when "note" NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) end opt = {} @@ -86,7 +97,7 @@ class SlackService < Service if message notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments) + notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) end end @@ -103,9 +114,21 @@ class SlackService < Service def is_update?(data) data[:object_attributes][:action] == 'update' end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end end require "slack_service/issue_message" require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" +require "slack_service/build_message" diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb index aa00d6061a1..f1182824687 100644 --- a/app/models/project_services/slack_service/base_message.rb +++ b/app/models/project_services/slack_service/base_message.rb @@ -10,6 +10,9 @@ class SlackService format(message) end + def fallback + end + def attachments raise NotImplementedError end diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb new file mode 100644 index 00000000000..c124cad4afd --- /dev/null +++ b/app/models/project_services/slack_service/build_message.rb @@ -0,0 +1,82 @@ +class SlackService + class BuildMessage < BaseMessage + attr_reader :sha + attr_reader :ref_type + attr_reader :ref + attr_reader :status + attr_reader :project_name + attr_reader :project_url + attr_reader :user_name + attr_reader :duration + + def initialize(params, commit = true) + @sha = params[:sha] + @ref_type = params[:tag] ? 'tag' : 'branch' + @ref = params[:ref] + @project_name = params[:project_name] + @project_url = params[:project_url] + @status = params[:commit][:status] + @user_name = params[:commit][:author_name] + @duration = params[:commit][:duration] + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def commit_url + "#{project_url}/commit/#{sha}/builds" + end + + def commit_link + "[#{Commit.truncate_sha(sha)}](#{commit_url})" + end + end +end diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index 074478b292d..b15d9a14677 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -45,30 +45,27 @@ class SlackService def create_commit_note(commit) commit_sha = commit[:id] commit_sha = Commit.truncate_sha(commit_sha) - commit_link = "[commit #{commit_sha}](#{@note_url})" - title = format_title(commit[:message]) - @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*" + commented_on_message( + "[commit #{commit_sha}](#{@note_url})", + format_title(commit[:message])) end def create_issue_note(issue) - issue_iid = issue[:iid] - note_link = "[issue ##{issue_iid}](#{@note_url})" - title = format_title(issue[:title]) - @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*" + commented_on_message( + "[issue ##{issue[:iid]}](#{@note_url})", + format_title(issue[:title])) end def create_merge_note(merge_request) - merge_request_id = merge_request[:iid] - merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" - title = format_title(merge_request[:title]) - @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*" + commented_on_message( + "[merge request ##{merge_request[:iid]}](#{@note_url})", + format_title(merge_request[:title])) end def create_snippet_note(snippet) - snippet_id = snippet[:id] - snippet_link = "[snippet ##{snippet_id}](#{@note_url})" - title = format_title(snippet[:title]) - @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*" + commented_on_message( + "[snippet ##{snippet[:id]}](#{@note_url})", + format_title(snippet[:title])) end def description_message @@ -78,5 +75,9 @@ class SlackService def project_link "[#{@project_name}](#{@project_url})" end + + def commented_on_message(target_link, title) + @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*" + end end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 0b022461250..a63700693d7 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -23,16 +23,14 @@ class TeamcityService < CiService prop_accessor :teamcity_url, :build_type, :username, :password - validates :teamcity_url, - presence: true, - format: { with: /\A#{URI.regexp}\z/ }, if: :activated? + validates :teamcity_url, presence: true, url: true, if: :activated? validates :build_type, presence: true, if: :activated? validates :username, presence: true, - if: ->(service) { service.password? }, if: :activated? + if: ->(service) { service.activated? && service.password } validates :password, presence: true, - if: ->(service) { service.username? }, if: :activated? + if: ->(service) { service.activated? && service.username } attr_accessor :response @@ -147,6 +145,6 @@ class TeamcityService < CiService '</build>', headers: { 'Content-type' => 'application/xml' }, basic_auth: auth - ) + ) end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 231973fa543..b5fec38378b 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -86,6 +86,8 @@ class ProjectWiki commit = commit_details(:created, message, title) wiki.write_page(title, format, content, commit) + + update_project_activity rescue Gollum::DuplicatePageError => e @error_message = "Duplicate page: #{e.message}" return false @@ -95,10 +97,14 @@ class ProjectWiki commit = commit_details(:updated, message, page.title) wiki.update_page(page, page.name, format, content, commit) + + update_project_activity end def delete_page(page, message = nil) wiki.delete_page(page, commit_details(:deleted, message, page.title)) + + update_project_activity end def page_title_and_dir(title) @@ -146,4 +152,8 @@ class ProjectWiki def path_to_repo @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") end + + def update_project_activity + @project.touch(:last_activity_at) + end end diff --git a/app/models/release.rb b/app/models/release.rb new file mode 100644 index 00000000000..89f70278af5 --- /dev/null +++ b/app/models/release.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: releases +# +# id :integer not null, primary key +# tag :string(255) +# description :text +# project_id :integer +# created_at :datetime +# updated_at :datetime +# + +class Release < ActiveRecord::Base + belongs_to :project + + validates :description, :project, :tag, presence: true +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 0808896fd87..6ecd2d2f27e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,12 +1,11 @@ require 'securerandom' class Repository - class PreReceiveError < StandardError; end class CommitError < StandardError; end include Gitlab::ShellAdapter - attr_accessor :raw_repository, :path_with_namespace, :project + attr_accessor :path_with_namespace, :project def self.clean_old_archives repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path @@ -19,14 +18,18 @@ class Repository def initialize(path_with_namespace, default_branch = nil, project = nil) @path_with_namespace = path_with_namespace @project = project + end - if path_with_namespace - @raw_repository = Gitlab::Git::Repository.new(path_to_repo) - @raw_repository.autocrlf = :input - end + def raw_repository + return nil unless path_with_namespace - rescue Gitlab::Git::Repository::NoRepository - nil + @raw_repository ||= begin + repo = Gitlab::Git::Repository.new(path_to_repo) + repo.autocrlf = :input + repo + rescue Gitlab::Git::Repository::NoRepository + nil + end end # Return absolute path to repository @@ -44,6 +47,19 @@ class Repository raw_repository.empty? end + # + # Git repository can contains some hidden refs like: + # /refs/notes/* + # /refs/git-as-svn/* + # /refs/pulls/* + # This refs by default not visible in project page and not cloned to client side. + # + # This method return true if repository contains some content visible in project page. + # + def has_visible_content? + !raw_repository.branches.empty? + end + def commit(id = 'HEAD') return nil unless raw_repository commit = Gitlab::Git::Commit.find(raw_repository, id) @@ -54,13 +70,18 @@ class Repository end def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false) - commits = Gitlab::Git::Commit.where( + options = { repo: raw_repository, ref: ref, path: path, limit: limit, offset: offset, - ) + # --follow doesn't play well with --skip. See: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 + follow: false + } + + commits = Gitlab::Git::Commit.where(options) commits = Commit.decorate(commits, @project) if commits.present? commits end @@ -71,38 +92,62 @@ class Repository commits end + def find_commits_by_message(query) + # Limited to 1000 commits for now, could be parameterized? + args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query}) + + git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) + commits = git_log_results.map { |c| commit(c) } + commits + end + def find_branch(name) - branches.find { |branch| branch.name == name } + raw_repository.branches.find { |branch| branch.name == name } end def find_tag(name) - tags.find { |tag| tag.name == name } + raw_repository.tags.find { |tag| tag.name == name } end - def add_branch(branch_name, ref) - cache.expire(:branch_names) - @branches = nil + def add_branch(user, branch_name, target) + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + target = commit(target).try(:id) + + return false unless target - gitlab_shell.add_branch(path_with_namespace, branch_name, ref) + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do + rugged.branches.create(branch_name, target) + end + + expire_branches_cache + find_branch(branch_name) end def add_tag(tag_name, ref, message = nil) - cache.expire(:tag_names) - @tags = nil + expire_tags_cache gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end - def rm_branch(branch_name) - cache.expire(:branch_names) - @branches = nil + def rm_branch(user, branch_name) + expire_branches_cache + + branch = find_branch(branch_name) + oldrev = branch.try(:target) + newrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + + GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do + rugged.branches.delete(branch_name) + end - gitlab_shell.rm_branch(path_with_namespace, branch_name) + expire_branches_cache + true end def rm_tag(tag_name) - cache.expire(:tag_names) - @tags = nil + expire_tags_cache gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -130,11 +175,29 @@ class Repository def size cache.fetch(:size) { raw_repository.size } end + + def diverging_commit_counts(branch) + root_ref_hash = raw_repository.rev_parse_target(root_ref).oid + cache.fetch(:"diverging_commit_counts_#{branch.name}") do + # Rugged seems to throw a `ReferenceError` when given branch_names rather + # than SHA-1 hashes + number_commits_behind = commits_between(branch.target, root_ref_hash).size + number_commits_ahead = commits_between(root_ref_hash, branch.target).size + + { behind: number_commits_behind, ahead: number_commits_ahead } + end + end def cache_keys %i(size branch_names tag_names commit_count readme version contribution_guide changelog license) end + + def branch_cache_keys + branches.map do |branch| + :"diverging_commit_counts_#{branch.name}" + end + end def build_cache cache_keys.each do |key| @@ -142,12 +205,36 @@ class Repository send(key) end end + + branches.each do |branch| + unless cache.exist?(:"diverging_commit_counts_#{branch.name}") + send(:diverging_commit_counts, branch) + end + end + end + + def expire_tags_cache + cache.expire(:tag_names) + @tags = nil + end + + def expire_branches_cache + cache.expire(:branch_names) + @branches = nil end def expire_cache cache_keys.each do |key| cache.expire(key) end + + expire_branch_cache + end + + def expire_branch_cache + branches.each do |branch| + cache.expire(:"diverging_commit_counts_#{branch.name}") + end end def rebuild_cache @@ -155,6 +242,11 @@ class Repository cache.expire(key) send(key) end + + branches.each do |branch| + cache.expire(:"diverging_commit_counts_#{branch.name}") + diverging_commit_counts(branch) + end end def lookup_cache @@ -218,9 +310,25 @@ class Repository def license cache.fetch(:license) do - tree(:head).blobs.find do |file| - file.name =~ /\Alicense/i + 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 + ] + + license = nil + preferences.each do |r| + license = licenses.find { |l| l.name =~ r } + break if license end + + license end end @@ -271,11 +379,22 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(git rev-list --max-count=1 #{sha} -- #{path}) + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) sha = Gitlab::Popen.popen(args, path_to_repo).first.strip commit(sha) end + def next_patch_branch + patch_branch_ids = self.branch_names.map do |n| + result = n.match(/\Apatch-([0-9]+)\z/) + result[1].to_i if result + end.compact + + highest_patch_branch_id = patch_branch_ids.max || 0 + + "patch-#{highest_patch_branch_id + 1}" + end + # Remove archives older than 2 hours def branches_sorted_by(value) case value @@ -321,8 +440,8 @@ class Repository end end - def branch_names_contains(sha) - args = %W(git branch --contains #{sha}) + def refs_contains_sha(ref_type, sha) + args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first if names.respond_to?(:split) @@ -338,21 +457,12 @@ class Repository end end - def tag_names_contains(sha) - args = %W(git tag --contains #{sha}) - names = Gitlab::Popen.popen(args, path_to_repo).first - - if names.respond_to?(:split) - names = names.split("\n").map(&:strip) - - names.each do |name| - name.slice! '* ' - end + def branch_names_contains(sha) + refs_contains_sha('branch', sha) + end - names - else - [] - end + def tag_names_contains(sha) + refs_contains_sha('tag', sha) end def branches @@ -468,7 +578,7 @@ class Repository root_ref_commit = commit(root_ref) if branch_commit - rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id + is_ancestor?(branch_commit.id, root_ref_commit.id) else nil end @@ -478,9 +588,14 @@ class Repository rugged.merge_base(first_commit_id, second_commit_id) end + def is_ancestor?(ancestor_id, descendant_id) + merge_base(ancestor_id, descendant_id) == ancestor_id + end + + def search_files(query, ref) offset = 2 - args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end @@ -512,60 +627,57 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(git fetch #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end - def commit_with_hooks(current_user, branch) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - gl_id = Gitlab::ShellEnv.gl_id(current_user) - was_empty = empty? - - # Create temporary ref + def with_tmp_ref(oldrev = nil) random_string = SecureRandom.hex tmp_ref = "refs/tmp/#{random_string}/head" - unless was_empty - oldrev = find_branch(branch).target + if oldrev && !Gitlab::Git.blank_ref?(oldrev) rugged.references.create(tmp_ref, oldrev) end # Make commit in tmp ref - newrev = yield(tmp_ref) + yield(tmp_ref) + ensure + rugged.references.delete(tmp_ref) rescue nil + end - unless newrev - raise CommitError.new('Failed to create commit') + def commit_with_hooks(current_user, branch) + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch + was_empty = empty? + + unless was_empty + oldrev = find_branch(branch).target end - # Run GitLab pre-receive hook - pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo) - status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) + with_tmp_ref(oldrev) do |tmp_ref| + # Make commit in tmp ref + newrev = yield(tmp_ref) - if status - if was_empty - # Create branch - rugged.references.create(ref, newrev) - else - # Update head - current_head = find_branch(branch).target + unless newrev + raise CommitError.new('Failed to create commit') + end - # Make sure target branch was not changed during pre-receive hook - if current_head == oldrev - rugged.references.update(ref, newrev) + GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do + if was_empty + # Create branch + rugged.references.create(ref, newrev) else - raise CommitError.new('Commit was rejected because branch received new push') + # Update head + current_head = find_branch(branch).target + + # Make sure target branch was not changed during pre-receive hook + if current_head == oldrev + rugged.references.update(ref, newrev) + else + raise CommitError.new('Commit was rejected because branch received new push') + end end end - - # Run GitLab post receive hook - post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo) - post_receive_hook.trigger(gl_id, oldrev, newrev, ref) - else - # Remove tmp ref and return error to user - rugged.references.delete(tmp_ref) - - raise PreReceiveError.new('Commit was rejected by pre-receive hook') end end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 3eed5c16e45..f36eda1531b 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -17,12 +17,11 @@ class SentNotification < ActiveRecord::Base belongs_to :noteable, polymorphic: true belongs_to :recipient, class_name: "User" - validate :project, :recipient, :reply_key, presence: true - validate :reply_key, uniqueness: true - + validates :project, :recipient, :reply_key, presence: true + validates :reply_key, uniqueness: true validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true + validates :line_code, line_code: true, allow_blank: true class << self def reply_key diff --git a/app/models/service.rb b/app/models/service.rb index d610abd1683..d3bf7f0ebd1 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -30,6 +30,7 @@ class Service < ActiveRecord::Base default_value_for :merge_requests_events, true default_value_for :tag_push_events, true default_value_for :note_events, true + default_value_for :build_events, true after_initialize :initialize_properties @@ -40,13 +41,14 @@ class Service < ActiveRecord::Base validates :project_id, presence: true, unless: Proc.new { |service| service.template? } - scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } + scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) } scope :push_hooks, -> { where(push_events: true, active: true) } scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } scope :issue_hooks, -> { where(issues_events: true, active: true) } 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) } def activated? active @@ -133,6 +135,21 @@ class Service < ActiveRecord::Base end end + # Provide convenient boolean accessor methods + # for each serialized property. + # Also keep track of updated properties in a similar way as ActiveModel::Dirty + def self.boolean_accessor(*args) + self.prop_accessor(*args) + + args.each do |arg| + class_eval %{ + def #{arg}? + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end + } + end + end + # Returns a hash of the properties that have been assigned a new value since last save, # indicating their original values (attr => original value). # ActiveRecord does not provide a mechanism to track changes in serialized keys, @@ -163,6 +180,7 @@ class Service < ActiveRecord::Base assembla bamboo buildkite + builds_email campfire custom_issue_tracker drone_ci @@ -170,7 +188,6 @@ class Service < ActiveRecord::Base external_wiki flowdock gemnasium - gitlab_ci hipchat irker jira diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b0831982aa7..f876be7a4c8 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -65,6 +65,10 @@ class Snippet < ActiveRecord::Base }x end + def self.link_reference_pattern + super("snippets", /(?<snippet>\d+)/) + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{id}" diff --git a/app/models/tree.rb b/app/models/tree.rb index 93b3246a668..e0e04d8859f 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -17,18 +17,16 @@ class Tree def readme return @readme if defined?(@readme) - available_readmes = blobs.select(&:readme?) + # Take the first previewable readme, or return nil if none is available or + # we can't preview any of them + readme_tree = blobs.find do |blob| + blob.readme? && (previewable?(blob.name) || plain?(blob.name)) + end - if available_readmes.count == 0 + if readme_tree.nil? return @readme = nil end - # Take the first previewable readme, or the first available readme, if we - # can't preview any of them - readme_tree = available_readmes.find do |readme| - previewable?(readme.name) - end || available_readmes.first - readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) git_repo = repository.raw_repository diff --git a/app/models/user.rb b/app/models/user.rb index 7e4321d5376..20f907e4347 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime +# unlock_token :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null @@ -54,7 +55,9 @@ # public_email :string(255) default(""), not null # dashboard :integer default(0) # project_view :integer default(0) +# consumed_timestep :integer # layout :integer default(0) +# hide_project_limit :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -67,8 +70,10 @@ class User < ActiveRecord::Base include Gitlab::CurrentSettings include Referable include Sortable - include TokenAuthenticatable include CaseSensitivity + include TokenAuthenticatable + + add_authentication_token_field :authentication_token default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group @@ -132,7 +137,7 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy - has_many :ci_builds, dependent: :nullify, class_name: 'Ci::Build' + has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # @@ -147,11 +152,9 @@ class User < ActiveRecord::Base validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :username, + namespace: true, presence: true, - uniqueness: { case_sensitive: false }, - exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.namespace_regex, - message: Gitlab::Regex.namespace_regex_message } + uniqueness: { case_sensitive: false } validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } @@ -218,9 +221,9 @@ class User < ActiveRecord::Base def find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup if login = conditions.delete(:login) - where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first + where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase) else - where(conditions).first + find_by(conditions) end end @@ -235,21 +238,16 @@ class User < ActiveRecord::Base # Find a User by their primary email or any associated secondary email def find_by_any_email(email) - user_table = arel_table - email_table = Email.arel_table - - # Use ARel to build a query: - query = user_table. - # SELECT "users".* FROM "users" - project(user_table[Arel.star]). - # LEFT OUTER JOIN "emails" - join(email_table, Arel::Nodes::OuterJoin). - # ON "users"."id" = "emails"."user_id" - on(user_table[:id].eq(email_table[:user_id])). - # WHERE ("user"."email" = '<email>' OR "emails"."email" = '<email>') - where(user_table[:email].eq(email).or(email_table[:email].eq(email))) - - find_by_sql(query.to_sql).first + sql = 'SELECT * + FROM users + WHERE id IN ( + SELECT id FROM users WHERE email = :email + UNION + SELECT emails.user_id FROM emails WHERE email = :email + ) + LIMIT 1;' + + User.find_by_sql([sql, { email: email }]).first end def filter(filter_name) @@ -288,7 +286,7 @@ class User < ActiveRecord::Base end def by_username_or_id(name_or_id) - where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first + find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) end def build_user(attrs = {}) @@ -354,10 +352,13 @@ class User < ActiveRecord::Base end def namespace_uniq + # Return early if username already failed the first uniqueness validation + return if self.errors[:username].include?('has already been taken') + namespace_name = self.username existing_namespace = Namespace.by_path(namespace_name) if existing_namespace && existing_namespace != self.namespace - self.errors.add :username, "already exists" + self.errors.add(:username, 'has already been taken') end end @@ -393,31 +394,23 @@ class User < ActiveRecord::Base end end - # Groups user has access to + # Returns the groups a user has access to def authorized_groups - @authorized_groups ||= begin - group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) - Group.where(id: group_ids) - end - end + union = Gitlab::SQL::Union. + new([groups.select(:id), authorized_projects.select(:namespace_id)]) + Group.where("namespaces.id IN (#{union.to_sql})") + end - # Projects user has access to + # Returns the groups a user is authorized to access. def authorized_projects - @authorized_projects ||= begin - project_ids = personal_projects.pluck(:id) - project_ids.push(*groups_projects.pluck(:id)) - project_ids.push(*projects.pluck(:id).uniq) - Project.where(id: project_ids) - end + Project.where("projects.id IN (#{projects_union.to_sql})") end def owned_projects @owned_projects ||= - begin - namespace_ids = owned_groups.pluck(:id).push(namespace.id) - Project.in_namespace(namespace_ids).joins(:namespace) - end + Project.where('namespace_id IN (?) OR namespace_id = ?', + owned_groups.select(:id), namespace.id).joins(:namespace) end # Team membership in authorized projects @@ -649,11 +642,11 @@ class User < ActiveRecord::Base email.start_with?('temp-email-for-oauth') end - def avatar_url(size = nil) + def avatar_url(size = nil, scale = 2) if avatar.present? [gitlab_config.url, avatar.url].join else - GravatarService.new.execute(email, size) + GravatarService.new.execute(email, size, scale) end end @@ -702,7 +695,7 @@ class User < ActiveRecord::Base end def starred?(project) - starred_projects.exists?(project) + starred_projects.exists?(project.id) end def toggle_star(project) @@ -732,12 +725,25 @@ class User < ActiveRecord::Base Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end - def contributed_projects_ids - Event.contributions.where(author_id: self). + # Returns the projects a user contributed to in the last year. + # + # This method relies on a subquery as this performs significantly better + # compared to a JOIN when coupled with, for example, + # `Project.visible_to_user`. That is, consider the following code: + # + # some_user.contributed_projects.visible_to_user(other_user) + # + # If this method were to use a JOIN the resulting query would take roughly 200 + # ms on a database with a similar size to GitLab.com's database. On the other + # hand, using a subquery means we can get the exact same data in about 40 ms. + def contributed_projects + events = Event.select(:project_id). + contributions.where(author_id: self). where("created_at > ?", Time.now - 1.year). - reorder(project_id: :desc). - select(:project_id). - uniq.map(&:project_id) + uniq. + reorder(nil) + + Project.where(id: events) end def restricted_signup_domains @@ -767,12 +773,34 @@ class User < ActiveRecord::Base !solo_owned_groups.present? end - def ci_authorized_projects - @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects) + def ci_authorized_runners + @ci_authorized_runners ||= begin + runner_ids = Ci::RunnerProject. + where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})"). + select(:runner_id) + Ci::Runner.specific.where(id: runner_ids) + end end - def ci_authorized_runners - Ci::Runner.specific.includes(:runner_projects). - where(ci_runner_projects: { project_id: ci_authorized_projects } ) + private + + def projects_union + Gitlab::SQL::Union.new([personal_projects.select(:id), + groups_projects.select(:id), + projects.select(:id)]) + end + + def ci_projects_union + scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } + groups = groups_projects.where(members: scope) + other = projects.where(members: scope) + + Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id), + other.select(:id)]) + end + + # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration + def send_devise_notification(notification, *args) + devise_mailer.send(notification, self, *args).deliver_later end end diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 3d49cb05949..413f3f485a8 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -10,7 +10,7 @@ # class UsersStarProject < ActiveRecord::Base - belongs_to :project, counter_cache: :star_count + belongs_to :project, counter_cache: :star_count, touch: true belongs_to :user validates :user, presence: true diff --git a/app/services/base_service.rb b/app/services/base_service.rb index f00ec7408b6..b48ca67d4d2 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -39,10 +39,7 @@ class BaseService def deny_visibility_level(model, denied_visibility_level = nil) denied_visibility_level ||= model.visibility_level - level_name = 'Unknown' - Gitlab::VisibilityLevel.options.each do |name, level| - level_name = name if level == denied_visibility_level - end + level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level) model.errors.add( :visibility_level, diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 912eb6258a4..ad901f2da5d 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -29,9 +29,11 @@ module Ci build_attrs.merge!(ref: ref, tag: tag, trigger_request: trigger_request, - user: user) + user: user, + project: commit.project) - commit.builds.create!(build_attrs) + build = commit.builds.create!(build_attrs) + build.execute_hooks end end end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb deleted file mode 100644 index 479a2d6defc..00000000000 --- a/app/services/ci/create_commit_service.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Ci - class CreateCommitService - def execute(project, user, params) - sha = params[:checkout_sha] || params[:after] - origin_ref = params[:ref] - - unless origin_ref && sha.present? - return false - end - - ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') - - # Skip branch removal - if sha == Ci::Git::BLANK_SHA - return false - end - - tag = origin_ref.start_with?('refs/tags/') - commit = project.gl_project.ensure_ci_commit(sha) - unless commit.skip_ci? - commit.update_committed! - commit.create_builds(ref, tag, user) - end - - commit - end - end -end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 4b86cb0a1f5..b3dfc707221 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,13 +1,13 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - commit = project.gl_project.commit(ref) + commit = project.commit(ref) return unless commit # check if ref is tag - tag = project.gl_project.repository.find_tag(ref).present? + tag = project.repository.find_tag(ref).present? - ci_commit = project.gl_project.ensure_ci_commit(commit.sha) + ci_commit = project.ensure_ci_commit(commit.sha) trigger_request = trigger.trigger_requests.create!( variables: variables, diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb deleted file mode 100644 index 3f4e02dd26c..00000000000 --- a/app/services/ci/event_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Ci - class EventService - def remove_project(user, project) - create( - description: "Project \"#{project.name}\" has been removed by #{user.username}", - user_id: user.id, - is_admin: true - ) - end - - def create_project(user, project) - create( - description: "Project \"#{project.name}\" has been created by #{user.username}", - user_id: user.id, - is_admin: true - ) - end - - def change_project_settings(user, project) - create( - project_id: project.id, - user_id: user.id, - description: "User \"#{user.username}\" updated projects settings" - ) - end - - def create(*args) - Ci::Event.create!(*args) - end - end -end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index b95835ba093..f469b13e902 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -1,17 +1,15 @@ module Ci class ImageForBuildService def execute(project, params) - image_name = - if params[:sha] - commit = project.commits.find_by(sha: params[:sha]) - image_for_commit(commit) - elsif params[:ref] - commit = project.last_commit_for_ref(params[:ref]) - image_for_commit(commit) - else - 'build-unknown.svg' + sha = params[:sha] + sha ||= + if params[:ref] + project.commit(params[:ref]).try(:sha) end + commit = project.ci_commits.ordered.find_by(sha: sha) + image_name = image_for_commit(commit) + image_path = Rails.root.join('public/ci', image_name) OpenStruct.new( diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 7beb098659c..4ff268a6f06 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -8,10 +8,10 @@ module Ci builds = if current_runner.shared? # don't run projects which have not enables shared runners - builds.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true }) + builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }) else # do run projects which are only assigned to this runner - builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids }) + builds.where(project: current_runner.projects.where(builds_enabled: true)) end builds = builds.order('created_at ASC') @@ -20,10 +20,9 @@ module Ci build.can_be_served?(current_runner) end - if build # In case when 2 runners try to assign the same build, second runner will be declined - # with StateMachine::InvalidTransition in run! method. + # with StateMachines::InvalidTransition in run! method. build.with_lock do build.runner_id = current_runner.id build.save! @@ -33,7 +32,7 @@ module Ci build - rescue StateMachine::InvalidTransition + rescue StateMachines::InvalidTransition nil end end diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb deleted file mode 100644 index 3a17596aaeb..00000000000 --- a/app/services/ci/test_hook_service.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ci - class TestHookService - def execute(hook, current_user) - Ci::WebHookService.new.build_end(hook.project.commits.last.last_build) - end - end -end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index bfe6a3dc4be..ec581658fc1 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,7 +3,7 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::CompareResult object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch) + def execute(source_project, source_branch, target_project, target_branch, diff_options = {}) source_commit = source_project.commit(source_branch) return unless source_commit @@ -25,7 +25,7 @@ class CompareService target_project.repository.raw_repository, target_branch, source_sha, - ) + ), diff_options ) end end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index cf7ae4345f3..f139872c728 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,10 +1,10 @@ require_relative 'base_service' class CreateBranchService < BaseService - def execute(branch_name, ref) + def execute(branch_name, ref, source_project: @project) valid_branch = Gitlab::GitRefValidator.validate(branch_name) if valid_branch == false - return error('Branch name invalid') + return error('Branch name is invalid') end repository = project.repository @@ -13,8 +13,20 @@ class CreateBranchService < BaseService return error('Branch already exists') end - repository.add_branch(branch_name, ref) - new_branch = repository.find_branch(branch_name) + new_branch = nil + if source_project != @project + repository.with_tmp_ref do |tmp_ref| + repository.fetch_ref( + source_project.repository.path_to_repo, + "refs/heads/#{ref}", + tmp_ref + ) + + new_branch = repository.add_branch(current_user, branch_name, tmp_ref) + end + else + new_branch = repository.add_branch(current_user, branch_name, ref) + end if new_branch push_data = build_push_data(project, current_user, new_branch) @@ -27,6 +39,8 @@ class CreateBranchService < BaseService else error('Invalid reference name') end + rescue GitHooksService::PreReceiveError + error('Branch creation was rejected by Git hook') end def success(branch) diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb new file mode 100644 index 00000000000..31b407efeb1 --- /dev/null +++ b/app/services/create_commit_builds_service.rb @@ -0,0 +1,42 @@ +class CreateCommitBuildsService + def execute(project, user, params) + return false unless project.builds_enabled? + + sha = params[:checkout_sha] || params[:after] + origin_ref = params[:ref] + + unless origin_ref && sha.present? + return false + end + + ref = Gitlab::Git.ref_name(origin_ref) + + # Skip branch removal + if sha == Gitlab::Git::BLANK_SHA + return false + end + + commit = project.ci_commit(sha) + unless commit + commit = project.ci_commits.new(sha: sha) + + # Skip creating ci_commit when no gitlab-ci.yml is found + unless commit.ci_yaml_file + return false + end + + # Create a new ci_commit + commit.save! + end + + # Skip creating builds for commits that have [ci skip] + unless commit.skip_ci? + # Create builds for commit + tag = Gitlab::Git.tag_ref?(origin_ref) + commit.update_committed! + commit.create_builds(ref, tag, user) + end + + commit + end +end diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb new file mode 100644 index 00000000000..e06a6f2f47a --- /dev/null +++ b/app/services/create_release_service.rb @@ -0,0 +1,31 @@ +require_relative 'base_service' + +class CreateReleaseService < BaseService + def execute(tag_name, release_description) + + repository = project.repository + existing_tag = repository.find_tag(tag_name) + + # Only create a release if the tag exists + if existing_tag + release = project.releases.find_by(tag: tag_name) + + if release + error('Release already exists', 409) + else + release = project.releases.new({ tag: tag_name, description: release_description }) + release.save + + success(release) + end + else + error('Tag does not exist', 404) + end + end + + def success(release) + out = super() + out[:release] = release + out + end +end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 1a7318048b3..2452999382a 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -1,7 +1,7 @@ require_relative 'base_service' class CreateTagService < BaseService - def execute(tag_name, ref, message) + def execute(tag_name, ref, message, release_description = nil) valid_tag = Gitlab::GitRefValidator.validate(tag_name) if valid_tag == false return error('Tag name invalid') @@ -20,11 +20,15 @@ class CreateTagService < BaseService if new_tag push_data = create_push_data(project, current_user, new_tag) - EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) + if release_description + CreateReleaseService.new(@project, @current_user). + execute(tag_name, release_description) + end + success(new_tag) else error('Invalid reference name') diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index b19b112a0c4..22bf9dd935e 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -24,7 +24,7 @@ class DeleteBranchService < BaseService return error('You dont have push access to repo', 405) end - if repository.rm_branch(branch_name) + if repository.rm_branch(current_user, branch_name) push_data = build_push_data(branch) EventCreateService.new.push(project, current_user, push_data) @@ -35,6 +35,8 @@ class DeleteBranchService < BaseService else error('Failed to remove branch') end + rescue GitHooksService::PreReceiveError + error('Branch deletion was rejected by Git hook') end def error(message, return_code = 400) diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 0c836401136..de3352a6756 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -11,8 +11,10 @@ class DeleteTagService < BaseService end if repository.rm_tag(tag_name) + release = project.releases.find_by(tag: tag_name) + release.destroy if release + push_data = build_push_data(tag) - EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 008833eed80..0326a8823e9 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -3,8 +3,10 @@ module Files class ValidationError < StandardError; end def execute - @current_branch = params[:current_branch] + @source_project = params[:source_project] || @project + @source_branch = params[:source_branch] @target_branch = params[:target_branch] + @commit_message = params[:commit_message] @file_path = params[:file_path] @file_content = if params[:file_content_encoding] == 'base64' @@ -16,8 +18,8 @@ module Files # Validate parameters validate - # Create new branch if it different from current_branch - if @target_branch != @current_branch + # Create new branch if it different from source_branch + if different_branch? create_target_branch end @@ -26,18 +28,14 @@ module Files else error("Something went wrong. Your changes were not committed") end - rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex + rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex error(ex.message) end private - def current_branch - @current_branch ||= params[:current_branch] - end - - def target_branch - @target_branch ||= params[:target_branch] + def different_branch? + @source_branch != @target_branch || @source_project != @project end def raise_error(message) @@ -52,11 +50,11 @@ module Files end unless project.empty_repo? - unless repository.branch_names.include?(@current_branch) - raise_error("You can only create files if you are on top of a branch") + unless @source_project.repository.branch_names.include?(@source_branch) + raise_error("You can only create or edit files when you are on a branch") end - if @current_branch != @target_branch + if different_branch? if repository.branch_names.include?(@target_branch) raise_error("Branch with such name already exists. You need to switch to this branch in order to make changes") end @@ -65,10 +63,10 @@ module Files end def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch) + result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) unless result[:status] == :success - raise_error("Something went wrong when we tried to create #{@target_branch} for you") + raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") end end end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 71272fb5707..6107254a34e 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -5,5 +5,16 @@ module Files def commit repository.commit_dir(current_user, @file_path, @commit_message, @target_branch) end + + def validate + super + + unless @file_path =~ Gitlab::Regex.file_path_regex + raise_error( + 'Your changes could not be committed, because the file path ' + + Gitlab::Regex.file_path_regex_message + ) + end + end end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index c8e3a910bba..e4cde4a2fd8 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -9,19 +9,24 @@ module Files def validate super - file_name = File.basename(@file_path) + if @file_path =~ Gitlab::Regex.directory_traversal_regex + raise_error( + 'Your changes could not be committed, because the file name ' + + Gitlab::Regex.directory_traversal_regex_message + ) + end - unless file_name =~ Gitlab::Regex.file_name_regex + unless @file_path =~ Gitlab::Regex.file_path_regex raise_error( 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.file_name_regex_message + Gitlab::Regex.file_path_regex_message ) end unless project.empty_repo? @file_path.slice!(0) if @file_path.start_with?('/') - blob = repository.blob_at_branch(@current_branch, @file_path) + blob = repository.blob_at_branch(@source_branch, @file_path) if blob raise_error("Your changes could not be committed because a file with the same name already exists") diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb new file mode 100644 index 00000000000..8f5c3393dfc --- /dev/null +++ b/app/services/git_hooks_service.rb @@ -0,0 +1,28 @@ +class GitHooksService + PreReceiveError = Class.new(StandardError) + + def execute(user, repo_path, oldrev, newrev, ref) + @repo_path = repo_path + @user = Gitlab::ShellEnv.gl_id(user) + @oldrev = oldrev + @newrev = newrev + @ref = ref + + %w(pre-receive update).each do |hook_name| + unless run_hook(hook_name) + raise PreReceiveError.new("Git operation was rejected by #{hook_name} hook") + end + end + + yield + + run_hook('post-receive') + end + + private + + def run_hook(name) + hook = Gitlab::Git::Hook.new(name, @repo_path) + hook.trigger(@user, @oldrev, @newrev, @ref) + end +end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 3de7bb9dcaa..d7ea30bc315 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -58,15 +58,10 @@ class GitPushService @push_data = build_push_data(oldrev, newrev, ref) - # If CI was disabled but .gitlab-ci.yml file was pushed - # we enable CI automatically - if !project.gitlab_ci? && gitlab_ci_yaml?(newrev) - project.enable_ci - end - EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) + CreateCommitBuildsService.new.execute(project, @user, @push_data) ProjectCacheWorker.perform_async(project.id) end @@ -134,10 +129,4 @@ class GitPushService def commit_user(commit) commit.author || user end - - def gitlab_ci_yaml?(sha) - @project.repository.blob_at(sha, '.gitlab-ci.yml') - rescue Rugged::ReferenceError - nil - end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 1cc42b0b0ad..4144c7111d0 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -10,6 +10,7 @@ class GitTagPushService EventCreateService.new.push(project, user, @push_data) 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) ProjectCacheWorker.perform_async(project.id) true diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb index 4bee0c26a68..433ecc2df32 100644 --- a/app/services/gravatar_service.rb +++ b/app/services/gravatar_service.rb @@ -1,13 +1,13 @@ class GravatarService include Gitlab::CurrentSettings - def execute(email, size = nil) + def execute(email, size = nil, scale = 2) if current_application_settings.gravatar_enabled? && email.present? size = 40 if size.nil? || size <= 0 sprintf gravatar_url, hash: Digest::MD5.hexdigest(email.strip.downcase), - size: size, + size: size * scale, email: email.strip end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 15b3825f96a..2556f06e2d3 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -27,7 +27,16 @@ class IssuableBaseService < BaseService old_branch, new_branch) end + def create_task_status_note(issuable) + issuable.updated_tasks.each do |task| + SystemNoteService.change_task_status(issuable, issuable.project, current_user, task) + end + 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 + ability = :"admin_#{issuable_ability_name}" unless can?(current_user, ability, project) @@ -36,4 +45,44 @@ class IssuableBaseService < BaseService params.delete(:assignee_id) end end + + def update(issuable) + change_state(issuable) + filter_params + old_labels = issuable.labels.to_a + + if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) + issuable.reset_events_cache + handle_common_system_notes(issuable, old_labels: old_labels) + handle_changes(issuable) + issuable.create_new_cross_references!(current_user) + execute_hooks(issuable, 'update') + end + + issuable + end + + def change_state(issuable) + case params.delete(:state_event) + when 'reopen' + reopen_service.new(project, current_user, {}).execute(issuable) + when 'close' + close_service.new(project, current_user, {}).execute(issuable) + end + end + + def handle_common_system_notes(issuable, options = {}) + if issuable.previous_changes.include?('title') + create_title_change_note(issuable, issuable.previous_changes['title'].first) + end + + if issuable.previous_changes.include?('description') && issuable.tasks? + create_task_status_note(issuable) + end + + old_labels = options[:old_labels] + if old_labels && (issuable.labels != old_labels) + create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels) + end + end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 3d85f97b7e5..a1a20e47681 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,6 +1,11 @@ module Issues class CloseService < Issues::BaseService def execute(issue, commit = nil) + if project.jira_tracker? && project.jira_service.active + project.jira_service.execute(commit, issue) + return issue + end + if project.default_issues_tracker? && issue.close event_service.close_issue(issue, current_user) create_note(issue, commit) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 2b5426ad452..a55a04dd5e0 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -1,45 +1,26 @@ module Issues class UpdateService < Issues::BaseService def execute(issue) - case params.delete(:state_event) - when 'reopen' - Issues::ReopenService.new(project, current_user, {}).execute(issue) - when 'close' - Issues::CloseService.new(project, current_user, {}).execute(issue) - end - - params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE - params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE - - filter_params - old_labels = issue.labels.to_a - - if params.present? && issue.update_attributes(params.merge(updated_by: current_user)) - issue.reset_events_cache - - if issue.labels != old_labels - create_labels_note( - issue, issue.labels - old_labels, old_labels - issue.labels) - end - - if issue.previous_changes.include?('milestone_id') - create_milestone_note(issue) - end - - if issue.previous_changes.include?('assignee_id') - create_assignee_note(issue) - notification_service.reassigned_issue(issue, current_user) - end + update(issue) + end - if issue.previous_changes.include?('title') - create_title_change_note(issue, issue.previous_changes['title'].first) - end + def handle_changes(issue) + if issue.previous_changes.include?('milestone_id') + create_milestone_note(issue) + end - issue.create_new_cross_references! - execute_hooks(issue, 'update') + if issue.previous_changes.include?('assignee_id') + create_assignee_note(issue) + notification_service.reassigned_issue(issue, current_user) end + end + + def reopen_service + Issues::ReopenService + end - issue + def close_service + Issues::CloseService end end end diff --git a/app/services/labels/group_service.rb b/app/services/labels/group_service.rb deleted file mode 100644 index b26cee24d56..00000000000 --- a/app/services/labels/group_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Labels - class GroupService < ::BaseService - def initialize(project_labels) - @project_labels = project_labels.group_by(&:title) - end - - def execute - build(@project_labels) - end - - def label(title) - if title - group_label = @project_labels[title].group_by(&:title) - build(group_label).first - else - nil - end - end - - private - - def build(label) - label.map { |title, labels| GroupLabel.new(title, labels) } - end - end -end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 7963af127e1..cabc3d8fabb 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,15 +6,12 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService - attr_reader :merge_request, :commit_message + attr_reader :merge_request - def execute(merge_request, commit_message) - @commit_message = commit_message + def execute(merge_request) @merge_request = merge_request - unless @merge_request.mergeable? - return error('Merge request is not mergeable') - end + return error('Merge request is not mergeable') unless @merge_request.mergeable? merge_request.in_locked_state do if commit @@ -32,13 +29,13 @@ module MergeRequests committer = repository.user_to_committer(current_user) options = { - message: commit_message, + message: params[:commit_message] || merge_request.merge_commit_message, author: committer, committer: committer } repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) - rescue Exception => e + rescue StandardError => e merge_request.update(merge_error: "Something went wrong during merge") Rails.logger.error(e.message) return false @@ -46,6 +43,11 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) + + if params[:should_remove_source_branch] + DeleteBranchService.new(@merge_request.source_project, current_user). + execute(merge_request.source_branch) + end end end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb new file mode 100644 index 00000000000..5cf7404a493 --- /dev/null +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -0,0 +1,55 @@ +module MergeRequests + class MergeWhenBuildSucceedsService < MergeRequests::BaseService + # Marks the passed `merge_request` to be merged when the build succeeds or + # updates the params for the automatic merge + def execute(merge_request) + merge_request.merge_params.merge!(params) + + # The service is also called when the merge params are updated. + already_approved = merge_request.merge_when_build_succeeds? + + unless already_approved + merge_request.merge_when_build_succeeds = true + merge_request.merge_user = @current_user + + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit) + end + + merge_request.save + end + + # Triggers the automatic merge of merge_request once the build succeeds + def trigger(build) + merge_requests = merge_request_from(build) + + merge_requests.each do |merge_request| + next unless merge_request.merge_when_build_succeeds? + + if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable? + MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) + end + end + end + + # Cancels the automatic merge + def cancel(merge_request) + if merge_request.merge_when_build_succeeds? && merge_request.open? + merge_request.reset_merge_when_build_succeeds + SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + + success + else + error("Can't cancel the automatic merge", 406) + end + end + + private + + def merge_request_from(build) + merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a + + merge_requests.uniq.select(&:source_project) + end + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 121f6899011..8b3d56c2b4c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -5,20 +5,20 @@ module MergeRequests @oldrev, @newrev = oldrev, newrev @branch_name = Gitlab::Git.ref_name(ref) - @fork_merge_requests = @project.fork_merge_requests.opened - @commits = [] - # Leave a system note if a branch were deleted/added - if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) + find_new_commits + # Be sure to close outstanding MRs before reloading them to avoid generating an + # empty diff during a manual merge + close_merge_requests + reload_merge_requests + reset_merge_when_build_succeeds + + # Leave a system note if a branch was deleted/added + if branch_added? || branch_removed? comment_mr_branch_presence_changed - comment_mr_with_commits if @commits.present? - else - @commits = @project.repository.commits_between(oldrev, newrev) - comment_mr_with_commits - close_merge_requests end - reload_merge_requests + comment_mr_with_commits execute_mr_web_hooks true @@ -54,11 +54,10 @@ module MergeRequests # Note: we should update merge requests from forks too def reload_merge_requests merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a - merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a + merge_requests += fork_merge_requests.by_branch(@branch_name).to_a merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| - if merge_request.source_branch == @branch_name || force_push? merge_request.reload_code merge_request.mark_as_unchecked @@ -77,37 +76,51 @@ module MergeRequests end end - # Add comment about branches being deleted or added to merge requests - def comment_mr_branch_presence_changed - presence = Gitlab::Git.blank_ref?(@oldrev) ? :add : :delete + def reset_merge_when_build_succeeds + merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) + end - merge_requests_for_source_branch.each do |merge_request| - last_commit = merge_request.last_commit + def find_new_commits + if branch_added? + @commits = [] - # Only look at changed commits in restore branch case - unless Gitlab::Git.blank_ref?(@newrev) - begin - # Since any number of commits could have been made to the restored branch, - # find the common root to see what has been added. - common_ref = @project.repository.merge_base(last_commit.id, @newrev) - # If the a commit no longer exists in this repo, gitlab_git throws - # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 - @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref - rescue - end + merge_request = merge_requests_for_source_branch.first + return unless merge_request - # Prevent system notes from seeing a blank SHA - @oldrev = nil + last_commit = merge_request.last_commit + + begin + # Since any number of commits could have been made to the restored branch, + # find the common root to see what has been added. + common_ref = @project.repository.merge_base(last_commit.id, @newrev) + # If the a commit no longer exists in this repo, gitlab_git throws + # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 + @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref + rescue end + elsif branch_removed? + # No commits for a deleted branch. + @commits = [] + else + @commits = @project.repository.commits_between(@oldrev, @newrev) + end + end + # Add comment about branches being deleted or added to merge requests + def comment_mr_branch_presence_changed + presence = branch_added? ? :add : :delete + + merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( - merge_request, merge_request.project, @current_user, + merge_request, merge_request.project, @current_user, :source, @branch_name, presence) end end # Add comment about pushing new commits to merge requests def comment_mr_with_commits + return unless @commits.present? + merge_requests_for_source_branch.each do |merge_request| mr_commit_ids = Set.new(merge_request.commits.map(&:id)) @@ -135,9 +148,21 @@ module MergeRequests def merge_requests_for_source_branch @source_merge_requests ||= begin merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a + merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a filter_merge_requests(merge_requests) end end + + def fork_merge_requests + @fork_merge_requests ||= @project.fork_merge_requests.opened + end + + def branch_added? + Gitlab::Git.blank_ref?(@oldrev) + end + + def branch_removed? + Gitlab::Git.blank_ref?(@newrev) + end end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index ebbe0af803b..5ff2cc03dda 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,59 +11,37 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) - case params.delete(:state_event) - when 'reopen' - MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request) - when 'close' - MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request) - end - - params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE - params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE - - filter_params - old_labels = merge_request.labels.to_a - - if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user)) - merge_request.reset_events_cache - - if merge_request.labels != old_labels - create_labels_note( - merge_request, - merge_request.labels - old_labels, - old_labels - merge_request.labels - ) - end - - if merge_request.previous_changes.include?('target_branch') - create_branch_change_note(merge_request, 'target', - merge_request.previous_changes['target_branch'].first, - merge_request.target_branch) - end - - if merge_request.previous_changes.include?('milestone_id') - create_milestone_note(merge_request) - end + update(merge_request) + end - if merge_request.previous_changes.include?('assignee_id') - create_assignee_note(merge_request) - notification_service.reassigned_merge_request(merge_request, current_user) - end + def handle_changes(merge_request) + if merge_request.previous_changes.include?('target_branch') + create_branch_change_note(merge_request, 'target', + merge_request.previous_changes['target_branch'].first, + merge_request.target_branch) + end - if merge_request.previous_changes.include?('title') - create_title_change_note(merge_request, merge_request.previous_changes['title'].first) - end + if merge_request.previous_changes.include?('milestone_id') + create_milestone_note(merge_request) + end - if merge_request.previous_changes.include?('target_branch') || - merge_request.previous_changes.include?('source_branch') - merge_request.mark_as_unchecked - end + if merge_request.previous_changes.include?('assignee_id') + create_assignee_note(merge_request) + notification_service.reassigned_merge_request(merge_request, current_user) + end - merge_request.create_new_cross_references! - execute_hooks(merge_request, 'update') + if merge_request.previous_changes.include?('target_branch') || + merge_request.previous_changes.include?('source_branch') + merge_request.mark_as_unchecked end + end + + def reopen_service + MergeRequests::ReopenService + end - merge_request + def close_service + MergeRequests::CloseService end end end diff --git a/app/services/milestones/group_service.rb b/app/services/milestones/group_service.rb deleted file mode 100644 index 11d702f1e7b..00000000000 --- a/app/services/milestones/group_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Milestones - class GroupService < Milestones::BaseService - def initialize(project_milestones) - @project_milestones = project_milestones.group_by(&:title) - end - - def execute - build(@project_milestones) - end - - def milestone(title) - if title - group_milestone = @project_milestones[title].group_by(&:title) - build(group_milestone).first - else - nil - end - end - - private - - def build(milestone) - milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) } - end - end -end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2001dc89c33..a8486e6a5a1 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -8,8 +8,8 @@ module Notes if note.save notification_service.new_note(note) - # Skip system notes, like status changes and cross-references. - unless note.system + # Skip system notes, like status changes and cross-references and awards + unless note.system || note.is_award event_service.leave_note(note, note.author) note.create_cross_references! execute_hooks(note) diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 6c2f08e5963..72e2f78008d 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -4,7 +4,7 @@ module Notes return note unless note.editable? note.update_attributes(params.merge(updated_by: current_user)) - note.create_new_cross_references! + note.create_new_cross_references!(current_user) note.reset_events_cache note diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index a6b22348650..e4edc55bf69 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -13,14 +13,14 @@ class NotificationService # even if user disabled notifications def new_key(key) if key.user - mailer.new_ssh_key_email(key.id) + mailer.new_ssh_key_email(key.id).deliver_later end end # Always notify user about email added to profile def new_email(email) if email.user - mailer.new_email_email(email.id) + mailer.new_email_email(email.id).deliver_later end end @@ -79,17 +79,27 @@ class NotificationService end def merge_mr(merge_request, current_user) - close_resource_email(merge_request, merge_request.target_project, current_user, 'merged_merge_request_email') + close_resource_email( + merge_request, + merge_request.target_project, + current_user, + 'merged_merge_request_email' + ) end def reopen_mr(merge_request, current_user) - reopen_resource_email(merge_request, merge_request.target_project, current_user, 'merge_request_status_email', 'reopened') + reopen_resource_email( + merge_request, + merge_request.target_project, + current_user, 'merge_request_status_email', + 'reopened' + ) end # Notify new user with email after creation def new_user(user, token = nil) # Don't email omniauth created users - mailer.new_user_email(user.id, token) unless user.identities.any? + mailer.new_user_email(user.id, token).deliver_later unless user.identities.any? end # Notify users on new note in system @@ -102,6 +112,7 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true + return true if note.is_award target = note.noteable @@ -113,7 +124,7 @@ class NotificationService end # Add all users participating in the thread (author, assignee, comment authors) - participants = + participants = if target.respond_to?(:participants) target.participants(note.author) else @@ -134,53 +145,63 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, note.noteable) recipients.delete(note.author) + recipients = recipients.uniq # build notify method like 'note_commit_email' notify_method = "note_#{note.noteable_type.underscore}_email".to_sym - recipients.each do |recipient| - mailer.send(notify_method, recipient.id, note.id) + mailer.send(notify_method, recipient.id, note.id).deliver_later end end def invite_project_member(project_member, token) - mailer.project_member_invited_email(project_member.id, token) + mailer.project_member_invited_email(project_member.id, token).deliver_later end def accept_project_invite(project_member) - mailer.project_invite_accepted_email(project_member.id) + mailer.project_invite_accepted_email(project_member.id).deliver_later end def decline_project_invite(project_member) - mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id) + mailer.project_invite_declined_email( + project_member.project.id, + project_member.invite_email, + project_member.access_level, + project_member.created_by_id + ).deliver_later end def new_project_member(project_member) - mailer.project_access_granted_email(project_member.id) + mailer.project_access_granted_email(project_member.id).deliver_later end def update_project_member(project_member) - mailer.project_access_granted_email(project_member.id) + mailer.project_access_granted_email(project_member.id).deliver_later end def invite_group_member(group_member, token) - mailer.group_member_invited_email(group_member.id, token) + mailer.group_member_invited_email(group_member.id, token).deliver_later end def accept_group_invite(group_member) - mailer.group_invite_accepted_email(group_member.id) + mailer.group_invite_accepted_email(group_member.id).deliver_later end def decline_group_invite(group_member) - mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id) + mailer.group_invite_declined_email( + group_member.group.id, + group_member.invite_email, + group_member.access_level, + group_member.created_by_id + ).deliver_later end def new_group_member(group_member) - mailer.group_access_granted_email(group_member.id) + mailer.group_access_granted_email(group_member.id).deliver_later end def update_group_member(group_member) - mailer.group_access_granted_email(group_member.id) + mailer.group_access_granted_email(group_member.id).deliver_later end def project_was_moved(project, old_path_with_namespace) @@ -188,7 +209,11 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients.each do |recipient| - mailer.project_was_moved_email(project.id, recipient.id, old_path_with_namespace) + mailer.project_was_moved_email( + project.id, + recipient.id, + old_path_with_namespace + ).deliver_later end end @@ -276,35 +301,25 @@ class NotificationService # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) - users = users.to_a.compact.uniq - users = users.reject(&:blocked?) - - users.reject do |user| - next user.notification.disabled? unless project - - member = project.project_members.find_by(user_id: user.id) - - if !member && project.group - member = project.group.group_members.find_by(user_id: user.id) - end - - # reject users who globally disabled notification and has no membership - next user.notification.disabled? unless member - - # reject users who disabled notification in project - next true if member.notification.disabled? - - # reject users who have N_GLOBAL in project and disabled in global settings - member.notification.global? && user.notification.disabled? - end + reject_users(users, :disabled?, project) end # Remove users with notification level 'Mentioned' def reject_mention_users(users, project = nil) + reject_users(users, :mention?, project) + end + + # Reject users which method_name from notification object returns true. + # + # Example: + # reject_users(users, :watch?, project) + # + def reject_users(users, method_name, project = nil) users = users.to_a.compact.uniq + users = users.reject(&:blocked?) users.reject do |user| - next user.notification.mention? unless project + next user.notification.send(method_name) unless project member = project.project_members.find_by(user_id: user.id) @@ -313,19 +328,19 @@ class NotificationService end # reject users who globally set mention notification and has no membership - next user.notification.mention? unless member + next user.notification.send(method_name) unless member # reject users who set mention notification in project - next true if member.notification.mention? + next true if member.notification.send(method_name) # reject users who have N_MENTION in project and disabled in global settings - member.notification.global? && user.notification.mention? + member.notification.global? && user.notification.send(method_name) end end def reject_unsubscribed_users(recipients, target) return recipients unless target.respond_to? :subscriptions - + recipients.reject do |user| subscription = target.subscriptions.find_by_user_id(user.id) subscription && !subscription.subscribed @@ -343,12 +358,12 @@ class NotificationService recipients end end - + def new_resource_email(target, project, method) recipients = build_recipients(target, project, target.author) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id) + mailer.send(method, recipient.id, target.id).deliver_later end end @@ -356,16 +371,24 @@ class NotificationService recipients = build_recipients(target, project, current_user) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, current_user.id) + mailer.send(method, recipient.id, target.id, current_user.id).deliver_later end end def reassign_resource_email(target, project, current_user, method) - assignee_id_was = previous_record(target, "assignee_id") - recipients = build_recipients(target, project, current_user) + previous_assignee_id = previous_record(target, "assignee_id") + previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + + recipients = build_recipients(target, project, current_user, [previous_assignee]) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id) + mailer.send( + method, + recipient.id, + target.id, + previous_assignee_id, + current_user.id + ).deliver_later end end @@ -373,13 +396,15 @@ class NotificationService recipients = build_recipients(target, project, current_user) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, status, current_user.id) + mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later end end - def build_recipients(target, project, current_user) + def build_recipients(target, project, current_user, extra_recipients = nil) recipients = target.participants(current_user) + recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients + recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) recipients = reject_muted_users(recipients, project) @@ -388,12 +413,13 @@ class NotificationService recipients = reject_unsubscribed_users(recipients, target) recipients.delete(current_user) + recipients = recipients.uniq recipients end def mailer - Notify.delay + Notify end def previous_record(object, attribute) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index faf1ee008e7..a6820183bee 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -55,15 +55,19 @@ module Projects @project.save if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository + unless @project.create_repository + raise 'Failed to create repository' + end end end after_create_actions if @project.persisted? @project - rescue - @project.errors.add(:base, "Can't save project. Please try again later") + rescue => e + message = "Unable to save project: #{e.message}" + Rails.logger.error(message) + @project.errors.add(:base, message) if @project @project end @@ -94,11 +98,7 @@ module Projects @project.team << [current_user, :master, current_user] end - @project.update_column(:last_activity_at, @project.created_at) - - if @project.import? - @project.import_start - end + @project.import_start if @project.import? end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 46374a3909a..0577ae778d5 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -7,6 +7,8 @@ module Projects description: @project.description, name: @project.name, path: @project.path, + shared_runners_enabled: @project.shared_runners_enabled, + builds_enabled: @project.builds_enabled, namespace_id: @params[:namespace].try(:id) || current_user.namespace.id } @@ -15,19 +17,6 @@ module Projects end new_project = CreateService.new(current_user, new_params).execute - - if new_project.persisted? - if @project.gitlab_ci? - new_project.enable_ci - - settings = @project.gitlab_ci_project.attributes.select do |attr_name, value| - ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name - end - - new_project.gitlab_ci_project.update(settings) - end - end - new_project end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 64ea6dd42eb..2e734654466 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -55,6 +55,9 @@ module Projects # Move uploads 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 end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 69bdd045ddf..895e089bea3 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -3,12 +3,16 @@ module Projects def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] - if new_visibility && new_visibility.to_i != project.visibility_level - unless can?(current_user, :change_visibility_level, project) && - Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) - deny_visibility_level(project, new_visibility) - return project + if new_visibility + if new_visibility.to_i != project.visibility_level + unless can?(current_user, :change_visibility_level, project) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(project, new_visibility) + return project + end end + + return false unless visibility_level_allowed?(new_visibility) end new_branch = params[:default_branch] @@ -23,5 +27,19 @@ module Projects end end end + + private + + def visibility_level_allowed?(level) + return true if project.visibility_level_allowed?(level) + + level_name = Gitlab::VisibilityLevel.level_name(level) + project.errors.add( + :visibility_level, + "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive" + ) + + false + end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 9a5fe4af9dd..6dc854ec33d 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -18,7 +18,8 @@ class SystemHooksService def build_event_data(model, event) data = { event_name: build_event_name(model, event), - created_at: model.created_at.xmlschema + created_at: model.created_at.xmlschema, + updated_at: model.updated_at.xmlschema } case model @@ -33,17 +34,15 @@ class SystemHooksService ) end when Project - owner = model.owner + data.merge!(project_data(model)) - data.merge!({ - name: model.name, - path: model.path, - path_with_namespace: model.path_with_namespace, - project_id: model.id, - owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", - project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase - }) + if event == :rename || event == :transfer + data.merge!({ + old_path_with_namespace: model.old_path_with_namespace + }) + end + + data when User data.merge!({ name: model.name, @@ -51,16 +50,7 @@ class SystemHooksService user_id: model.id }) when ProjectMember - data.merge!({ - project_name: model.project.name, - project_path: model.project.path, - project_path_with_namespace: model.project.path_with_namespace, - project_id: model.project.id, - user_name: model.user.name, - user_email: model.user.email, - access_level: model.human_access, - project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase - }) + data.merge!(project_member_data(model)) when Group owner = model.owner @@ -72,15 +62,7 @@ class SystemHooksService owner_email: owner.respond_to?(:email) ? owner.email : nil, ) when GroupMember - data.merge!( - group_name: model.group.name, - group_path: model.group.path, - group_id: model.group.id, - user_name: model.user.name, - user_email: model.user.email, - user_id: model.user.id, - group_access: model.human_access, - ) + data.merge!(group_member_data(model)) end end @@ -96,4 +78,43 @@ class SystemHooksService "#{model.class.name.downcase}_#{event.to_s}" end end + + def project_data(model) + owner = model.owner + + { + name: model.name, + path: model.path, + path_with_namespace: model.path_with_namespace, + project_id: model.id, + owner_name: owner.name, + owner_email: owner.respond_to?(:email) ? owner.email : "", + project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase + } + end + + def project_member_data(model) + { + project_name: model.project.name, + project_path: model.project.path, + project_path_with_namespace: model.project.path_with_namespace, + project_id: model.project.id, + user_name: model.user.name, + user_email: model.user.email, + access_level: model.human_access, + project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase + } + end + + def group_member_data(model) + { + group_name: model.group.name, + group_path: model.group.path, + group_id: model.group.id, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + group_access: model.human_access, + } + end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 37f454cfc3f..98a71cbf1ad 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -125,7 +125,21 @@ class SystemNoteService # Returns the created Note object def self.change_status(noteable, project, author, status, source) body = "Status changed to #{status}" - body += " by #{source.gfm_reference}" if source + body += " by #{source.gfm_reference(project)}" if source + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when 'merge when build succeeds' is executed + def self.merge_when_build_succeeds(noteable, project, author, last_commit) + body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when 'merge when build succeeds' is canceled + def self.cancel_merge_when_build_succeeds(noteable, project, author) + body = "Canceled the automatic merge" create_note(noteable: noteable, project: project, author: author, note: body) end @@ -227,9 +241,14 @@ class SystemNoteService note_options.merge!(noteable: noteable) end - create_note(note_options) + if noteable.is_a?(ExternalIssue) + noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author) + else + create_note(note_options) + end end + def self.cross_reference?(note_text) note_text.start_with?(cross_reference_note_prefix) end @@ -245,7 +264,7 @@ class SystemNoteService # # Returns Boolean def self.cross_reference_disallowed?(noteable, mentioner) - return true if noteable.is_a?(ExternalIssue) + return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active? return false unless mentioner.is_a?(MergeRequest) return false unless noteable.is_a?(Commit) @@ -327,7 +346,7 @@ class SystemNoteService commit_ids = if count == 1 existing_commits.first.short_id else - if oldrev + if oldrev && !Gitlab::Git.blank_ref?(oldrev) "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" else "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" @@ -341,4 +360,22 @@ class SystemNoteService "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" end + + # Called when the status of a Task has changed + # + # noteable - Noteable object. + # project - Project owning noteable + # author - User performing the change + # new_task - TaskList::Item object. + # + # Example Note text: + # + # "Soandso marked the task Whatever as completed." + # + # Returns the created Note object + def self.change_task_status(noteable, project, author, new_task) + status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE + body = "Marked the task **#{new_task.source}** as #{status_label}" + create_note(noteable: noteable, project: project, author: author, note: body) + end end diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb new file mode 100644 index 00000000000..25eb13ef09a --- /dev/null +++ b/app/services/update_release_service.rb @@ -0,0 +1,29 @@ +require_relative 'base_service' + +class UpdateReleaseService < BaseService + def execute(tag_name, release_description) + + repository = project.repository + existing_tag = repository.find_tag(tag_name) + + if existing_tag + release = project.releases.find_by(tag: tag_name) + + if release + release.update_attributes(description: release_description) + + success(release) + else + error('Release does not exist', 404) + end + else + error('Tag does not exist', 404) + end + end + + def success(release) + out = super() + out[:release] = release + out + end +end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb new file mode 100644 index 00000000000..1b0ae6c0056 --- /dev/null +++ b/app/uploaders/artifact_uploader.rb @@ -0,0 +1,46 @@ +# encoding: utf-8 +class ArtifactUploader < CarrierWave::Uploader::Base + storage :file + + attr_accessor :build, :field + + def self.artifacts_path + Gitlab.config.artifacts.path + end + + def self.artifacts_upload_path + File.join(self.artifacts_path, 'tmp/uploads/') + end + + def self.artifacts_cache_path + File.join(self.artifacts_path, 'tmp/cache/') + end + + def initialize(build, field) + @build, @field = build, field + end + + def store_dir + File.join(self.class.artifacts_path, @build.artifacts_path) + end + + def cache_dir + File.join(self.class.artifacts_cache_path, @build.artifacts_path) + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + + def exists? + file.try(:exists?) + end + + def move_to_cache + true + end + + def move_to_store + true + end +end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index a9691bee46e..a65a896e41e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,26 +1,11 @@ # encoding: utf-8 class AttachmentUploader < CarrierWave::Uploader::Base + include UploaderHelper + storage :file def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end - - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 7cad044555b..6135c3ad96f 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,6 +1,8 @@ # encoding: utf-8 class AvatarUploader < CarrierWave::Uploader::Base + include UploaderHelper + storage :file after :store, :reset_events_cache @@ -9,23 +11,6 @@ class AvatarUploader < CarrierWave::Uploader::Base "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - def reset_events_cache(file) model.reset_events_cache if model.is_a?(User) end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index e8211585834..ac920119a85 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,5 +1,7 @@ # encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base + include UploaderHelper + storage :file attr_accessor :project, :secret @@ -28,21 +30,4 @@ class FileUploader < CarrierWave::Uploader::Base def secure_url File.join("/uploads", @secret, file.filename) end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb new file mode 100644 index 00000000000..28085b31083 --- /dev/null +++ b/app/uploaders/lfs_object_uploader.rb @@ -0,0 +1,29 @@ +# encoding: utf-8 + +class LfsObjectUploader < CarrierWave::Uploader::Base + storage :file + + def store_dir + "#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}" + end + + def cache_dir + "#{Gitlab.config.lfs.storage_path}/tmp/cache" + end + + def move_to_cache + true + end + + def move_to_store + true + end + + def exists? + file.try(:exists?) + end + + def filename + model.oid[4..-1] + end +end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb new file mode 100644 index 00000000000..5ef440f3367 --- /dev/null +++ b/app/uploaders/uploader_helper.rb @@ -0,0 +1,19 @@ +# Extra methods for uploader +module UploaderHelper + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end +end diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb new file mode 100644 index 00000000000..571d0007aa2 --- /dev/null +++ b/app/validators/color_validator.rb @@ -0,0 +1,20 @@ +# ColorValidator +# +# Custom validator for web color codes. It requires the leading hash symbol and +# will accept RGB triplet or hexadecimal formats. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :background_color, allow_blank: true, color: true +# end +# +class ColorValidator < ActiveModel::EachValidator + PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze + + def validate_each(record, attribute, value) + unless value =~ PATTERN + record.errors.add(attribute, "must be a valid color code") + end + end +end diff --git a/app/validators/email_validator.rb b/app/validators/email_validator.rb new file mode 100644 index 00000000000..b35af100803 --- /dev/null +++ b/app/validators/email_validator.rb @@ -0,0 +1,18 @@ +# EmailValidator +# +# Based on https://github.com/balexand/email_validator +# +# Extended to use only strict mode with following allowed characters: +# ' - apostrophe +# +# See http://www.remote.org/jochen/mail/info/chars.html +# +class EmailValidator < ActiveModel::EachValidator + PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze + + def validate_each(record, attribute, value) + unless value =~ PATTERN + record.errors.add(attribute, options[:message] || :invalid) + end + end +end diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb new file mode 100644 index 00000000000..ed29e5aeb67 --- /dev/null +++ b/app/validators/line_code_validator.rb @@ -0,0 +1,12 @@ +# LineCodeValidator +# +# Custom validator for GitLab line codes. +class LineCodeValidator < ActiveModel::EachValidator + PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze + + def validate_each(record, attribute, value) + unless value =~ PATTERN + record.errors.add(attribute, "must be a valid line code") + end + end +end diff --git a/app/validators/namespace_name_validator.rb b/app/validators/namespace_name_validator.rb new file mode 100644 index 00000000000..2e51af2982d --- /dev/null +++ b/app/validators/namespace_name_validator.rb @@ -0,0 +1,10 @@ +# NamespaceNameValidator +# +# Custom validator for GitLab namespace name strings. +class NamespaceNameValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value =~ Gitlab::Regex.namespace_name_regex + record.errors.add(attribute, Gitlab::Regex.namespace_name_regex_message) + end + end +end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb new file mode 100644 index 00000000000..10e35ce665a --- /dev/null +++ b/app/validators/namespace_validator.rb @@ -0,0 +1,50 @@ +# NamespaceValidator +# +# Custom validator for GitLab namespace values. +# +# Values are checked for formatting and exclusion from a list of reserved path +# names. +class NamespaceValidator < ActiveModel::EachValidator + RESERVED = %w( + admin + all + assets + ci + dashboard + files + groups + help + hooks + issues + merge_requests + notes + profile + projects + public + repository + s + search + services + snippets + teams + u + unsubscribes + users + ).freeze + + def validate_each(record, attribute, value) + unless value =~ Gitlab::Regex.namespace_regex + record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) + end + + if reserved?(value) + record.errors.add(attribute, "#{value} is a reserved name") + end + end + + private + + def reserved?(value) + RESERVED.include?(value) + end +end diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb new file mode 100644 index 00000000000..2848b9cd33d --- /dev/null +++ b/app/validators/url_validator.rb @@ -0,0 +1,36 @@ +# UrlValidator +# +# Custom validator for URLs. +# +# By default, only URLs for the HTTP(S) protocols will be considered valid. +# Provide a `:protocols` option to configure accepted protocols. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :personal_url, url: true +# +# validates :ftp_url, url: { protocols: %w(ftp) } +# +# validates :git_url, url: { protocols: %w(http https ssh git) } +# end +# +class UrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless valid_url?(value) + record.errors.add(attribute, "must be a valid URL") + end + end + + private + + def default_options + @default_options ||= { protocols: %w(http https) } + end + + def valid_url?(value) + options = default_options.merge(self.options) + + value =~ /\A#{URI.regexp(options[:protocols])}\z/ + end +end diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml index 619533e09a7..2741eb44357 100644 --- a/app/views/abuse_report_mailer/notify.html.haml +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -8,4 +8,4 @@ = @abuse_report.message %p - = link_to "View details", abuse_reports_url + = link_to "View details", admin_abuse_reports_url diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index cffd7684008..3e5cdd2ce4a 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -2,7 +2,7 @@ %h3.page-title Report abuse %p Please use this form to report users who create spam issues, comments or behave inappropriately. %hr -= form_for @abuse_report, html: { class: 'form-horizontal'} do |f| += form_for @abuse_report, html: { class: 'form-horizontal js-requires-input'} do |f| = f.hidden_field :user_id - if @abuse_report.errors.any? .alert.alert-danger @@ -16,7 +16,7 @@ .form-group = f.label :message, class: 'control-label' .col-sm-10 - = f.text_area :message, class: "form-control", rows: 2, required: true + = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true .help-block Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index d3afc658cd6..cf50a376e11 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -2,19 +2,21 @@ - user = abuse_report.user %tr %td - - if reporter - = link_to reporter.name, reporter + - if user + = link_to user.name, [:admin, user] + .light.small + Joined #{time_ago_with_tooltip(user.created_at)} - else (removed) %td - = abuse_report.created_at.to_s(:short) - %td - = abuse_report.message - %td - - if user - = link_to user.name, user + - if reporter + = link_to reporter.name, [:admin, reporter] - else (removed) + .light.small + = time_ago_with_tooltip(abuse_report.created_at) + %td + = markdown(abuse_report.message.squish!, pipeline: :single_line) %td - if user = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 40a5fe4628b..bc4a9cedb2c 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -6,10 +6,9 @@ %table.table %thead %tr + %th User %th Reported by - %th Reported at %th Message - %th User %th Primary action %th = render @abuse_reports diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 7a78526e09a..89b38a0dad0 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -14,11 +14,11 @@ .form-group.project-visibility-level-holder = f.label :default_project_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project') + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project) .form-group.project-visibility-level-holder = f.label :default_snippet_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet') + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet) .form-group = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' .col-sm-10 @@ -105,6 +105,18 @@ = f.check_box :signin_enabled Sign-in enabled .form-group + = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2' + .col-sm-10 + .checkbox + = f.label :require_two_factor_authentication do + = f.check_box :require_two_factor_authentication + Require all users to setup Two-Factor authentication + .form-group + = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' + .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication + .form-group = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2' .col-sm-10 = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control' @@ -130,5 +142,96 @@ = f.text_area :help_page_text, class: 'form-control', rows: 4 .help-block Markdown enabled + %fieldset + %legend Continuous Integration + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :shared_runners_enabled do + = f.check_box :shared_runners_enabled + Enable shared runners for new projects + + .form-group + = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :max_artifacts_size, class: 'form-control' + + %fieldset + %legend Metrics + %p + These settings require a restart to take effect. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :metrics_enabled do + = f.check_box :metrics_enabled + Enable InfluxDB Metrics + .form-group + = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' + .form-group + = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' + .help-block + The UDP port to use for connecting to InfluxDB. InfluxDB requires that + your server configuration specifies a database to store data in when + sending messages to this port, without it metrics data will not be + saved. + .form-group + = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_username, class: 'form-control' + .form-group + = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :metrics_password, class: 'form-control' + .form-group + = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_pool_size, class: 'form-control' + .help-block + The amount of InfluxDB connections to open. Connections are opened + lazily. Users using multi-threaded application servers should ensure + enough connections are available (at minimum the amount of application + server threads). + .form-group + = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_timeout, class: 'form-control' + .help-block + The amount of seconds after which an InfluxDB connection will time + out. + .form-group + = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_method_call_threshold, class: 'form-control' + .help-block + A method call is only tracked when it takes longer to complete than + the given amount of milliseconds. + + %fieldset + %legend Spam and Anti-bot Protection + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :recaptcha_enabled do + = f.check_box :recaptcha_enabled + Enable reCAPTCHA + %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts + + .form-group + = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :recaptcha_site_key, class: 'form-control' + .help-block + Generate site and private keys here: + %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + .form-group + = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :recaptcha_private_key, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml new file mode 100644 index 00000000000..6936e614346 --- /dev/null +++ b/app/views/admin/builds/_build.html.haml @@ -0,0 +1,73 @@ +- project = build.project +%tr.build + %td.status + = ci_status_with_icon(build.status) + + %td.build-link + - if build.target_url + = link_to build.target_url do + %strong Build ##{build.id} + - else + %strong Build ##{build.id} + + - if build.show_warning? + %i.fa.fa-warning.text-warning + + %td + - if project + = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace" + + %td + = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace" + + %td + - if build.ref + = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref) + - else + .light none + + %td + - if build.try(:runner) + = runner_link(build.runner) + - else + .light none + + %td + #{build.stage} / #{build.name} + + .pull-right + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_with_tooltip(build.finished_at)} + + - if defined?(coverage) && coverage + %td.coverage + - if build.try(:coverage) + #{build.coverage}% + + %td + .pull-right + - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url + = link_to build.download_url, title: 'Download artifacts' do + %i.fa.fa-download + - if current_user && can?(current_user, :manage_builds, build.project) + - if build.active? + - if build.cancel_url + = link_to build.cancel_url, method: :post, title: 'Cancel' do + %i.fa.fa-remove.cred + - elsif defined?(allow_retry) && allow_retry && build.retry_url + = link_to build.retry_url, method: :post, title: 'Retry' do + %i.fa.fa-repeat diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml new file mode 100644 index 00000000000..ddd4e1481eb --- /dev/null +++ b/app/views/admin/builds/index.html.haml @@ -0,0 +1,50 @@ +.project-issuable-filter + .controls + .pull-left.hidden-xs + - if @all_builds.running_or_pending.any? + = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + + %ul.center-top-menu + %li{class: ('active' if @scope.nil?)} + = link_to admin_builds_path do + All + %span.badge.js-totalbuilds-count= @all_builds.count(:id) + + %li{class: ('active' if @scope == 'running')} + = link_to admin_builds_path(scope: :running) do + Running + %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id)) + + %li{class: ('active' if @scope == 'finished')} + = link_to admin_builds_path(scope: :finished) do + Finished + %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id)) + +.gray-content-block + #{(@scope || 'running').capitalize} builds + +%ul.content-list + - if @builds.blank? + %li + .nothing-here-block No builds to show + - else + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Project + %th Commit + %th Ref + %th Runner + %th Name + %th Duration + %th Finished at + %th + + - @builds.each do |build| + = render "admin/builds/build", build: build + + = paginate @builds, theme: 'gitlab' + diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 8657d2c71fe..cc389c3ae08 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -6,35 +6,35 @@ %p Forks %span.light.pull-right - = ForkedProjectLink.count + = number_with_delimiter(ForkedProjectLink.count) %p Issues %span.light.pull-right - = Issue.count + = number_with_delimiter(Issue.count) %p Merge Requests %span.light.pull-right - = MergeRequest.count + = number_with_delimiter(MergeRequest.count) %p Notes %span.light.pull-right - = Note.count + = number_with_delimiter(Note.count) %p Snippets %span.light.pull-right - = Snippet.count + = number_with_delimiter(Snippet.count) %p SSH Keys %span.light.pull-right - = Key.count + = number_with_delimiter(Key.count) %p Milestones %span.light.pull-right - = Milestone.count + = number_with_delimiter(Milestone.count) %p Active Users %span.light.pull-right - = User.active.count + = number_with_delimiter(User.active.count) .col-md-4 %h4 Features @@ -80,6 +80,10 @@ %span.pull-right = API::API::version %p + Git + %span.pull-right + = Gitlab::Git.version + %p Ruby %span.pull-right #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} @@ -95,7 +99,7 @@ %h4 Projects .data = link_to admin_namespaces_projects_path do - %h1= Project.count + %h1= number_with_delimiter(Project.count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 @@ -103,7 +107,7 @@ %h4 Users .data = link_to admin_users_path do - %h1= User.count + %h1= number_with_delimiter(User.count) %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 @@ -111,7 +115,7 @@ %h4 Groups .data = link_to admin_groups_path do - %h1= Group.count + %h1= number_with_delimiter(Group.count) %hr = link_to 'New Group', new_admin_group_path, class: "btn btn-new" diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 5ce7cdf2f8d..3940210e19b 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,6 +1,6 @@ - page_title "Groups" %h3.page-title - Groups (#{@groups.total_count}) + Groups (#{number_with_delimiter(@groups.total_count)}) = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" %p.light diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index 8358a14445b..741d111fb7d 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -1,6 +1,7 @@ - page_title "Identities", @user.name, "Users" = render 'admin/users/head' += link_to 'New Identity', new_admin_user_identity_path, class: 'pull-right btn btn-new' - if @identities.present? .table-holder %table.table diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml new file mode 100644 index 00000000000..e30bf0ef0ee --- /dev/null +++ b/app/views/admin/identities/new.html.haml @@ -0,0 +1,4 @@ +- page_title "New Identity" +%h3.page-title New identity +%hr += render 'form' diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index ad58a3837f6..eaa94ed9e36 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -12,7 +12,7 @@ .col-sm-10 = f.text_field :title, class: "form-control", required: true .form-group - = f.label :color, "Background Color", class: 'control-label' + = f.label :color, "Background color", class: 'control-label' .col-sm-10 .input-group .input-group-addon.label-color-preview @@ -31,5 +31,5 @@ = f.submit 'Save', class: 'btn btn-save js-save-button' = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' -:coffeescript - new Labels +:javascript + new Labels(); diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index 596e06243dd..e3ccbf6c3a8 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -2,4 +2,4 @@ = render_colored_label(label) .pull-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' - = link_to 'Remove', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml index 45c62a76259..309aedceded 100644 --- a/app/views/admin/labels/edit.html.haml +++ b/app/views/admin/labels/edit.html.haml @@ -1,9 +1,5 @@ - page_title "Edit", @label.name, "Labels" -%h3 - Edit label - %span.light #{@label.name} -.back-link - = link_to admin_labels_path do - ← To labels list +%h3.page-title + Edit Label %hr = render 'form' diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml index 8d298ad20f7..0135ad0723d 100644 --- a/app/views/admin/labels/new.html.haml +++ b/app/views/admin/labels/new.html.haml @@ -1,7 +1,5 @@ - page_title "New Label" -%h3 New label -.back-link - = link_to admin_labels_path do - ← To labels list +%h3.page-title + New Label %hr = render 'form' diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 701782d26bb..6745e58deca 100644 --- a/app/views/ci/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -8,14 +8,14 @@ %span.label.label-danger paused %td - = link_to ci_admin_runner_path(runner) do + = link_to admin_runner_path(runner) do = runner.short_sha %td .runner-description = runner.description %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) .runner-description-form.hide - = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f| + = form_for [:admin, runner], remote: true, html: { class: 'form-inline' } do |f| .form-group = f.text_field :description, class: 'form-control' = f.submit 'Save', class: 'btn' @@ -38,11 +38,11 @@ Never %td .pull-right - = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm' + = link_to 'Edit', admin_runner_path(runner), class: 'btn btn-sm' - if runner.active? - = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' + = link_to 'Pause', [:pause, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' - else - = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm' - = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to 'Resume', [:resume, :admin, runner], method: :get, class: 'btn btn-success btn-sm' + = link_to 'Remove', [:admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index bb213fbffc4..c407972cd08 100644 --- a/app/views/ci/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,6 +1,20 @@ %p.lead - %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. - %code #{GitlabCi::REGISTRATION_TOKEN} + %span + To register a new runner you should enter the following registration token. + With this token the runner will request a unique runner token and use that for future communication. + Registration token is + %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} + +.bs-callout.clearfix + .pull-left + %p + You can reset runners registration token by pressing a button below. + %p + = button_to reset_runners_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset registration token?' } do + = icon('refresh') + Reset runners registration token .bs-callout %p @@ -21,11 +35,11 @@ \- run builds from assigned projects %li %span.label.label-danger paused - \- runner will not receive any new build + \- runner will not receive any new builds .append-bottom-20.clearfix .pull-left - = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do + = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do .form-group = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false = submit_tag 'Search', class: 'btn' @@ -49,5 +63,5 @@ %th - @runners.each do |runner| - = render "ci/admin/runners/runner", runner: runner + = render "admin/runners/runner", runner: runner = paginate @runners diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 92787b2e6ac..8700b4820cd 100644 --- a/app/views/ci/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -13,16 +13,16 @@ - if @runner.shared? .bs-callout.bs-callout-success - %h4 This runner will process build from ALL UNASSIGNED projects + %h4 This runner will process builds from ALL UNASSIGNED projects %p If you want runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. - else .bs-callout.bs-callout-info - %h4 This runner will process build only from ASSIGNED projects + %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr -= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| += form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| .form-group = label_tag :token, class: 'control-label' do Token @@ -37,7 +37,7 @@ = label_tag :tag_list, class: 'control-label' do Tags .col-sm-10 - = f.text_field :tag_list, class: 'form-control' + = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' .help-block You can setup builds to only use runners with specific tags .form-actions = f.submit 'Save', class: 'btn btn-save' @@ -53,28 +53,24 @@ %th - @runner.runner_projects.each do |runner_project| - project = runner_project.project - %tr.alert-info - %td - %strong - = project.name - %td - .pull-right - = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + - if project + %tr.alert-info + %td + %strong + = project.name_with_namespace + %td + .pull-right + = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' %table.table %thead %tr %th Project %th - .pull-right - = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner), - class: 'btn btn-sm assign-all-runner', - title: 'Assign runner to all projects', - method: :put %tr %td - = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do + = form_tag admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do .form-group = search_field_tag :search, params[:search], class: 'form-control', spellcheck: false = submit_tag 'Search', class: 'btn' @@ -83,41 +79,46 @@ - @projects.each do |project| %tr %td - = project.name + = project.name_with_namespace %td .pull-right - = form_for [:ci, :admin, project, project.runner_projects.new] do |f| + = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' = paginate @projects .col-md-6 %h4 Recent builds served by this runner - %table.builds.runner-builds + %table.table.builds.runner-builds %thead %tr - %th Build ID + %th Build %th Status %th Project %th Commit %th Finished at - @builds.each do |build| + - project = build.project %tr.build %td.id - - gl_project = build.project.gl_project - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do - = build.id + - if project + = link_to namespace_project_build_path(project.namespace, project, build) do + %strong ##{build.id} + - else + %strong ##{build.id} %td.status = ci_status_with_icon(build.status) %td.status - = build.project.name + - if project + = project.name_with_namespace %td.build-link - = link_to ci_status_path(build.commit) do - %strong #{build.commit.short_sha} + - if project + = link_to ci_status_path(build.commit) do + %strong #{build.commit.short_sha} %td.timestamp - if build.finished_at diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/admin/runners/update.js.haml index 2b7d3067e20..2b7d3067e20 100644 --- a/app/views/ci/admin/runners/update.js.haml +++ b/app/views/admin/runners/update.js.haml diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 4245d0f1eda..5e17b018163 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -6,8 +6,8 @@ %span.cred (Admin) .pull-right - - unless @user == current_user - = link_to 'Log in as this user', login_as_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" + - unless @user == current_user || @user.blocked? + = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml index 90d9980c85c..7d11edc79e2 100644 --- a/app/views/admin/users/_profile.html.haml +++ b/app/views/admin/users/_profile.html.haml @@ -16,11 +16,11 @@ - unless user.linkedin.blank? %li %span.light LinkedIn: - %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}" + %strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}" - unless user.twitter.blank? %li %span.light Twitter: - %strong= link_to user.twitter, "http://www.twitter.com/#{user.twitter}" + %strong= link_to user.twitter, "https://twitter.com/#{user.twitter}" - unless user.website_url.blank? %li %span.light Website: diff --git a/app/views/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml index a126a858ea8..a126a858ea8 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/admin/users/_projects.html.haml diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml index a8837d74dd9..3b6fd71500d 100644 --- a/app/views/admin/users/edit.html.haml +++ b/app/views/admin/users/edit.html.haml @@ -1,8 +1,5 @@ - page_title "Edit", @user.name, "Users" %h3.page-title Edit user: #{@user.name} -.back-link - = link_to admin_user_path(@user) do - ← Back to user page %hr = render 'form' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index bc08458312c..a92c9c152b9 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -8,27 +8,27 @@ %li{class: "#{'active' unless params[:filter]}"} = link_to admin_users_path do Active - %small.pull-right= User.active.count + %small.pull-right= number_with_delimiter(User.active.count) %li{class: "#{'active' if params[:filter] == "admins"}"} = link_to admin_users_path(filter: "admins") do Admins - %small.pull-right= User.admins.count + %small.pull-right= number_with_delimiter(User.admins.count) %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} = link_to admin_users_path(filter: 'two_factor_enabled') do 2FA Enabled - %small.pull-right= User.with_two_factor.count + %small.pull-right= number_with_delimiter(User.with_two_factor.count) %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} = link_to admin_users_path(filter: 'two_factor_disabled') do 2FA Disabled - %small.pull-right= User.without_two_factor.count + %small.pull-right= number_with_delimiter(User.without_two_factor.count) %li{class: "#{'active' if params[:filter] == "blocked"}"} = link_to admin_users_path(filter: "blocked") do Blocked - %small.pull-right= User.blocked.count + %small.pull-right= number_with_delimiter(User.blocked.count) %li{class: "#{'active' if params[:filter] == "wop"}"} = link_to admin_users_path(filter: "wop") do Without projects - %small.pull-right= User.without_projects.count + %small.pull-right= number_with_delimiter(User.without_projects.count) %hr = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group @@ -42,7 +42,7 @@ %section.col-md-9 .panel.panel-default .panel-heading - Users (#{@users.total_count}) + Users (#{number_with_delimiter(@users.total_count)}) .panel-head-actions .dropdown.inline %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 0d7a1a25a80..b655b2a15f5 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -14,7 +14,7 @@ .row .col-md-6 - if @personal_projects.present? - = render 'users/projects', projects: @personal_projects + = render 'admin/users/projects', projects: @personal_projects - else .nothing-here-block This user has no personal projects. diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml deleted file mode 100644 index 634c9daa477..00000000000 --- a/app/views/ci/admin/application_settings/_form.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -= form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| - - if @application_setting.errors.any? - #error_explanation - .alert.alert-danger - - @application_setting.errors.full_messages.each do |msg| - %p= msg - - %fieldset - %legend Default Project Settings - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :all_broken_builds do - = f.check_box :all_broken_builds - Send emails only on broken builds - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :add_pusher do - = f.check_box :add_pusher - Add pusher to recipients list - - .form-actions - = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml deleted file mode 100644 index 7ef0aa89ed6..00000000000 --- a/app/views/ci/admin/application_settings/show.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%h3.page-title Settings -%hr -= render 'form' diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml deleted file mode 100644 index 2df58713214..00000000000 --- a/app/views/ci/admin/builds/_build.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -- gl_project = build.project.gl_project -- if build.commit && build.project - %tr.build - %td.build-link - = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do - %strong #{build.id} - - %td.status - = ci_status_with_icon(build.status) - - %td.commit-link - = link_to ci_status_path(build.commit) do - %strong #{build.commit.short_sha} - - %td.runner - - if build.runner - = link_to build.runner.id, ci_admin_runner_path(build.runner) - - %td.build-project - = truncate build.project.name, length: 30 - - %td.build-message - %span= truncate(build.commit.git_commit_message, length: 30) - - %td.build-branch - %span= truncate(build.ref, length: 25) - - %td.duration - - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} - - %td.timestamp - - if build.finished_at - %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml deleted file mode 100644 index d23119162cc..00000000000 --- a/app/views/ci/admin/builds/index.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -%ul.nav.nav-tabs.append-bottom-20 - %li{class: ("active" if @scope.nil?)} - = link_to 'All builds', ci_admin_builds_path - - %li{class: ("active" if @scope == "pending")} - = link_to "Pending", ci_admin_builds_path(scope: :pending) - - %li{class: ("active" if @scope == "running")} - = link_to "Running", ci_admin_builds_path(scope: :running) - - -%table.builds - %thead - %tr - %th Build - %th Status - %th Commit - %th Runner - %th Project - %th Message - %th Branch - %th Duration - %th Finished at - - - @builds.each do |build| - = render "ci/admin/builds/build", build: build - -= paginate @builds diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml deleted file mode 100644 index 5a5b4dc7c35..00000000000 --- a/app/views/ci/admin/events/index.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -.table-holder - %table.table - %thead - %tr - %th User ID - %th Description - %th When - - @events.each do |event| - %tr - %td - = event.user_id - %td - = event.description - %td.light - = time_ago_in_words event.updated_at - ago - -= paginate @events diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml deleted file mode 100644 index a342d6e1cf0..00000000000 --- a/app/views/ci/admin/projects/_project.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -- last_commit = project.commits.last -%tr - %td - = project.id - %td - = link_to [:ci, project] do - %strong= project.name - %td - - if last_commit - = ci_status_with_icon(last_commit.status) - - if project.last_commit_date - · - = time_ago_in_words project.last_commit_date - ago - - else - No builds yet - %td - - if project.public - %i.fa.fa-globe - Public - - else - %i.fa.fa-lock - Private - %td - = project.commits.count - %td - = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do - %i.fa.fa-remove - Remove diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml deleted file mode 100644 index 0da8547924b..00000000000 --- a/app/views/ci/admin/projects/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.table-holder - %table.table - %thead - %tr - %th ID - %th Name - %th Last build - %th Access - %th Builds - %th - - - @projects.each do |project| - = render "ci/admin/projects/project", project: project - -= paginate @projects - diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml deleted file mode 100644 index f049b4f4c4e..00000000000 --- a/app/views/ci/admin/runner_projects/index.html.haml +++ /dev/null @@ -1,57 +0,0 @@ -%p.lead - To register new runner visit #{link_to 'this page ', ci_runners_path} - -.row - .col-md-8 - %h5 Activated: - %table.table - %tr - %th Runner ID - %th Runner Description - %th Last build - %th Builds Stats - %th Registered - %th - - - @runner_projects.each do |runner_project| - - runner = runner_project.runner - - builds = runner.builds.where(project_id: @project.id) - %tr - %td - %span.badge.badge-info= runner.id - %td - = runner.display_name - %td - - last_build = builds.last - - if last_build - = link_to last_build.short_sha, [last_build.project, last_build] - - else - unknown - %td - %span.badge.badge-success - #{builds.success.count} - %span / - %span.badge.badge-important - #{builds.failed.count} - %td - #{time_ago_in_words(runner_project.created_at)} ago - %td - = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right' - .col-md-4 - %h5 Available - %table.table - %tr - %th ID - %th Token - %th - - - (Ci::Runner.all - @project.runners).each do |runner| - %tr - %td - = runner.id - %td - = runner.token - %td - = form_for [:ci, @project, @runner_project] do |f| - = f.hidden_field :runner_id, value: runner.id - = f.submit 'Add', class: 'btn btn-sm' diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml index b24a3b826cf..11163813f3e 100644 --- a/app/views/ci/commits/_commit.html.haml +++ b/app/views/ci/commits/_commit.html.haml @@ -27,7 +27,6 @@ - if commit.finished_at %span #{time_ago_in_words commit.finished_at} ago - - if commit.project.coverage_enabled? + - if commit.coverage %td.coverage - - if commit.coverage - #{commit.coverage}% + #{commit.coverage}% diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml deleted file mode 100644 index 9824e85b1af..00000000000 --- a/app/views/ci/events/index.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -%h3.page-title Events - -.table-holder - %table.table - %thead - %tr - %th User ID - %th Description - %th When - - @events.each do |event| - %tr - %td - = event.user_id - %td - = event.description - %td.light - = time_ago_in_words event.updated_at - ago - -= paginate @events diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index f45cd05aec0..f7875e68b7e 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -17,7 +17,7 @@ %td #{stage.capitalize} Job - #{build[:name]} %td %pre - = simple_format build[:script] + = simple_format build[:commands] %br %b Tag list: @@ -28,6 +28,11 @@ %br %b Refs except: = build[:except] && build[:except].join(", ") + %br + %b When: + = build[:when] + - if build[:allow_failure] + %b Allowed to fail -else %p @@ -36,5 +41,3 @@ %i.fa.fa-remove.incorrect-syntax %b Error: = @error - - diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml deleted file mode 100644 index a96c0b11b6e..00000000000 --- a/app/views/ci/lints/create.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -:plain - $(".results").html("#{escape_javascript(render "create")}")
\ No newline at end of file diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index a9b954771c5..a144c43be47 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,25 +1,17 @@ %h2 Check your .gitlab-ci.yml %hr -= form_tag ci_lint_path, method: :post, remote: true do - .control-group - = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label' - .controls - = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true +.row + = form_tag ci_lint_path, method: :post do + .form-group + = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap' + .col-sm-12 + = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true + .col-sm-12 + .pull-left.prepend-top-10 + = submit_tag 'Validate', class: 'btn btn-success submit-yml' - .control-group.clearfix - .controls.pull-left.prepend-top-10 - = submit_tag "Validate", class: 'btn btn-success submit-yml' - - -%p.text-center.loading - %i.fa.fa-refresh.fa-spin - -.results.prepend-top-20 - -:coffeescript - $(".loading").hide() - $('form').bind 'ajax:beforeSend', -> - $(".loading").show() - $('form').bind 'ajax:complete', -> - $(".loading").hide() +.row.prepend-top-20 + .col-sm-12 + .results + = render partial: 'create' if defined?(@status) diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml index db2d7f2f4b6..09e7e653521 100644 --- a/app/views/ci/shared/_guide.html.haml +++ b/app/views/ci/shared/_guide.html.haml @@ -4,12 +4,10 @@ %ol %li Add at least one runner to the project. - Go to #{link_to 'Runners page', runners_path(@project.gl_project), target: :blank} for instructions. + Go to #{link_to 'Runners page', runners_path(@project), target: :blank} for instructions. %li - Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. + Put the .gitlab-ci.yml in the root of your repository. Examples can be found in + #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} %li - Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank} - and press the "Test settings" button. - %li Return to this page and refresh it, it should show a new build. diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml deleted file mode 100644 index 308b217ea78..00000000000 --- a/app/views/ci/user_sessions/new.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.login-block - %h2 Login using GitLab account - %p.light - Make sure you have account on GitLab server - = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink - %hr - = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' ) - diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index ed480b8caf8..f4a3e3162bf 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,10 +1,20 @@ -%ul.center-top-menu - = nav_link(path: ['projects#index', 'root#index']) do - = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do - Your Projects - = nav_link(page: starred_dashboard_projects_path) do - = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do - Starred Projects - = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do - = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do - Explore Projects += content_for :flash_message do + = render 'shared/project_limit' +.top-area + %ul.left-top-menu + = nav_link(page: [dashboard_projects_path, root_path]) do + = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + Your Projects + = nav_link(page: starred_dashboard_projects_path) do + = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + Starred Projects + = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do + = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do + Explore Projects + + .projects-search-form + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false + - if current_user.can_create_project? + = link_to new_project_path, class: 'btn btn-green' do + %i.fa.fa-plus + New Project diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index c249f5cacec..d5b7e729e7b 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -8,12 +8,12 @@ = link_to new_group_path, class: "btn btn-new" do %i.fa.fa-plus New Group - .title Welcome to the groups! - Group members have access to all group projects. + .oneline + Group members have access to all group projects. %ul.content-list - @group_members.each do |group_member| - group = group_member.group = render 'shared/groups/group', group: group, group_member: group_member -= paginate @group_members += paginate @group_members, theme: 'gitlab' diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index cd602e897b7..2d3da01178a 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,14 +4,20 @@ - if current_user = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues") +.project-issuable-filter + .controls + .pull-left + - if current_user + .hidden-xs.pull-left + = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss -.append-bottom-20 - .pull-right - - if current_user - .hidden-xs.pull-left.prepend-top-20 - = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: '' do - %i.fa.fa-rss + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues -= render 'shared/issues' +.gray-content-block.second-block + List all issues from all projects you have access to. + +.prepend-top-default + = render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index d1f332fa0d3..c5a5ec21f78 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -1,6 +1,14 @@ - page_title "Merge Requests" - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) -.append-bottom-20 +.project-issuable-filter + .controls + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" + = render 'shared/issuable/filter', type: :merge_requests -= render 'shared/merge_requests' + +.gray-content-block.second-block + List all merge requests from all projects you have access to. + +.prepend-top-default + = render 'shared/merge_requests' diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index 55080d6b3fe..7c882a32702 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -16,7 +16,10 @@ = milestone_progress_bar(milestone) .row .col-sm-6 - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = milestone.project.name_with_namespace + .expiration + = render 'shared/milestone_expired', milestone: milestone + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name_with_namespace diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 21b25c3986e..bec1692a4de 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,19 +1,21 @@ - page_title "Milestones" -- header_title "Milestones", dashboard_milestones_path +- header_title "Milestones", dashboard_milestones_path +.project-issuable-filter + .controls + = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true -= render 'shared/milestones_filter' + = render 'shared/milestones_filter' .gray-content-block - .oneline - List all milestones from all projects you have access to. + List all milestones from all projects you have access to. .milestones %ul.content-list - - if @dashboard_milestones.blank? + - if @milestones.blank? %li .nothing-here-block No milestones to show - else - - @dashboard_milestones.each do |milestone| + - @milestones.each do |milestone| = render 'milestone', milestone: milestone - = paginate @dashboard_milestones, theme: "gitlab" + = paginate @milestones, theme: "gitlab" diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 2fe14c6388c..4316c358dcb 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,18 +1,22 @@ -- page_title @dashboard_milestone.title, "Milestones" -%h4.page-title - .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } - - if @dashboard_milestone.closed? +- page_title @milestone.title, "Milestones" +- header_title "Milestones", dashboard_milestones_path + +.detail-page-header + .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? Closed - else Open - Milestone #{@dashboard_milestone.title} + %span.identifier + Milestone #{@milestone.title} -%hr -- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active? - .alert.alert-success - %span All issues for this milestone are closed. You may close the milestone now. +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@milestone.title), pipeline: :single_line -.description +- if @milestone.complete? && @milestone.active? + .alert.alert-success.prepend-top-default + %span All issues for this milestone are closed. You may close the milestone now. .table-holder %table.table @@ -22,7 +26,7 @@ %th Open issues %th State %th Due date - - @dashboard_milestone.milestones.each do |milestone| + - @milestone.milestones.each do |milestone| %tr %td = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) @@ -39,46 +43,60 @@ .context %p.lead Progress: - #{@dashboard_milestone.closed_items_count} closed + #{@milestone.closed_items_count} closed – - #{@dashboard_milestone.open_items_count} open - = milestone_progress_bar(@dashboard_milestone) + #{@milestone.open_items_count} open + = milestone_progress_bar(@milestone) -%ul.nav.nav-tabs +%ul.center-top-menu.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues - %span.badge= @dashboard_milestone.issue_count + %span.badge= @milestone.issue_count %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do Merge Requests - %span.badge= @dashboard_milestone.merge_requests_count + %span.badge= @milestone.merge_requests_count %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants - %span.badge= @dashboard_milestone.participants.count - - .pull-right - = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped" + %span.badge= @milestone.participants.count .tab-content .tab-pane.active#tab-issues - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All issues in this milestone + + .row.prepend-top-default .col-md-6 - = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues + = render 'issues', title: "Open", issues: @milestone.opened_issues .col-md-6 - = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues + = render 'issues', title: "Closed", issues: @milestone.closed_issues .tab-pane#tab-merge-requests - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All merge requests in this milestone + + .row.prepend-top-default .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests + = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests + = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests .tab-pane#tab-participants + .gray-content-block.middle-block + .oneline + All participants to this milestone %ul.bordered-list - - @dashboard_milestone.participants.each do |user| + - @milestone.participants.each do |user| %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index 81a5909e2d2..cea9ffcc748 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -1,11 +1,3 @@ .projects-list-holder - .projects-search-form - .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - - if current_user.can_create_project? - %span.input-group-btn - = link_to new_project_path, class: 'btn btn-green' do - %i.fa.fa-plus - New Project = render 'shared/projects/list', projects: @projects, ci: true diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index d2c51486841..c8c219f4cca 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 7a16b811f6b..53abf274bdb 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -3,7 +3,7 @@ = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity") - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path = render 'dashboard/projects_head' diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index f75f2e0a32a..70705923d42 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -1,5 +1,5 @@ - page_title "Starred Projects" -- header_title "Projects", projects_path +- header_title "Projects", dashboard_projects_path = render 'dashboard/projects_head' diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index d3908062f43..07b6d57932e 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -6,33 +6,29 @@ .gray-content-block .pull-right = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - Add new snippet + = icon('plus') + New Snippet - .oneline - Share code pastes with others out of git repository - -%ul.nav.nav-tabs.prepend-top-20 - = nav_tab :scope, nil do - = link_to dashboard_snippets_path do + .btn-group.btn-group-next.snippet-scope-menu + = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do All %span.badge = current_user.snippets.count - = nav_tab :scope, 'are_private' do - = link_to dashboard_snippets_path(scope: 'are_private') do + + = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do Private %span.badge = current_user.snippets.are_private.count - = nav_tab :scope, 'are_internal' do - = link_to dashboard_snippets_path(scope: 'are_internal') do + + = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do Internal %span.badge = current_user.snippets.are_internal.count - = nav_tab :scope, 'are_public' do - = link_to dashboard_snippets_path(scope: 'are_public') do + + = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do Public %span.badge = current_user.snippets.are_public.count -.my-snippets - = render 'snippets/snippets' += render 'snippets/snippets' diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb deleted file mode 100644 index 79d6c761d8f..00000000000 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<p>Hello <%= @resource.email %>!</p> - -<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p> - -<p>Click the link below to unlock your account:</p> - -<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p> diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml new file mode 100644 index 00000000000..52b327e20c5 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.haml @@ -0,0 +1,10 @@ +%p +Hello #{@resource.name}! + +%p + Your GitLab account has been locked due to an excessive amount of unsuccessful + sign in attempts. Your account will automatically unlock in + = time_ago_in_words(Devise.unlock_in.from_now) + or you may click the link below to unlock now. + +%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 9dc6aeffd59..cb93ff2465e 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -6,17 +6,21 @@ .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", placeholder: "Name", required: true + = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true %div - = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true + = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true %div - = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true + = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true + %div + - if current_application_settings.recaptcha_enabled + = recaptcha_tags %div = f.submit "Sign up", class: "btn-create btn" diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb deleted file mode 100644 index f9277d1673f..00000000000 --- a/app/views/devise/unlocks/new.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<h2>Resend unlock instructions</h2> - -<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> - <%= devise_error_messages! %> - - <div><%= f.label :email %><br /> - <%= f.email_field :email %></div> - - <div><%= f.submit "Resend unlock instructions" %></div> -<% end %> - -<%= render partial: "devise/shared/links" %> diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml new file mode 100644 index 00000000000..49c087c0646 --- /dev/null +++ b/app/views/devise/unlocks/new.html.haml @@ -0,0 +1,14 @@ +.login-box + .login-heading + %h3 Resend unlock email + .login-body + = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| + .devise-errors + = devise_error_messages! + .clearfix.append-bottom-20 + = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off' + .clearfix + = f.submit 'Resend unlock instructions', class: 'btn btn-success' + +.clearfix.prepend-top-20 + = render 'devise/shared/sign_in_link' diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index ad63841ccf3..4ba8b84fd92 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '' · - = gfm event_commit_title(commit[:message]), project: project + = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 9aacc79d686..46432a92348 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,7 +3,7 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache [event, "v2.1"] do + = cache [event, current_application_settings, "v2.1"] do = image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:'' - if event.created_project? = render "events/event/created_project", event: event diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 2761272aa8a..28b12c8dca8 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -3,7 +3,7 @@ .form-group = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false .form-group - = button_tag 'Search', class: "btn btn-success" + = button_tag 'Search', class: "btn" .pull-right.hidden-sm.hidden-xs - if current_user diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 67e38ca3127..b9a958fbe7b 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,12 +1,12 @@ - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path - if current_user = render 'dashboard/projects_head' - else = render 'explore/head' -.gray-content-block.clearfix +.gray-content-block.clearfix.second-block = render 'filter' = render 'projects', projects: @projects = paginate @projects, theme: "gitlab" diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 596cb0a96cd..95d46e331f8 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,5 +1,5 @@ - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path - if current_user = render 'dashboard/projects_head' @@ -7,7 +7,7 @@ = render 'explore/head' .explore-trending-block - .gray-content-block + .gray-content-block.second-block .pull-right = render 'explore/projects/dropdown' .oneline diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 5ea6d81c5b9..fa0b718e48b 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -1,5 +1,5 @@ - page_title "Projects" -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path - if current_user = render 'dashboard/projects_head' @@ -7,7 +7,7 @@ = render 'explore/head' .explore-trending-block - .gray-content-block + .gray-content-block.second-block .pull-right = render 'explore/projects/dropdown' .oneline diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 7e4fa7d4873..0f100c39ffb 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -10,7 +10,8 @@ - if current_user .pull-right = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do - Add new snippet + = icon('plus') + New Snippet .oneline Public snippets created by you and other users are listed here diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 11d69977ef9..bbafc08435a 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,5 +1,5 @@ -.panel.panel-default.projects-list-holder - .panel-heading.clearfix +.projects-list-holder + .projects-search-form .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - if can? current_user, :create_projects, @group diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index ae8fc9f85f0..7e3e2e28bc9 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -3,8 +3,7 @@ .panel.panel-default .panel-heading - %strong= @group.name - group settings: + Group settings .panel-body = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| - if @group.errors.any? @@ -36,4 +35,5 @@ %br %strong Removed group can not be restored! - = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" + .form-actions + = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 3c19381321a..a79a0fcdc8e 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,11 +1,10 @@ - user = member.user - return unless user || member.invite? -- show_roles = true if show_roles.nil? %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} - if member.user - = image_tag avatar_icon(user, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' %strong = link_to user.name, user_path(user) %span.cgray= user.username @@ -15,7 +14,7 @@ %label.label.label-danger %strong Blocked - else - = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' %strong = member.invite_email %span.cgray @@ -25,18 +24,19 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_controls && can?(current_user, :admin_group_member, member) + - if show_controls && can?(current_user, :admin_group_member, @group) = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if show_roles + - if should_user_see_group_roles?(current_user, @group) %span.pull-right - %strong= member.human_access + %strong.member-access-level= member.human_access - if show_controls - if can?(current_user, :update_group_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o + - if can?(current_user, :destroy_group_member, member) - if current_user == user diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index fee4b0052b5..335bf036074 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,42 +1,38 @@ - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) -- show_roles = should_user_see_group_roles?(current_user, @group) - -- if show_roles - %p.light - Members of group have access to all group projects. - Read more about permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - - -.clearfix.js-toggle-container - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } - = button_tag 'Search', class: 'btn' +- @blank_container = true +.group-members-page - if current_user && current_user.can?(:admin_group_member, @group) - .pull-right - = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do - Add members - %i.fa.fa-chevron-down - - .js-toggle-content.hide.new-group-member-holder - = render "new_group_member" - -.panel.panel-default.prepend-top-20 - .panel-heading - %strong #{@group.name} - group members - %small - (#{@members.total_count}) - %ul.well-list - - @members.each do |member| - = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true - -= paginate @members, theme: 'gitlab' - -:coffeescript - $('form.member-search-form').on 'submit', (event) -> - event.preventDefault() - Turbolinks.visit @.action + '?' + $(@).serialize() + .panel.panel-default + .panel-heading + Add new user to group + .panel-body + - if should_user_see_group_roles?(current_user, @group) + %p.light + Members of group have access to all group projects. + .new-group-member-holder + = render "new_group_member" + + .panel.panel-default + .panel-heading + %strong #{@group.name} + group members + %small + (#{@members.total_count}) + .pull-right + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } + = button_tag class: 'btn', title: 'Search' do + = icon("search") + %ul.content-list + - @members.each do |member| + = render 'groups/group_members/group_member', member: member, show_controls: true + = paginate @members, theme: 'gitlab' + +:javascript + $('form.member-search-form').on('submit', function(event) { + event.preventDefault(); + Turbolinks.visit(this.action + '?' + $(this).serialize()); + }); diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index 5bad48abafd..df726e2b2b9 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,2 +1,2 @@ :plain - $("##{dom_id(@member)}").replaceWith('#{escape_javascript(render(@member, member: @member, show_controls: true))}'); + $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}'); diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 08d97e418a3..90ade1e1680 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -4,21 +4,24 @@ - if current_user = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") +.project-issuable-filter + .controls + .pull-left + - if current_user + .hidden-xs.pull-left + = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do + %i.fa.fa-rss + = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" + + = render 'shared/issuable/filter', type: :issues -= render 'shared/issuable/filter', type: :issues .gray-content-block.second-block - .pull-right - - if current_user - .hidden-xs.pull-left - = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do - %i.fa.fa-rss - %div - Only issues from - %strong #{@group.name} - group are listed here. - - if current_user - To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. + Only issues from + %strong #{@group.name} + group are listed here. + - if current_user + To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. .prepend-top-default = render 'shared/issues' diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 425ad8331bf..f662f5a8c17 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,13 +1,18 @@ - page_title "Merge Requests" - header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group)) -= render 'shared/issuable/filter', type: :merge_requests +.project-issuable-filter + .controls + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" + + = render 'shared/issuable/filter', type: :merge_requests + .gray-content-block.second-block - %div - Only merge requests from - %strong #{@group.name} - group are listed here. - - if current_user - To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. + Only merge requests from + %strong #{@group.name} + group are listed here. + - if current_user + To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. + .prepend-top-default = render 'shared/merge_requests' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 41dffdd2fb8..a20bf75bc39 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -22,7 +22,7 @@ %span.label.label-gray = milestone.project.name .col-sm-6 - - if can?(current_user, :admin_group, @group) + - if can?(current_user, :admin_milestones, @group) - if milestone.closed? = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" - else diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 2bbcad5fdfb..b221d3a89a4 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,17 +1,28 @@ - page_title "Milestones" - header_title group_title(@group, "Milestones", group_milestones_path(@group)) -= render 'shared/milestones_filter' +.project-issuable-filter + .controls + - if can?(current_user, :admin_milestones, @group) + .pull-right + %span.pull-right.hidden-xs + = link_to new_group_milestone_path(@group), class: "btn btn-new" do + = icon('plus') + New Milestone + + = render 'shared/milestones_filter' + .gray-content-block Only milestones from %strong #{@group.name} group are listed here. + .milestones %ul.content-list - - if @group_milestones.blank? + - if @milestones.blank? %li .nothing-here-block No milestones to show - else - - @group_milestones.each do |milestone| + - @milestones.each do |milestone| = render 'milestone', milestone: milestone - = paginate @group_milestones, theme: "gitlab" + = paginate @milestones, theme: "gitlab" diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml new file mode 100644 index 00000000000..3894a0ece74 --- /dev/null +++ b/app/views/groups/milestones/new.html.haml @@ -0,0 +1,47 @@ +- page_title "Milestones" +- header_title group_title(@group, "Milestones", group_milestones_path(@group)) + +%h3.page-title + New Milestone + +%p.light + 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-requires-input' } do |f| + .row + .col-md-6 + .form-group + = f.label :title, "Title", class: "control-label" + .col-sm-10 + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true + .form-group.milestone-description + = 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 js-quick-submit' + .clearfix + .error-alert + .form-group + = f.label :projects, "Projects", class: "control-label" + .col-sm-10 + = f.collection_select :project_ids, @group.projects, :id, :name, + { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2' + + .col-md-6 + .form-group + = f.label :due_date, "Due Date", class: "control-label" + .col-sm-10= f.hidden_field :due_date + .col-sm-10 + .datepicker + + .form-actions + = f.submit 'Create Milestone', class: "btn-create btn" + = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" + + +:javascript + $(".datepicker").datepicker({ + dateFormat: "yy-mm-dd", + onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } + }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index a92ad5d751b..d063b257b5e 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,26 +1,28 @@ -- page_title @group_milestone.title, "Milestones" +- page_title @milestone.title, "Milestones" = render "header_title" -%h4.page-title - .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" } - - if @group_milestone.closed? +.detail-page-header + .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } + - if @milestone.closed? Closed - else Open - Milestone #{@group_milestone.title} + %span.identifier + Milestone #{@milestone.title} .pull-right - - if can?(current_user, :admin_group, @group) - - if @group_milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" + - if can?(current_user, :admin_milestones, @group) + - if @milestone.active? + = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" -%hr -- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active? - .alert.alert-success - %span All issues for this milestone are closed. You may close the milestone now. +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@milestone.title), pipeline: :single_line -.description +- if @milestone.complete? && @milestone.active? + .alert.alert-success.prepend-top-default + %span All issues for this milestone are closed. You may close the milestone now. .table-holder %table.table @@ -30,7 +32,7 @@ %th Open issues %th State %th Due date - - @group_milestone.milestones.each do |milestone| + - @milestone.milestones.each do |milestone| %tr %td = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) @@ -47,46 +49,61 @@ .context %p.lead Progress: - #{@group_milestone.closed_items_count} closed + #{@milestone.closed_items_count} closed – - #{@group_milestone.open_items_count} open - = milestone_progress_bar(@group_milestone) + #{@milestone.open_items_count} open + = milestone_progress_bar(@milestone) -%ul.nav.nav-tabs +%ul.center-top-menu.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues - %span.badge= @group_milestone.issue_count + %span.badge= @milestone.issue_count %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do Merge Requests - %span.badge= @group_milestone.merge_requests_count + %span.badge= @milestone.merge_requests_count %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants - %span.badge= @group_milestone.participants.count - - .pull-right - = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped" + %span.badge= @milestone.participants.count .tab-content .tab-pane.active#tab-issues - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All issues in this milestone + + .row.prepend-top-default .col-md-6 - = render 'issues', title: "Open", issues: @group_milestone.opened_issues + = render 'issues', title: "Open", issues: @milestone.opened_issues .col-md-6 - = render 'issues', title: "Closed", issues: @group_milestone.closed_issues + = render 'issues', title: "Closed", issues: @milestone.closed_issues .tab-pane#tab-merge-requests - .row + .gray-content-block.middle-block + .pull-right + = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All merge requests in this milestone + + .row.prepend-top-default .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests + = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests + = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests .tab-pane#tab-participants + .gray-content-block.middle-block + .oneline + All participants to this milestone + %ul.bordered-list - - @group_milestone.participants.each do |user| + - @milestone.participants.each do |user| %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 0665cdf387a..4bc31cabea6 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,5 +1,10 @@ - page_title 'New Group' -- header_title 'New Group' +- header_title "Groups", dashboard_groups_path + +%h3.page-title + New Group +%hr + = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - if @group.errors.any? .alert.alert-danger @@ -18,3 +23,4 @@ .form-actions = f.submit 'Create group', class: "btn btn-create", tabindex: 3 + = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index a91d1a6e94b..7ea574434c3 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index dc8e81323a6..a607d860d7d 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -5,37 +5,47 @@ - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") -.dashboard - .header-with-avatar.clearfix - = image_tag group_icon(@group), class: "avatar group-avatar s90" - %h3 - = @group.name - .username - @#{@group.path} - - if @group.description.present? - .description - = markdown(@group.description, pipeline: :description) - %hr - - = render 'shared/show_aside' - - - if can?(current_user, :read_group, @group) - .row - %section.activities.col-md-7 - .hidden-xs - - if current_user - = render "events/event_last_push", event: @last_push - .pull-right - = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do - %i.fa.fa-rss - - = render 'shared/event_filter' - %hr - - .content_list - = spinner - %aside.side.col-md-5 - = render "projects", projects: @projects - - else - %p - This group does not have public projects +.cover-block + .avatar-holder + = link_to group_icon(@group), target: '_blank' do + = image_tag group_icon(@group), class: "avatar group-avatar s90" + .cover-title + = @group.name + + .cover-desc.username + @#{@group.path} + + - if @group.description.present? + .cover-desc.description + = markdown(@group.description, pipeline: :description) + +- if can?(current_user, :read_group, @group) + %ul.center-top-menu.no-top + %li.active + = link_to "#activity", 'data-toggle' => 'tab' do + Activity + - if @projects.present? + %li + = link_to "#projects", 'data-toggle' => 'tab' do + Projects + + .tab-content + .tab-pane.active#activity + .gray-content-block.activity-filter-block + - if current_user + = render "events/event_last_push", event: @last_push + .pull-right + = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do + %i.fa.fa-rss + + = render 'shared/event_filter' + + .content_list + = spinner + + .tab-pane#projects + = render "projects", projects: @projects + +- else + %p.center-top-menu.no-top + No projects to show diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 67349fcbd78..e8e331dd109 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -219,11 +219,3 @@ %td.shortcut .key r %td Reply (quoting selected text) - - -:javascript - $('.js-more-help-button').click(function(e){ - $(this).remove() - $('.hidden-shortcut').show() - e.preventDefault() - }); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 2169a821fb2..d9ffda884c8 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -31,11 +31,9 @@ %h2#blocks Blocks - %h3 + %h4 %code .gray-content-block - - .gray-content-block.middle-block %h4 Normal block inside content = lorem @@ -45,9 +43,28 @@ = lorem + %h4 + %code .cover-block + %br + .cover-block + .avatar-holder + = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: '' + .cover-title + John Smith + + .cover-desc + = lorem + + .cover-controls + = link_to '#', class: 'btn btn-gray' do + = icon('pencil') + + = link_to '#', class: 'btn btn-gray' do + = icon('rss') + %h2#lists Lists - %h3 + %h4 %code .content-list %ul.content-list %li @@ -57,7 +74,7 @@ %li One item - %h3 + %h4 %code .well-list %ul.well-list %li @@ -67,7 +84,7 @@ %li One item - %h3 + %h4 %code .panel .well-list .panel.panel-default @@ -80,7 +97,7 @@ %li One item - %h3 + %h4 %code .bordered-list %ul.bordered-list %li @@ -121,7 +138,7 @@ %h2#navs Navigation - %h3 + %h4 %code .center-top-menu .example %ul.center-top-menu @@ -130,7 +147,7 @@ %li %a Closed - %h3 + %h4 %code .btn-group.btn-group-next .example %div.btn-group.btn-group-next @@ -138,7 +155,7 @@ %a.btn Closed - %h3 + %h4 %code .nav.nav-tabs .example %ul.nav.nav-tabs @@ -204,7 +221,7 @@ %h2#forms Forms - %h3 + %h4 %code form.horizontal-form %form.form-horizontal @@ -226,7 +243,7 @@ .col-sm-offset-2.col-sm-10 %button.btn.btn-default{:type => "submit"} Sign in - %h3 + %h4 %code form %form @@ -243,7 +260,7 @@ %button.btn.btn-default{:type => "submit"} Sign in %h2#file File - %h3 + %h4 %code .file-holder - blob = Snippet.new(content: "Wow\nSuch\nFile") @@ -254,13 +271,12 @@ .file-actions .btn-group %a.btn Edit - %a.btn Remove + %a.btn.btn-danger Remove .file-contenta.code = render 'shared/file_highlight', blob: blob - %h2#markdown Markdown - %h3 + %h4 %code .md or .wiki and others Markdown rendering has a bit different css and presented in next UI elements: diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 30bcdb86827..aec2e836c9f 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,4 +1,5 @@ - page_title "Bitbucket import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bitbucket Import projects from Bitbucket @@ -66,5 +67,5 @@ again. -:coffeescript - new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") +:javascript + new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}"); diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml index e1bb88ca4ed..5515fad6f48 100644 --- a/app/views/import/fogbugz/new.html.haml +++ b/app/views/import/fogbugz/new.html.haml @@ -1,4 +1,5 @@ - page_title "FogBugz Import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bug Import projects from FogBugz diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index a701e49ac56..07338736bac 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -1,4 +1,5 @@ - page_title 'User map', 'FogBugz import' +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bug Import projects from FogBugz @@ -22,7 +23,7 @@ %strong Map a FogBugz account ID to a GitLab user %p Selecting a GitLab user will add a link to the GitLab user in the descriptions - of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also + of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also associate and/or assign these issues and comments with the selected user. .table-holder @@ -46,5 +47,5 @@ .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' -:coffeescript - new UsersSelect() +:javascript + new UsersSelect(); diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index beca6ab1423..6ee16c8be4b 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -1,4 +1,5 @@ - page_title "FogBugz import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-bug Import projects from FogBugz @@ -48,5 +49,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}") +: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 0669b05adca..1416ee5bd5a 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,4 +1,5 @@ - page_title "GitHub import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-github Import projects from GitHub @@ -43,5 +44,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}") +: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 3bc85059e7d..911a55eb85d 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -1,4 +1,5 @@ - page_title "GitLab.com import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-heart Import projects from GitLab.com @@ -43,5 +44,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}") +: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 2e3a535737f..6b0fa1edf8c 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -1,4 +1,5 @@ - page_title "Gitorious import" +- header_title "Projects", root_path %h3.page-title %i.icon-gitorious.icon-gitorious-big Import projects from Gitorious.org @@ -43,5 +44,5 @@ %td.import-actions.job-status = button_tag "Import", class: "btn js-add-to-import" -:coffeescript - new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") +:javascript + new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}"); diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index 9c64e0a009f..5d2f149cd5f 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -1,4 +1,5 @@ - page_title "Google Code import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-google Import projects from Google Code @@ -6,7 +7,7 @@ = form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do %p - Follow the steps below to export your Google Code project data. + Follow the steps below to export your Google Code project data. In the next step, you'll be able to select the projects you want to import. %ol %li diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml index e53ebda7dc1..0738b3db1eb 100644 --- a/app/views/import/google_code/new_user_map.html.haml +++ b/app/views/import/google_code/new_user_map.html.haml @@ -1,4 +1,5 @@ - page_title "User map", "Google Code import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-google Import projects from Google Code @@ -8,31 +9,31 @@ %p Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import. - %p + %p The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side. %ul %li %strong Default: Directly import the Google Code email address or username %p - <code>"johnsmith@example.com": "johnsm...@example.com"</code> - will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. + <code>"johnsmith@example.com": "johnsm...@example.com"</code> + will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. The email address or username is masked to ensure the user's privacy. %li %strong Map a Google Code user to a GitLab user %p - <code>"johnsmith@example.com": "@johnsmith"</code> - will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com, + <code>"johnsmith@example.com": "@johnsmith"</code> + will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com, and will set <a href="#">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com. %li %strong Map a Google Code user to a full name %p - <code>"johnsmith@example.com": "John Smith"</code> + <code>"johnsmith@example.com": "John Smith"</code> will add "By John Smith" to all issues and comments originally created by johnsmith@example.com. %li %strong Map a Google Code user to a full email address %p - <code>"johnsmith@example.com": "johnsmith@example.com"</code> - will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com. + <code>"johnsmith@example.com": "johnsmith@example.com"</code> + will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address. .form-group diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index c5af06edf87..175ef6921cd 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -1,4 +1,5 @@ - page_title "Google Code import" +- header_title "Projects", root_path %h3.page-title %i.fa.fa-google Import projects from Google Code @@ -67,5 +68,5 @@ = link_to "import flow", new_import_google_code_path again. -:coffeescript - new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}") +:javascript + new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}"); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 74174a72f5a..dd133ee8b56 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,11 +1,24 @@ -- page_title "GitLab" -%head +%head{prefix: "og: http://ogp.me/ns#"} %meta{charset: "utf-8"} %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'} - %meta{content: "GitLab Community Edition", name: "description"} - %meta{name: 'referrer', content: 'origin-when-cross-origin'} - %title= page_title + -# Open Graph - http://ogp.me/ + %meta{property: 'og:type', content: "object"} + %meta{property: 'og:site_name', content: "GitLab"} + %meta{property: 'og:title', content: page_title} + %meta{property: 'og:description', content: page_description} + %meta{property: 'og:image', content: page_image} + %meta{property: 'og:url', content: request.base_url + request.fullpath} + + -# Twitter Card - https://dev.twitter.com/cards/types/summary + %meta{property: 'twitter:card', content: "summary"} + %meta{property: 'twitter:title', content: page_title} + %meta{property: 'twitter:description', content: page_description} + %meta{property: 'twitter:image', content: page_image} + = page_card_meta_tags + + %title= page_title('GitLab') + %meta{name: "description", content: page_description} = favicon_link_tag 'favicon.ico' @@ -18,6 +31,8 @@ = include_gon + - unless browser.safari? + %meta{name: 'referrer', content: 'origin-when-cross-origin'} %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'theme-color', content: '#474D57'} diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 352b8040cf4..ec7cd79bc54 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,8 +1,8 @@ -.page-with-sidebar{ class: nav_sidebar_class } +.page-with-sidebar{ class: page_sidebar_class } = render "layouts/broadcast" - .sidebar-wrapper.nicescroll + .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do = brand_header_logo .gitlab-text-container %h3 GitLab @@ -17,8 +17,8 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - = link_to current_user, class: 'sidebar-user' do - = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36' + = link_to current_user, class: 'sidebar-user', title: "Profile" do + = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username .content-wrapper diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml index 135e8daca26..259b4f7cdfc 100644 --- a/app/views/layouts/_piwik.html.haml +++ b/app/views/layouts/_piwik.html.haml @@ -1,12 +1,14 @@ +<!-- Piwik --> :javascript var _paq = _paq || []; - _paq.push(["trackPageView"]); - _paq.push(["enableLinkTracking"]); - + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); (function() { - var u=(("https:" == document.location.protocol) ? "https" : "http") + "://#{extra_config.piwik_url}/"; - _paq.push(["setTrackerUrl", u+"piwik.php"]); - _paq.push(["setSiteId", "#{extra_config.piwik_site_id}"]); - var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0]; g.type="text/javascript"; - g.defer=true; g.async=true; g.src=u+"piwik.js"; s.parentNode.insertBefore(g,s); + var u="//#{extra_config.piwik_url}/"; + _paq.push(['setTrackerUrl', u+'piwik.php']); + _paq.push(['setSiteId', #{extra_config.piwik_site_id}]); + var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; + g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); })(); +<noscript><p><img src="//#{extra_config.piwik_url}/piwik.php?idsite=#{extra_config.piwik_site_id}" style="border:0;" alt="" /></p></noscript> +<!-- End Piwik Code --> diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index ceb64ce3157..a44f5762a6b 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -11,6 +11,8 @@ = hidden_field_tag :scope, 'merge_requests' - elsif current_controller?(:wikis) = hidden_field_tag :scope, 'wiki_blobs' + - elsif current_controller?(:commits) + = hidden_field_tag :scope, 'commits' - else = hidden_field_tag :search_code, true @@ -23,6 +25,6 @@ :javascript $('.search-input').on('keyup', function(e) { if (e.keyCode == 27) { - $('.search-input').blur() + $('.search-input').blur(); } - }) + }); diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 1c738719bd8..6591c52bdbd 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,5 +1,5 @@ -- page_title "Admin area" -- header_title "Admin area", admin_root_path +- page_title "Admin Area" +- header_title "Admin Area", admin_root_path - sidebar "admin" = render template: "layouts/application" diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml deleted file mode 100644 index af2545a22d8..00000000000 --- a/app/views/layouts/ci/_nav_admin.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to admin_root_path, title: 'Back to admin', data: {placement: 'right'}, class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Back to admin - - %li.separate-item - = nav_link path: 'projects#index' do - = link_to ci_admin_projects_path do - = icon('list-alt fw') - Projects - = nav_link path: 'events#index' do - = link_to ci_admin_events_path do - = icon('book fw') - Events - = nav_link path: ['runners#index', 'runners#show'] do - = link_to ci_admin_runners_path do - = icon('cog fw') - Runners - %small.pull-right - = Ci::Runner.count(:all) - = nav_link path: 'builds#index' do - = link_to ci_admin_builds_path do - = icon('link fw') - Builds - %small.pull-right - = Ci::Build.count(:all) - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to ci_admin_application_settings_path do - = icon('cogs fw') - %span - Settings diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml deleted file mode 100644 index f094edbfa87..00000000000 --- a/app/views/layouts/ci/_nav_project.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to project_path(@project.gl_project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Back to project - %li.separate-item - = nav_link path: 'events#index' do - = link_to ci_project_events_path(@project) do - = icon('book fw') - %span - Events diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml index ab3e29c3f42..7e90af21b21 100644 --- a/app/views/layouts/ci/_page.html.haml +++ b/app/views/layouts/ci/_page.html.haml @@ -1,8 +1,8 @@ -.page-with-sidebar{ class: nav_sidebar_class } +.page-with-sidebar{ class: page_sidebar_class } = render "layouts/broadcast" - .sidebar-wrapper.nicescroll + .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do = brand_header_logo .gitlab-text-container %h3 GitLab @@ -14,8 +14,8 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - = link_to current_user, class: 'sidebar-user' do - = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36' + = link_to current_user, class: 'sidebar-user', title: "Profile" do + = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username .content-wrapper diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml deleted file mode 100644 index c8cb185d28c..00000000000 --- a/app/views/layouts/ci/admin.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render 'layouts/head' - %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} - - header_title = "Admin area" - - if current_user - = render "layouts/header/default", title: header_title - - else - = render "layouts/header/public", title: header_title - - = render 'layouts/ci/page', sidebar: 'nav_admin' diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml deleted file mode 100644 index 38023468d0b..00000000000 --- a/app/views/layouts/ci/application.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render 'layouts/head' - %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} - - header_title = "Continuous Integration" - - if current_user - = render "layouts/header/default", title: header_title - - else - = render "layouts/header/public", title: header_title - - = render 'layouts/ci/page' diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml deleted file mode 100644 index 15478c3f5bc..00000000000 --- a/app/views/layouts/ci/project.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -%html{ lang: "en"} - = render 'layouts/head' - %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} - - header_title @project.name, ci_project_path(@project) - - if current_user - = render "layouts/header/default", title: header_title - - else - = render "layouts/header/public", title: header_title - - = render 'layouts/ci/page', sidebar: 'nav_project' diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 95e077c339f..f08cb0a5428 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,13 +1,13 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body.ui_charcoal.login-page.application + %body.ui_charcoal.login-page.application.navless = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container .content = render "layouts/flash" - .row.prepend-top-20 + .row .col-sm-5.pull-right = yield .col-sm-7.brand-holder.pull-left diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 2af265a2296..915acc4612e 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body{class: "#{user_application_theme} application"} + %body{class: "#{user_application_theme} application navless"} = render "layouts/header/empty" .container.navless-container = render "layouts/flash" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index c31b1cbe9a8..3892ef8eefa 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -11,18 +11,27 @@ %li.hidden-sm.hidden-xs = render 'layouts/search' %li.visible-sm.visible-xs - = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') + - if session[:impersonator_id] + %li.impersonation + = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = icon('user-secret fw') - if current_user.is_admin? %li - = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - if current_user.can_create_project? %li - = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('plus fw') + - if Gitlab::Sherlock.enabled? + %li + = link_to sherlock_transactions_path, title: 'Sherlock Transactions', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('tachometer fw') %li - = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do + = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('sign-out') %h1.title= title diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 2079feeeab6..cffdb52cc23 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -5,78 +5,85 @@ %span Overview = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do + = link_to admin_namespaces_projects_path, title: 'Projects' do = icon('cube fw') %span Projects = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do + = link_to admin_users_path, title: 'Users' do = icon('user fw') %span Users = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to admin_groups_path, title: 'Groups' do = icon('group fw') %span Groups = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do = icon('key fw') %span Deploy Keys - = nav_link do - = link_to ci_admin_projects_path, title: 'Continuous Integration', data: {placement: 'right'} do - = icon('building fw') + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path do + = icon('cog fw') + %span + Runners + %span.count= number_with_delimiter(Ci::Runner.count(:all)) + = nav_link path: 'builds#index' do + = link_to admin_builds_path do + = icon('link fw') %span - Continuous Integration + Builds + %span.count= number_with_delimiter(Ci::Build.count(:all)) = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do + = link_to admin_logs_path, title: 'Logs' do = icon('file-text fw') %span Logs = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do + = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') %span Messages = nav_link(controller: :hooks) do - = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do + = link_to admin_hooks_path, title: 'Hooks' do = icon('external-link fw') %span Hooks = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do + = link_to admin_background_jobs_path, title: 'Background Jobs' do = icon('cog fw') %span Background Jobs = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do + = link_to admin_applications_path, title: 'Applications' do = icon('cloud fw') %span Applications = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do + = link_to admin_application_settings_services_path, title: 'Service Templates' do = icon('copy fw') %span Service Templates = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels', data: {placement: 'right'} do + = link_to admin_labels_path, title: 'Labels' do = icon('tags fw') %span Labels = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse reports" do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do = icon('exclamation-circle fw') %span Abuse Reports - %span.count= AbuseReport.count(:all) + %span.count= number_with_delimiter(AbuseReport.count(:all)) = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do - = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do + = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index b1a1d531846..106abd24a56 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,50 +1,50 @@ %ul.nav.nav-sidebar = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do - = link_to dashboard_projects_path, title: 'Projects', data: {placement: 'right'} do + = link_to dashboard_projects_path, title: 'Projects' do = icon('home fw') %span Projects = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity', data: {placement: 'right'} do + = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do = icon('dashboard fw') %span Activity = nav_link(controller: :groups) do - = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups = nav_link(controller: :milestones) do - = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do + = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span Milestones = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do = icon('exclamation-circle fw') %span Issues - %span.count= current_user.assigned_issues.opened.count + %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests - %span.count= current_user.assigned_merge_requests.opened.count + %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) = nav_link(controller: :snippets) do - = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') %span Snippets = nav_link(controller: :help) do - = link_to help_path, title: 'Help', data: {placement: 'right'} do + = link_to help_path, title: 'Help' do = icon('question-circle fw') %span Help %li.separate-item = nav_link(controller: :profile) do - = link_to profile_path, title: 'Profile settings', data: {placement: 'bottom'} do + = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') %span Profile Settings diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index 21e565972a7..48039ca2918 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,21 +1,21 @@ %ul.nav.nav-sidebar = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do - = link_to explore_root_path, title: 'Projects', data: {placement: 'right'} do + = link_to explore_root_path, title: 'Projects' do = icon('home fw') %span Projects = nav_link(controller: :groups) do - = link_to explore_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to explore_groups_path, title: 'Groups' do = icon('group fw') %span Groups = nav_link(controller: :snippets) do - = link_to explore_snippets_path, title: 'Snippets', data: {placement: 'right'} do + = link_to explore_snippets_path, title: 'Snippets' do = icon('clipboard fw') %span Snippets = nav_link(controller: :help) do - = link_to help_path, title: 'Help', data: {placement: 'right'} do + = link_to help_path, title: 'Help' do = icon('question-circle fw') %span Help diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index eb35af22b93..e5e2a59eaed 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,46 +1,46 @@ %ul.nav.nav-sidebar = nav_link do - = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to dashboard + Go to dashboard %li.separate-item = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do + = link_to group_path(@group), title: 'Home' do = icon('dashboard fw') %span Group - if can?(current_user, :read_group, @group) - if current_user = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do + = link_to group_milestones_path(@group), title: 'Milestones' do = icon('clock-o fw') %span Milestones = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do + = link_to issues_group_path(@group), title: 'Issues' do = icon('exclamation-circle fw') %span Issues - if current_user - %span.count= Issue.opened.of_group(@group).count + %span.count= number_with_delimiter(Issue.opened.of_group(@group).count) = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do = icon('tasks fw') %span Merge Requests - if current_user - %span.count= MergeRequest.opened.of_group(@group).count + %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count) = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do + = link_to group_group_members_path(@group), title: 'Members' do = icon('users fw') %span Members - if can?(current_user, :admin_group, @group) = nav_link(html_options: { class: "separate-item" }) do - = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do + = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 8075fe32fbc..56a92fe9103 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,20 +1,20 @@ %ul.nav.nav-sidebar = nav_link do - = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = link_to group_path(@group), title: 'Go to group', class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to group + Go to group %li.separate-item %ul.sidebar-subnav = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Group Settings', data: {placement: 'right'} do + = link_to edit_group_path(@group), title: 'Group Settings' do = icon ('pencil-square-o fw') %span Group Settings = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do + = link_to projects_group_path(@group), title: 'Projects' do = icon('folder fw') %span Projects diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 5a47b8e6db2..f3ded04419b 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,59 +1,59 @@ %ul.nav.nav-sidebar = nav_link do - = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to dashboard + Go to dashboard %li.separate-item = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: 'Profile', data: {placement: 'right'} do + = link_to profile_path, title: 'Profile Settings' do = icon('user fw') %span Profile Settings = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do + = link_to profile_account_path, title: 'Account' do = icon('gear fw') %span Account = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do - = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do + = link_to applications_profile_path, title: 'Applications' do = icon('cloud fw') %span Applications = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do + = link_to profile_emails_path, title: 'Emails' do = icon('envelope-o fw') %span Emails - %span.count= current_user.emails.count + 1 + %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', data: {placement: 'right'} do + = link_to edit_profile_password_path, title: 'Password' do = icon('lock fw') %span Password = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do + = link_to profile_notifications_path, title: 'Notifications' do = icon('inbox fw') %span Notifications = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do + = link_to profile_keys_path, title: 'SSH Keys' do = icon('key fw') %span SSH Keys - %span.count= current_user.keys.count + %span.count= number_with_delimiter(current_user.keys.count) = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} do + = link_to profile_preferences_path, title: 'Preferences' do -# TODO (rspeicher): Better icon? = icon('image fw') %span Preferences = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Audit Log', data: {placement: 'right'} do + = link_to audit_log_profile_path, title: 'Audit Log' do = icon('history fw') %span Audit Log diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 53a913fe8f3..d3eaf0f3209 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,120 +1,120 @@ %ul.nav.nav-sidebar - if @project.group = nav_link do - = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do + = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to group + Go to group - else = nav_link do - = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = link_to root_path, title: 'Go to dashboard', class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to dashboard + Go to dashboard %li.separate-item = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = icon('home fw') %span Project = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Project Activity', class: 'shortcuts-project-activity', data: {placement: 'right'} do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do = icon('dashboard fw') %span Activity - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do = icon('files-o fw') %span Files - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do + = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do = icon('history fw') %span Commits - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds', data: {placement: 'right'} do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = icon('cubes fw') %span Builds - %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all) - - - if project_nav_tab? :network - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do - = icon('code-fork fw') - %span - Network + %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do = icon('area-chart fw') %span Graphs - if project_nav_tab? :milestones = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones', data: {placement: 'right'} do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do = icon('clock-o fw') %span Milestones - if project_nav_tab? :issues = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do = icon('exclamation-circle fw') %span Issues - if @project.default_issues_tracker? - %span.count.issue_counter= @project.issues.opened.count + %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests - %span.count.merge_counter= @project.merge_requests.opened.count + %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - if project_nav_tab? :settings = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do = icon('users fw') %span Members - if project_nav_tab? :labels = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', data: {placement: 'right'} do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do = icon('tags fw') %span Labels - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do = icon('book fw') %span Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do = icon('clipboard fw') %span Snippets - if project_nav_tab? :settings = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings', data: {placement: 'right'} do + = link_to edit_project_path(@project), title: 'Settings' do = icon('cogs fw') %span Settings + + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network + diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 954dbe5d2b9..970da78a5c9 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,72 +1,52 @@ %ul.nav.nav-sidebar = nav_link do - = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do + = link_to project_path(@project), title: 'Go to project', class: 'back-link' do = icon('caret-square-o-left fw') %span - Back to project + Go to project %li.separate-item %ul.sidebar-subnav = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project Settings', data: {placement: 'right'} do + = link_to edit_project_path(@project), title: 'Project Settings' do = icon('pencil-square-o fw') %span Project Settings = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = icon('key fw') %span Deploy Keys = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do = icon('link fw') %span Web Hooks = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do = icon('cogs fw') %span Services = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do = icon('lock fw') %span Protected Branches - - if @project.gitlab_ci? + - if @project.builds_enabled? = nav_link(controller: :runners) do - = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do = icon('cog fw') %span Runners = nav_link(controller: :variables) do - = link_to namespace_project_variables_path(@project.namespace, @project) do + = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do = icon('code fw') %span Variables = nav_link path: 'triggers#index' do - = link_to namespace_project_triggers_path(@project.namespace, @project) do + = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do = icon('retweet fw') %span Triggers - = nav_link path: 'ci_web_hooks#index' do - = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project) do - = icon('link fw') - %span - CI Web Hooks - = nav_link path: 'ci_settings#edit' do - = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do - = icon('building fw') - %span - CI Settings - = nav_link controller: 'ci_services' do - = link_to namespace_project_ci_services_path(@project.namespace, @project) do - = icon('share fw') - %span - CI Services - = nav_link path: 'events#index' do - = link_to ci_project_events_path(@project.gitlab_ci_project) do - = icon('book fw') - %span - CI Events diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 854cda57c39..3ca4c340406 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -40,9 +40,10 @@ Reply to this email directly or #{link_to "view it on GitLab", @target_url}. - else - #{link_to "View it on GitLab", @target_url} + #{link_to "View it on GitLab", @target_url}. %br - You're receiving this email because of your account on #{link_to Gitlab.config.gitlab.host, root_url}. + -# Don't link the host is the line below, one link in the email is easier to quickly click than two. + You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. If you'd like to receive fewer emails, you can adjust your notification settings. - = email_action @target_url + = email_action @target_url
\ No newline at end of file diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index 69689a75022..f4e9749e5c7 100644 --- a/app/views/ci/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,19 +1,23 @@ - content_for :header do %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} - GitLab CI (build failed) + GitLab (build failed) %h3 Project: = link_to ci_project_url(@project) do = @project.name %p - Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p Branch: #{@build.ref} %p + Stage: #{@build.stage} +%p + Job: #{@build.name} +%p Message: #{@build.commit.git_commit_message} %p - Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} + Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 6de5dc10f17..675acea60a1 100644 --- a/app/views/ci/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -4,6 +4,8 @@ Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> +Stage: <%= @build.stage %> +Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> -Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> +Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 4e3015a356b..8b004d34cca 100644 --- a/app/views/ci/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} - GitLab CI (build successful) + GitLab (build successful) %h3 Project: @@ -8,13 +8,17 @@ = @project.name %p - Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} + Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p Author: #{@build.commit.git_author_name} %p Branch: #{@build.ref} %p + Stage: #{@build.stage} +%p + Job: #{@build.name} +%p Message: #{@build.commit.git_commit_message} %p - Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)} + Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index d0a43ae1c12..747da44acae 100644 --- a/app/views/ci/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -4,6 +4,8 @@ Status: <%= @build.status %> Commit: <%= @build.commit.short_sha %> Author: <%= @build.commit.git_author_name %> Branch: <%= @build.ref %> +Stage: <%= @build.stage %> +Job: <%= @build.name %> Message: <%= @build.commit.git_commit_message %> -Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %> +Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb index d8a23dabf49..b2c5f71e465 100644 --- a/app/views/notify/project_was_moved_email.text.erb +++ b/app/views/notify/project_was_moved_email.text.erb @@ -1,4 +1,4 @@ -Project #{@old_path_with_namespace} was moved to another location +Project <%= @old_path_with_namespace %> was moved to another location The project is now located under <%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 12f83aae04b..4361f67a74d 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,30 +1,32 @@ -%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} +%h3 + #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} + at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))} -- if @compare - - if @reverse_compare +- if @message.compare + - if @message.reverse_compare? %p %strong WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. %h4 - = @reverse_compare ? "Deleted commits:" : "Commits:" + = @message.reverse_compare? ? "Deleted commits:" : "Commits:" %ul - - @commits.each do |commit| + - @message.commits.each do |commit| %li - %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} + %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))} %div %span by #{commit.author_name} %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} %pre.commit-message = commit.safe_message - %h4 #{pluralize @diffs.count, "changed file"}: + %h4 #{pluralize @message.diffs_count, "changed file"}: %ul - - @diffs.each_with_index do |diff, i| + - @message.diffs.each_with_index do |diff, i| %li.file-stats - %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } + %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" } - if diff.deleted_file %span.deleted-file − @@ -40,11 +42,11 @@ - else = diff.new_path - - unless @disable_diffs + - unless @message.disable_diffs? %h4 Changes: - - @diffs.each_with_index do |diff, i| + - @message.diffs.each_with_index do |diff, i| %li{id: "diff-#{i}"} - %a{href: @target_url + "#diff-#{i}"} + %a{href: @message.target_url + "#diff-#{i}"} - if diff.deleted_file %strong = diff.old_path @@ -62,5 +64,5 @@ = color_email_diff(diff.diff) %br - - if @compare.timeout + - if @message.compare_timeout %h5 Huge diff. To prevent performance issues changes are hidden diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 97a176ed2a3..aa0e263b6df 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,21 +1,21 @@ -#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace} -- if @compare +#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} at #{@message.project_name_with_namespace} +- if @message.compare \ \ - - if @reverse_compare + - if @message.reverse_compare? WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. \ \ - = @reverse_compare ? "Deleted commits:" : "Commits:" - - @commits.each do |commit| + = @message.reverse_compare? ? "Deleted commits:" : "Commits:" + - @message.commits.each do |commit| #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} #{commit.safe_message} \- - - - - \ \ - #{pluralize @diffs.count, "changed file"}: + #{pluralize @message.diffs_count, "changed file"}: \ - - @diffs.each do |diff| + - @message.diffs.each do |diff| - if diff.deleted_file \- − #{diff.old_path} - elsif diff.renamed_file @@ -24,11 +24,11 @@ \- + #{diff.new_path} - else \- #{diff.new_path} - - unless @disable_diffs + - unless @message.disable_diffs? \ \ Changes: - - @diffs.each do |diff| + - @message.diffs.each do |diff| \ \===================================== - if diff.deleted_file @@ -39,11 +39,11 @@ = diff.new_path \===================================== != diff.diff - - if @compare.timeout + - if @message.compare_timeout \ \ Huge diff. To prevent performance issues it was hidden - - if @target_url + - if @message.target_url \ \ - View it on GitLab: #{@target_url} + View it on GitLab: #{@message.target_url} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index cd7b1b0fe03..17e47c622ce 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -23,11 +23,14 @@ %p.cgray - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" - %div - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token" - else %span You don`t have one yet. Click generate to fix it. - = f.submit 'Generate', class: "btn btn-default btn-build-token" + + .form-actions + - if current_user.private_token + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default" + - else + = f.submit 'Generate', class: "btn btn-default" - unless current_user.ldap_user? .panel.panel-default @@ -54,7 +57,8 @@ %p Each time you log in you’ll be required to provide your username and password as usual, plus a randomly-generated code from your phone. - %div + + .form-actions = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - if button_based_providers.any? @@ -81,15 +85,16 @@ %p Changing your username will change path to all personal projects! %div - = f.text_field :username, required: true, class: 'form-control' + .input-group + .input-group-addon + = "#{root_url}u/" + = f.text_field :username, required: true, class: 'form-control' .loading-gif.hide %p = icon('spinner spin') Saving new username - %p.light - = user_url(@user) - %div + .form-actions = f.submit 'Save username', class: "btn btn-warning" - if signup_enabled? @@ -104,7 +109,8 @@ - rp = current_user.personal_projects.count - unless rp.zero? %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" + .form-actions + = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - else - if @user.solo_owned_groups.present? %p diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index 2342936a5d5..0436c2213da 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -15,24 +15,25 @@ .pull-right = link_to 'New Application', new_oauth_application_path, class: 'btn btn-success' - if @applications.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Callback URL - %th Clients - %th - %th - %tbody - - @applications.each do |application| - %tr{:id => "application_#{application.id}"} - %td= link_to application.name, oauth_application_path(application) - %td - - application.redirect_uri.split.each do |uri| - %div= uri - %td= application.access_tokens.count - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' - %td= render 'doorkeeper/applications/delete_form', application: application + .table-holder + %table.table.table-striped + %thead + %tr + %th Name + %th Callback URL + %th Clients + %th + %th + %tbody + - @applications.each do |application| + %tr{:id => "application_#{application.id}"} + %td= link_to application.name, oauth_application_path(application) + %td + - application.redirect_uri.split.each do |uri| + %div= uri + %td= application.access_tokens.count + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' + %td= render 'doorkeeper/applications/delete_form', application: application .oauth-authorized-applications.prepend-top-20 - if user_oauth_applications? @@ -40,29 +41,30 @@ Authorized applications - if @authorized_tokens.any? - %table.table.table-striped - %thead - %tr - %th Name - %th Authorized At - %th Scope - %th - %tbody - - @authorized_apps.each do |app| - - token = app.authorized_tokens.order('created_at desc').first - %tr{:id => "application_#{app.id}"} - %td= app.name - %td= token.created_at - %td= token.scopes - %td= render 'doorkeeper/authorized_applications/delete_form', application: app - - @authorized_anonymous_tokens.each do |token| + .table-holder + %table.table.table-striped + %thead %tr - %td - Anonymous - %div.help-block - %em Authorization was granted by entering your username and password in the application. - %td= token.created_at - %td= token.scopes - %td= render 'doorkeeper/authorized_applications/delete_form', token: token + %th Name + %th Authorized At + %th Scope + %th + %tbody + - @authorized_apps.each do |app| + - token = app.authorized_tokens.order('created_at desc').first + %tr{:id => "application_#{app.id}"} + %td= app.name + %td= token.created_at + %td= token.scopes + %td= render 'doorkeeper/authorized_applications/delete_form', application: app + - @authorized_anonymous_tokens.each do |token| + %tr + %td + Anonymous + %div.help-block + %em Authorization was granted by entering your username and password in the application. + %td= token.created_at + %td= token.scopes + %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else %p.light You don't have any authorized applications diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index b76a5b636ac..2a8800de60e 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -1,5 +1,5 @@ %div - = form_for [:profile, @key], html: { class: 'form-horizontal' } do |f| + = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f| - if @key.errors.any? .alert.alert-danger %ul @@ -9,12 +9,11 @@ .form-group = f.label :key, class: 'control-label' .col-sm-10 - = f.text_area :key, class: "form-control", rows: 8 + = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true .form-group = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, class: "form-control" + .col-sm-10= f.text_field :title, class: "form-control", required: true .form-actions = f.submit 'Add key', class: "btn btn-create" = link_to "Cancel", profile_keys_path, class: "btn btn-cancel" - diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml index ef0075aad3b..8c9d546af4c 100644 --- a/app/views/profiles/keys/_key_table.html.haml +++ b/app/views/profiles/keys/_key_table.html.haml @@ -1,6 +1,6 @@ - is_admin = defined?(admin) ? true : false -.panel.panel-default - - if @keys.any? +- if @keys.any? + .table-holder %table.table %thead.panel-heading %tr @@ -11,9 +11,9 @@ %tbody - @keys.each do |key| = render 'profiles/keys/key', key: key, is_admin: is_admin - - else - .nothing-here-block - - if is_admin - User has no ssh keys - - else - There are no SSH keys with access to your account. +- else + .nothing-here-block + - if is_admin + User has no ssh keys + - else + There are no SSH keys with access to your account. diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 14adba1c797..17a4195030e 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -3,7 +3,9 @@ .gray-content-block.top-block .pull-right - = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" + = link_to new_profile_key_path, class: "btn btn-new" do + = icon('plus') + Add SSH Key .oneline Before you can add an SSH key you need to = link_to "generate it.", help_page_path("ssh", "README") diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml index 2bf207a3221..13a18269d11 100644 --- a/app/views/profiles/keys/new.html.haml +++ b/app/views/profiles/keys/new.html.haml @@ -9,9 +9,9 @@ $('#key_key').on('focusout', function(){ var title = $('#key_title'), val = $('#key_key').val(), - comment = val.match(/^\S+ \S+ (.+)$/); + comment = val.match(/^\S+ \S+ (.+)\n?$/); if( comment && comment.length > 1 && title.val() == '' ){ - $('#key_title').val( comment[1] ); + $('#key_title').val( comment[1] ).change(); } }); diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml index 2c85d2a9b2b..742c5c4b68d 100644 --- a/app/views/profiles/notifications/_settings.html.haml +++ b/app/views/profiles/notifications/_settings.html.haml @@ -14,4 +14,4 @@ = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'trigger-submit' + = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 8eebd96b674..0bcadc965fa 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -50,12 +50,10 @@ Watch %p You will receive notifications for any activity - .form-actions + .gray-content-block = f.submit 'Save changes', class: "btn btn-create" -.clearfix - %hr -.row.all-notifications +.row.all-notifications.prepend-top-default .col-md-6 %p You can also specify notification level per group or per project. diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index cc41d7dd813..877589dc390 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -54,4 +54,4 @@ .help-block Choose what content you want to see on a project's home page. .panel-footer - = f.submit 'Save', class: 'btn btn-save' + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index ac7355dde1f..9459d8a6295 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -43,7 +43,7 @@ .form-group = f.label :public_email, class: "control-label" .col-sm-10 - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control" + = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2" %span.help-block This email will be displayed on your public profile. .form-group = f.label :skype, class: "control-label" @@ -96,8 +96,6 @@ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - .row - .col-md-7 - .form-group - .col-sm-offset-2.col-sm-10 - = f.submit 'Save changes', class: "btn btn-success" + .form-actions + = f.submit 'Save changes', class: "btn btn-success" + = link_to "Cancel", user_path(current_user), class: "btn btn-cancel" diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 92dc58c10d7..1a5b6efce35 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -38,3 +38,4 @@ = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true .form-actions = submit_tag 'Submit', class: 'btn btn-success' + = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 012858f70b4..101880bd105 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -8,5 +8,5 @@ .content_list{:"data-href" => activity_project_path(@project)} = spinner -:coffeescript - new Activities() +:javascript + new Activities(); diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index 35f7e7bb34b..640612ca433 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -1,6 +1,8 @@ .form-actions - .commit-button-annotation - = button_tag 'Commit Changes', - class: 'btn commit-btn js-commit-button btn-create' + = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create' = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} + + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 8c0980369fd..0f61e623396 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,5 +1,5 @@ - empty_repo = @project.empty_repo? -.project-home-panel.clearfix{:class => ("empty-project" if empty_repo)} +.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)} .project-identicon-holder = project_icon(@project, alt: '', class: 'project-avatar avatar s90') .project-home-desc @@ -12,23 +12,45 @@ Forked from = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) + .cover-controls.left + .visibility-level-label.has_tooltip{title: project_visibility_level_description(@project.visibility_level), data: { container: 'body' } } + = visibility_level_icon(@project.visibility_level, fw: false) + = visibility_level_label(@project.visibility_level) - + .cover-controls + - if current_user + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do + = icon('rss') + - access = user_max_access_in_project(current_user.id, @project) + - can_edit = can?(current_user, :admin_project, @project) + - if access || can_edit + %span.dropdown.project-settings-dropdown + %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('angle-down') + %ul.dropdown-menu.dropdown-menu-right + - if can_edit + %li + = link_to edit_project_path(@project) do + Edit Project + - if access + %li + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave Project .project-repo-buttons - .split-one + .split-one.count-buttons = render 'projects/buttons/star' + = render 'projects/buttons/fork' - - unless empty_repo - = render 'projects/buttons/fork' - = render "shared/clone_panel" - .split-repo-buttons - - unless empty_repo - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - = icon('download fw') - + + .split-repo-buttons + = render "projects/buttons/download" = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' - + +:coffeescript + new Star()
\ No newline at end of file diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index d7b20bfc6b1..386d72e7787 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -3,10 +3,10 @@ - if ci_commit = link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do = ci_status_icon(ci_commit) - = ci_commit.status + = ci_status_label(ci_commit) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" · #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 7b21095ea3e..54c818baaf4 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -5,20 +5,21 @@ %a.js-md-write-button(href="#md-write-holder" tabindex="-1") Write %li - %a.js-md-preview-button(href="md-preview-holder" tabindex="-1") + %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") Preview - - if defined?(referenced_users) && referenced_users - %span.referenced-users.pull-left.hide + %div + .md-write-holder + = yield + .md.md-preview-holder.hide + .js-md-preview{class: (preview_class if defined?(preview_class))} + + - if defined?(referenced_users) && referenced_users + %div.referenced-users.hide + %span = icon('exclamation-triangle') You are about to add %strong %span.js-referenced-users-count 0 people to the discussion. Proceed with caution. - - %div - .md-write-holder - = yield - .md.md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 0a1cecfdcdf..d1191928d4f 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -1,23 +1,22 @@ - if readme = @repository.readme - %article.file-holder.readme-holder - .file-title - = blob_icon readme.mode, readme.name - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do - %strong - = readme.name + %article.readme-holder + .pull-right + - if can?(current_user, :push_code, @project) + = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme' .file-content.wiki = cache(readme_cache_key) do = render_readme(readme) - else - %h3.page-title - This project does not have README yet - - if can?(current_user, :push_code, @project) - %p.slead - A - %code README - file contains information about other files in a repository and is commonly - distributed with computer software, forming part of its documentation. - %br - We recommend you to - = link_to "add README", new_readme_path, class: 'underlined-link' - file to the repository and GitLab will render it here instead of this message. + .gray-content-block.second-block.center + %h3.page-title + This project does not have README yet + - if can?(current_user, :push_code, @project) + %p + A + %code README + file contains information about other files in a repository and is commonly + distributed with computer software, forming part of its documentation. + %p + We recommend you to + = link_to "add 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 63ebfc9381f..7e6301abde8 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -2,9 +2,12 @@ %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") .zen-backdrop - classes << ' js-gfm-input markdown-area' - = f.text_area attr, class: classes, placeholder: '' + - if defined?(f) && f + = f.text_area attr, class: classes, placeholder: '' + - else + = text_area_tag attr, nil, class: classes, placeholder: '' %a.zen-enter-link(tabindex="-1" href="#") - %i.fa.fa-expand + = icon('expand') Edit in fullscreen %a.zen-leave-link(href="#") - %i.fa.fa-compress + = icon('compress') diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 6518c4173e1..8d9ec068a43 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -6,7 +6,7 @@ #tree-holder.tree-holder .file-holder .file-title - %i.fa.fa-file + = blob_icon @blob.mode, @blob.name %strong = @path %small= number_to_human_size @blob.size @@ -43,4 +43,3 @@ - blame_group[:lines].each do |line| :erb <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> - diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 373b3a0c5b0..cdac50f7a8d 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,9 +1,8 @@ .btn-group.tree-btn-group - = edit_blob_link(@project, @ref, @path) = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - - if @blob.text? + - if blob_text_viewable?(@blob) - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-sm' @@ -12,11 +11,11 @@ class: 'btn btn-sm' unless @blob.empty? = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-sm' - - if @ref != @commit.sha - = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-sm' + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, + tree_join(@commit.sha, @path)), class: 'btn btn-sm' -- if allowed_tree_edit? +- if current_user .btn-group{ role: "group" } - %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace - %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove + = edit_blob_link + = replace_blob_link + = delete_blob_link diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 42f632b38ef..2a3315da3db 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -29,10 +29,12 @@ %strong = blob.name %small - = number_to_human_size(blob.size) + = number_to_human_size(blob_size(blob)) .file-actions.hidden-xs = render "actions" - - if blob.text? + - if blob.lfs_pointer? + = render "download", blob: blob + - elsif blob.text? = render "text", blob: blob - elsif blob.image? = render "image", blob: blob diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml index f2c5e95ecf4..7908fcae3de 100644 --- a/app/views/projects/blob/_download.html.haml +++ b/app/views/projects/blob/_download.html.haml @@ -4,4 +4,4 @@ %h1.light %i.fa.fa-download %h4 - Download (#{number_to_human_size blob.size}) + Download (#{number_to_human_size blob_size(blob)}) diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f1ad0c3c403..10b02813733 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,23 +1,22 @@ -.file-holder.file - .file-title +.file-holder.file.append-bottom-default + .file-title.clearfix .editor-ref - %i.fa.fa-code-fork + = icon('code-fork') = ref %span.editor-file-name - - if @path - %span.monospace - = @path + = @path - - if current_action?(:new) || current_action?(:create) + - if current_action?(:new) || current_action?(:create) + %span.editor-file-name \/ - = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name js-quick-submit' - .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' + = text_field_tag 'file_name', params[:file_name], placeholder: "File name", + required: true, class: 'form-control new-file-name js-quick-submit' + + .pull-right + = 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] + %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} - if local_assigns[:path] .js-edit-mode-pane#preview.hide .center diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index cb1567a2e68..084608bbba3 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -5,21 +5,21 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title Create New Directory .modal-body - = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, id: 'dir-create-form', class: 'form-horizontal' do + = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do .form-group - = label_tag :dir_name, 'Directory Name', class: 'control-label' + = label_tag :dir_name, 'Directory name', class: 'control-label' .col-sm-10 - = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' - = render 'shared/commit_message_container', params: params, placeholder: '' - - unless @project.empty_repo? - .form-group - = label_tag :branch_name, 'Branch', class: 'control-label' - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" - .form-group - .col-sm-offset-2.col-sm-10 - = submit_tag "Create directory", class: 'btn btn-primary btn-create' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + = text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control' + + = render 'shared/new_commit_form', placeholder: "Add new directory" + + .form-actions + = submit_tag "Create directory", class: 'btn btn-create' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help -:coffeescript - disableButtonIfAnyEmptyField($("#dir-create-form"), ".form-control", ".btn-create"); +:javascript + new NewCommitForm($('.js-create-dir-form')) diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index cae5ff01099..1cf19a7d3db 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -3,16 +3,16 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title Remove #{@blob.name} - %p.light - From branch - %strong= @ref + %h3.page-title Delete #{@blob.name} .modal-body - = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do - = render 'shared/commit_message_container', params: params, - placeholder: 'Removed this file because...' + = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do + = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}" + .form-group .col-sm-offset-2.col-sm-10 - = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file' + = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + +:javascript + new NewCommitForm($('.js-replace-blob-form')) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index e27f1707527..676924dc6ca 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -5,7 +5,7 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title #{title} .modal-body - = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do + = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light @@ -13,19 +13,19 @@ = link_to 'click to upload', '#', class: "markdown-selector" %br .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} - = render 'shared/commit_message_container', params: params, - placeholder: placeholder - - unless @project.empty_repo? - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" - .form-group - .col-sm-offset-2.col-sm-10 - = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' - = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" -:coffeescript - disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file' - new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}') + = render 'shared/new_commit_form', placeholder: placeholder + + .form-actions + = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + + - unless can?(current_user, :push_code, @project) + .inline.prepend-left-10 + = commit_in_fork_help + + +:javascript + disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file'); + new BlobFileDropzone($('.js-upload-blob-form'), '#{method}'); + new NewCommitForm($('.js-upload-blob-form')) diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index a811adc5094..09fa148b129 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -5,28 +5,23 @@ %ul.center-top-menu.no-bottom.js-edit-mode %li.active = link_to '#editor' do - %i.fa.fa-edit - Edit file + = icon('edit') + Edit File %li = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do - %i.fa.fa-eye + = icon('eye') = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input') do + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data - = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" - - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] - = render 'projects/commit_button', ref: @ref, cancel_path: @after_edit_path + = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) :javascript blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") + new NewCommitForm($('.js-edit-blob-form')) diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 7975137c37f..167fa615182 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,21 +1,13 @@ - page_title "New File", @path.presence, @ref = render "header_title" -.gray-content-block.top-block - Create a new file +%h3.page-title + New File .file-editor - = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do = render 'projects/blob/editor', ref: @ref - = render 'shared/commit_message_container', params: params, - placeholder: 'Add new file' - - - unless @project.empty_repo? - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control js-quick-submit" + = render 'shared/new_commit_form', placeholder: "Add new file" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, @@ -23,3 +15,4 @@ :javascript blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) + new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index f52b89f6921..6988039b6c7 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -6,10 +6,8 @@ %div#tree-holder.tree-holder = render 'blob', blob: @blob -- if allowed_tree_edit? +- if can_edit_blob?(@blob) = render 'projects/blob/remove' - title = "Replace #{@blob.name}" - = render 'projects/blob/upload', title: title, placeholder: title, - button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), - method: :put + = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index cc0ec9483d2..a234536723e 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,19 +1,23 @@ - commit = @repository.commit(branch.target) +- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 +- diverging_commit_counts = @repository.diverging_commit_counts(branch) +- number_commits_behind = diverging_commit_counts[:behind] +- number_commits_ahead = diverging_commit_counts[:ahead] %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do %strong.str-truncated= branch.name - - - if branch.name == @repository.root_ref - %span.label.label-primary default - - elsif @repository.merged_to_root_ref? branch.name - %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}") - merged + + - if branch.name == @repository.root_ref + %span.label.label-primary default + - elsif @repository.merged_to_root_ref? branch.name + %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}") + merged - - if @project.protected_branch? branch.name - %span.label.label-success - %i.fa.fa-lock - protected + - if @project.protected_branch? branch.name + %span.label.label-success + %i.fa.fa-lock + protected .controls.hidden-xs - if create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do @@ -26,9 +30,20 @@ Compare - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") + - if branch.name != @repository.root_ref + .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" } + .graph-side + .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } + %span.count.count-behind= number_commits_behind + .graph-separator + .graph-side + .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } + %span.count.count-ahead= number_commits_ahead + + - if commit = render 'projects/branches/commit', commit: commit, project: @project - else diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index 68326e65d85..9fe65cbb104 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -1,5 +1,5 @@ -.branch-commit.light - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" +.branch-commit + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace" · %span.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 03ade02a0c8..204def60794 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -5,7 +5,7 @@ .pull-right - if can? current_user, :push_code, @project = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do - %i.fa.fa-add-sign + = icon('plus') New branch .dropdown.inline diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index f5577042ca4..c659af6338c 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -6,25 +6,25 @@ %button{ type: "button", class: "close", "data-dismiss" => "alert"} × = @error %h3.page-title - %i.fa.fa-code-fork - New branch -= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do + New Branch +%hr + += form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do .form-group - = label_tag :branch_name, 'Name for new branch', class: 'control-label' + = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], placeholder: 'enter new branch name', required: true, tabindex: 1, class: 'form-control' + = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' + .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control' + = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript - var availableTags = #{@project.repository.ref_names.to_json}; + var availableRefs = #{@project.repository.ref_names.to_json}; - $("#ref").autocomplete({ - source: availableTags, - minLength: 1 - }); + new NewBranchForm($('.js-create-branch-form'), availableRefs) diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml deleted file mode 100644 index 4ce4ed63b40..00000000000 --- a/app/views/projects/builds/_build.html.haml +++ /dev/null @@ -1,53 +0,0 @@ -%tr.build - %td.status - = ci_status_with_icon(build.status) - - %td.commit_status-link - - if build.target_url - = link_to build.target_url do - %strong Build ##{build.id} - - else - %strong Build ##{build.id} - - - if build.show_warning? - %i.fa.fa-warning.text-warning - - %td - = link_to build.short_sha, namespace_project_commit_path(@project.namespace, @project, build.sha) - - %td - = link_to build.ref, namespace_project_commits_path(@project.namespace, @project, build.ref) - - %td - - if build.runner - = runner_link(build.runner) - - else - .light none - - %td - = build.name - - .pull-right - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.trigger_request - %span.label.label-info triggered - - if build.allow_failure - %span.label.label-danger allowed to fail - - %td.duration - - if build.duration - #{duration_in_words(build.finished_at, build.started_at)} - - %td.timestamp - - if build.finished_at - %span #{time_ago_in_words build.finished_at} ago - - %td - .pull-right - - if current_user && can?(current_user, :manage_builds, @project) - - if build.cancel_url - = link_to build.cancel_url, title: 'Cancel' do - %i.fa.fa-remove.cred diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml new file mode 100644 index 00000000000..082dab1f5b0 --- /dev/null +++ b/app/views/projects/builds/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Builds", project_builds_path(@project)) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index e08556673ed..2fa5ad80fda 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,31 +1,34 @@ - page_title "Builds" -- header_title project_title(@project, "Builds", project_builds_path(@project)) += render "header_title" .project-issuable-filter .controls - - if @ci_project && current_user && can?(current_user, :manage_builds, @project) + - if can?(current_user, :manage_builds, @project) .pull-left.hidden-xs - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' + = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post %ul.center-top-menu %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_builds.count(:id)) + + %li{class: ('active' if @scope == 'running')} + = link_to project_builds_path(@project, scope: :running) do Running - %span.badge.js-running-count= @all_builds.running_or_pending.count(:id) + %span.badge.js-running-count + = number_with_delimiter(@all_builds.running_or_pending.count(:id)) %li{class: ('active' if @scope == 'finished')} = link_to project_builds_path(@project, scope: :finished) do Finished - %span.badge.js-running-count= @all_builds.finished.count(:id) - - %li{class: ('active' if @scope == 'all')} - = link_to project_builds_path(@project, scope: :all) do - All - %span.badge.js-totalbuilds-count= @all_builds.count(:id) + %span.badge.js-running-count + = number_with_delimiter(@all_builds.finished.count(:id)) .gray-content-block - List of #{@scope || 'running'} builds from this project + #{(@scope || 'running').capitalize} builds from this project %ul.content-list - if @builds.blank? @@ -37,17 +40,16 @@ %thead %tr %th Status - %th Build ID + %th Runner %th Commit %th Ref - %th Runner + %th Stage %th Name %th Duration %th Finished at %th - @builds.each do |build| - = render 'projects/builds/build', build: build - - = paginate @builds + = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true + = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c45bfb27b8f..5b7ecce86ab 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,14 +1,20 @@ +- page_title "#{@build.name} (##{@build.id})", "Builds" += render "header_title" + .build-page - .gray-content-block - Build for commit - %strong.monospace - = link_to @build.commit.short_sha, ci_status_path(@build.commit) + .gray-content-block.top-block + Build ##{@build.id} for commit + %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) from - %code #{@build.ref} + = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) + - merge_request = @build.merge_request + - if merge_request + via + = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request) #up-build-trace - if @commit.matrix_for_ref?(@build.ref) - %ul.center-top-menu.build-top-menu + %ul.center-top-menu.no-top.no-bottom - @commit.latest_builds_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do @@ -19,8 +25,7 @@ - else = build.id - - - unless @commit.latest_builds_for_ref(@build.ref).include?(@build) + - if @build.retried? %li.active %a Build ##{@build.id} @@ -28,7 +33,7 @@ %i.fa.fa-warning This build was retried. - .gray-content-block.second-block + .gray-content-block.middle-block .build-head .clearfix = ci_status_with_icon(@build.status) @@ -37,7 +42,7 @@ %i.fa.fa-time #{duration_in_words(@build.finished_at, @build.started_at)} .pull-right - = @build.updated_at.stamp('19:00 Aug 27') + #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at} - if @build.show_warning? - unless @build.any_runners_online? @@ -55,7 +60,7 @@ %br Go to - = link_to namespace_project_runners_path(@build.gl_project.namespace, @build.gl_project) do + = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page .row.prepend-top-default @@ -84,16 +89,19 @@ Test coverage %h1 #{@build.coverage}% + - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url + .build-widget.center + = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary' .build-widget %h4.title - Build + Build ##{@build.id} - if current_user && can?(current_user, :manage_builds, @project) .pull-right - - if @build.active? - = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger' - - elsif @build.commands.present? - = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post + - if @build.cancel_url + = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post + - elsif @build.retry_url + = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post - if @build.duration %p @@ -101,15 +109,15 @@ #{duration_in_words(@build.finished_at, @build.started_at)} %p %span.attr-name Created: - #{time_ago_in_words(@build.created_at)} ago + #{time_ago_with_tooltip(@build.created_at)} - if @build.finished_at %p %span.attr-name Finished: - #{time_ago_in_words(@build.finished_at)} ago + #{time_ago_with_tooltip(@build.finished_at)} %p %span.attr-name Runner: - if @build.runner && current_user && current_user.admin - \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)} + = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) - elsif @build.runner \##{@build.runner.id} @@ -134,10 +142,11 @@ %h4.title Commit .pull-right - %small #{build_commit_link @build} + %small + = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %p %span.attr-name Branch: - #{build_ref_link @build} + = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) %p %span.attr-name Author: #{@build.commit.git_author_name} @@ -155,14 +164,16 @@ - if @builds.present? .build-widget - %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: + %h4.title #{pluralize(@builds.count(:id), "other build")} for + = succeed ":" do + = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %table.table.builds - @builds.each_with_index do |build, i| %tr.build %td = ci_icon_for_status(build.status) %td - = link_to namespace_project_build_path(@project.namespace, @project, @build) do + = link_to namespace_project_build_path(@project.namespace, @project, build) do - if build.name = build.name - else @@ -171,8 +182,5 @@ %td.status= build.status - = paginate @builds - - :javascript - new CiBuild("#{namespace_project_build_path(@project.namespace, @project, @build)}", "#{@build.status}") + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml new file mode 100644 index 00000000000..14ee2263b7d --- /dev/null +++ b/app/views/projects/buttons/_download.html.haml @@ -0,0 +1,4 @@ +- unless @project.empty_repo? + - if can? current_user, :download_code, @project + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do + = icon('download') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 4580c912692..f9ab78e7874 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -5,12 +5,13 @@ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can?(current_user, :create_issue, @project) %li - = link_to url_for_new_issue do + = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - if can?(current_user, :create_merge_request, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project %li - = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') New merge request - if can?(current_user, :create_snippet, @project) @@ -18,9 +19,14 @@ = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') New snippet + - if can?(current_user, :push_code, @project) %li.divider %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do + = icon('file fw') + New file + %li = link_to new_namespace_project_branch_path(@project.namespace, @project) do = icon('code-fork fw') New branch @@ -28,5 +34,20 @@ = link_to new_namespace_project_tag_path(@project.namespace, @project) do = icon('tags fw') New tag - - + - elsif current_user && current_user.already_forked?(@project) + %li.divider + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do + = icon('file fw') + New file + - elsif can?(current_user, :fork_project, @project) + %li.divider + %li + - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('file fw') + New file diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 8f2f631eb7d..133531887a2 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,12 +1,18 @@ -- if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do - = icon('code-fork fw') - Fork - %span.count - = @project.forks_count - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do - = icon('code-fork fw') - %span.count - = @project.forks_count +- unless @project.empty_repo? + - if current_user && can?(current_user, :fork_project, @project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do + = icon('code-fork fw') + Fork + %div.count-with-arrow + %span.arrow + %span.count + = @project.forks_count + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do + = icon('code-fork fw') + Fork + %div.count-with-arrow + %span.arrow + %span.count + = @project.forks_count diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 0c298844912..3e83ec3912f 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -5,7 +5,7 @@ = hidden_field_tag :notification_id, @membership.id = hidden_field_tag :notification_level %span.dropdown - %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') = notification_label(@membership) = icon('angle-down') @@ -14,7 +14,7 @@ = notification_list_item(level, @membership) - when GroupMember - .btn.btn-new.disabled.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} + .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} = icon('bell') = notification_label(@membership) = icon('angle-down') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 3501dddefbe..21ba426aaa1 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,17 +1,21 @@ - if current_user - = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do - = icon('star fw') - %span.count + = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do + - if current_user.starred?(@project) + = icon('star fw') + %span.starred Unstar + - else + = icon('star-o fw') + %span Star + %div.count-with-arrow + %span.arrow + %span.count.star-count = @project.star_count - :coffeescript - $('.project-home-panel .toggle-star').on 'ajax:success', (e, data, status, xhr) -> - $(@).replaceWith(data.html) - .on 'ajax:error', (e, xhr, status, error) -> - new Flash('Star toggle failed. Try again later.', 'alert') - - else = link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do = icon('star fw') + Star + %div.count-with-arrow + %span.arrow %span.count = @project.star_count diff --git a/app/views/projects/ci_services/_form.html.haml b/app/views/projects/ci_services/_form.html.haml deleted file mode 100644 index 397832e56db..00000000000 --- a/app/views/projects/ci_services/_form.html.haml +++ /dev/null @@ -1,54 +0,0 @@ -%h3.page-title - = @service.title - = boolean_to_icon @service.activated? - -%p= @service.description - - -%hr - -= form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - - if @service.errors.any? - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg - - - if @service.help.present? - .bs-callout - = @service.help - - .form-group - = f.label :active, "Active", class: "control-label" - .col-sm-10 - = f.check_box :active - - - @service.fields.each do |field| - - name = field[:name] - - label = field[:label] || name - - value = @service.send(name) - - type = field[:type] - - placeholder = field[:placeholder] - - choices = field[:choices] - - default_choice = field[:default_choice] - - help = field[:help] - - .form-group - = f.label label, class: "control-label" - .col-sm-10 - - if type == 'text' - = f.text_field name, class: "form-control", placeholder: placeholder - - elsif type == 'textarea' - = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder - - elsif type == 'checkbox' - = f.check_box name - - elsif type == 'select' - = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - - if help - .light #{help} - - .form-actions - = f.submit 'Save', class: 'btn btn-save' - - - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn' diff --git a/app/views/projects/ci_services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml deleted file mode 100644 index bcc5832792f..00000000000 --- a/app/views/projects/ci_services/edit.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'form' diff --git a/app/views/projects/ci_services/index.html.haml b/app/views/projects/ci_services/index.html.haml deleted file mode 100644 index c164b2d4bc0..00000000000 --- a/app/views/projects/ci_services/index.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -%h3.page-title Project services -%p.light Project services allow you to integrate GitLab CI with other applications - -%table.table - %thead - %tr - %th - %th Service - %th Description - %th Last edit - - @services.sort_by(&:title).each do |service| - %tr - %td - = boolean_to_icon service.activated? - %td - = link_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) do - %strong= service.title - %td - = service.description - %td.light - = time_ago_in_words service.updated_at - ago diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml deleted file mode 100644 index d711413c6b9..00000000000 --- a/app/views/projects/ci_settings/_form.html.haml +++ /dev/null @@ -1,119 +0,0 @@ -%h3.page-title - CI settings -%hr -.bs-callout.help-callout - %p - If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path} - %p - Edit your - #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)} - -- unless @project.empty_repo? - %p - Paste build status image for #{@repository.root_ref} with next link - = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do - Status Badge - .badge-codes-block.bs-callout.bs-callout-info.hide - %p - Status badge for - %span.label.label-info #{@ref} - branch - %div - %label Markdown: - = text_field_tag 'badge_md', markdown_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' - %label Html: - = text_field_tag 'badge_html', html_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control' - -= nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - - if @ci_project.errors.any? - #error_explanation - %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:" - .alert.alert-error - %ul - - @ci_project.errors.full_messages.each do |msg| - %li= msg - - %fieldset - %legend Build settings - .form-group - = label_tag nil, class: 'control-label' do - Get code - .col-sm-10 - %p Get recent application code using the following command: - .radio - = label_tag do - = f.radio_button :allow_git_fetch, 'false' - %strong git clone - .light Slower but makes sure you have a clean dir before every build - .radio - = label_tag do - = f.radio_button :allow_git_fetch, 'true' - %strong git fetch - .light Faster - .form-group - = f.label :timeout_in_minutes, 'Timeout', class: 'control-label' - .col-sm-10 - = f.number_field :timeout_in_minutes, class: 'form-control', min: '0' - .light per build in minutes - - - %fieldset - %legend Build Schedule - .form-group - = f.label :always_build, 'Schedule build', class: 'control-label' - .col-sm-10 - .checkbox - = f.label :always_build do - = f.check_box :always_build - %span.light Repeat last build after X hours if no builds - .form-group - = f.label :polling_interval, "Build interval", class: 'control-label' - .col-sm-10 - = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control' - .light In hours - - %fieldset - %legend Project settings - .form-group - = f.label :default_ref, "Make tabs for the following branches", class: 'control-label' - .col-sm-10 - = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable' - .light You will be able to filter builds by the following branches - .form-group - = f.label :public, 'Public mode', class: 'control-label' - .col-sm-10 - .checkbox - = f.label :public do - = f.check_box :public - %span.light Anyone can see project and builds - .form-group - = f.label :coverage_regex, "Test coverage parsing", class: 'control-label' - .col-sm-10 - .input-group - %span.input-group-addon / - = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' - %span.input-group-addon / - .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature - .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: - %ul - %li - Simplecov (Ruby) - - %code \(\d+.\d+\%\) covered - %li - pytest-cov (Python) - - %code \d+\%$ - - - - %fieldset - %legend Advanced settings - .form-group - = f.label :token, "CI token", class: 'control-label' - .col-sm-10 - = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89' - - .form-actions - = f.submit 'Save changes', class: 'btn btn-save' - - unless @ci_project.new_record? - = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' diff --git a/app/views/projects/ci_settings/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml deleted file mode 100644 index 33038c52978..00000000000 --- a/app/views/projects/ci_settings/_no_runners.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.alert.alert-danger - %p - There are NO runners to build this project. - %br - You can add Specific runner for this project on Runners page - - - if current_user.admin - or add Shared runner for whole application in admin are. diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml deleted file mode 100644 index eedf484bf00..00000000000 --- a/app/views/projects/ci_settings/edit.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -- if @ci_project.generated_yaml_config - %p.alert.alert-danger - CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_project)} - or - %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview - yaml file which is based on your old jobs. - Put this file to the root of your project and name it .gitlab-ci.yml - -- if no_runners_for_project?(@ci_project) - = render 'no_runners' - -= render 'form' - -- if @ci_project.generated_yaml_config - #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"} - .modal-dialog - .modal-content - .modal-header - %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} × - %h4.modal-title Content of .gitlab-ci.yml - .modal-body - = text_area_tag :yaml, @ci_project.generated_yaml_config, size: "70x25", class: "form-control" - .modal-footer - %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close diff --git a/app/views/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml deleted file mode 100644 index 369086b39ed..00000000000 --- a/app/views/projects/ci_web_hooks/index.html.haml +++ /dev/null @@ -1,93 +0,0 @@ -%h3.page-title - CI Web hooks - -%p.light - Web Hooks can be used for binding events when build completed. - -%hr.clearfix - -= form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - -if @web_hook.errors.any? - .alert.alert-danger - - @web_hook.errors.full_messages.each do |msg| - %p= msg - .form-group - = f.label :url, "URL", class: 'control-label' - .col-sm-10 - = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' - .form-actions - = f.submit "Add Web Hook", class: "btn btn-create" - --if @web_hooks.any? - %h4 Activated web hooks (#{@web_hooks.count}) - .table-holder - %table.table - - @web_hooks.each do |hook| - %tr - %td - .clearfix - %span.monospace= hook.url - %td - .pull-right - - if @ci_project.commits.any? - = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" - = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - -%h4 Web Hook data example - -:erb - <pre> - <code> - { - "build_id": 2, - "build_name":"rspec_linux" - "build_status": "failed", - "build_started_at": "2014-05-05T18:01:02.563Z", - "build_finished_at": "2014-05-05T18:01:07.611Z", - "project_id": 1, - "project_name": "Brightbox \/ Brightbox Cli", - "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli", - "ref": "master", - "sha": "a26cf5de9ed9827746d4970872376b10d9325f40", - "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c", - "push_data": { - "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c", - "after": "a26cf5de9ed9827746d4970872376b10d9325f40", - "ref": "refs\/heads\/master", - "user_id": 1, - "user_name": "Administrator", - "project_id": 5, - "repository": { - "name": "Brightbox Cli", - "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git", - "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.", - "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli" - }, - "commits": [ - { - "id": "a26cf5de9ed9827746d4970872376b10d9325f40", - "message": "Release v1.2.2", - "timestamp": "2014-04-22T16:46:42+03:00", - "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40", - "author": { - "name": "Paul Thornthwaite", - "email": "tokengeek@gmail.com" - } - }, - { - "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c", - "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.", - "timestamp": "2014-04-11T18:17:26+03:00", - "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c", - "author": { - "name": "Paul Thornthwaite", - "email": "tokengeek@gmail.com" - } - } - ], - "total_commits_count": 2, - "ci_yaml_file":"rspec_linux:\r\n script: ls\r\n" - } - } - </code> - </pre> diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml new file mode 100644 index 00000000000..329aaa0bb8b --- /dev/null +++ b/app/views/projects/commit/_builds.html.haml @@ -0,0 +1,68 @@ +.gray-content-block.middle-block + .pull-right + - if can?(current_user, :manage_builds, @ci_commit.project) + - if @ci_commit.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + + - if @ci_commit.builds.running_or_pending.any? + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline + = pluralize @statuses.count(:id), "build" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace" + - if @ci_commit.duration > 0 + in + = time_interval_in_words @ci_commit.duration + +- if @ci_commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @ci_commit.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + +- if @ci_commit.project.builds_enabled? && !@ci_commit.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_commit.project.build_coverage_enabled? + %th Coverage + %th + - @ci_commit.refs.each do |ref| + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, + locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true } + +- if @ci_commit.retried.any? + .gray-content-block.second-block + Retried builds + + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_commit.project.build_coverage_enabled? + %th Coverage + %th + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, + locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true } diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index a634ae5dfda..f74f8b427ec 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -1,7 +1,9 @@ -%ul.center-top-menu.commit-ci-menu +%ul.center-top-menu.no-top.no-bottom.commit-ci-menu = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes - = nav_link(path: 'commit#ci') do - = link_to ci_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + %span.badge= @diffs.count + = nav_link(path: 'commit#builds') do + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Builds + %span.badge= @statuses.count diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index fbf0a9ec0c3..ddb77fd796b 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -13,13 +13,15 @@ - unless @commit.parents.length > 1 %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) - = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do - %span Browse Code » + = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do + = icon('files-o') + Browse Files %div %p %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit) + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + = clipboard_button(clipboard_text: @commit.id) .commit-info-row %span.light Authored by %strong @@ -36,24 +38,24 @@ .commit-info-row %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| - = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent) + = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" - if @ci_commit .pull-right = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do = ci_status_icon(@ci_commit) build: - = @ci_commit.status + = ci_status_label(@ci_commit) .commit-info-row.branches %i.fa.fa-spinner.fa-spin .commit-box.gray-content-block.middle-block %h3.commit-title - = gfm escape_once(@commit.title) + = markdown escape_once(@commit.title), pipeline: :single_line - if @commit.description.present? %pre.commit-description - = preserve(gfm(escape_once(@commit.description))) + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) -:coffeescript - $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") +:javascript + $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml new file mode 100644 index 00000000000..99d62503a94 --- /dev/null +++ b/app/views/projects/commit/builds.html.haml @@ -0,0 +1,6 @@ +- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" += render "projects/commits/header_title" += render "commit_box" += render "ci_menu" + += render "builds" diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml deleted file mode 100644 index 43033cad24c..00000000000 --- a/app/views/projects/commit/ci.html.haml +++ /dev/null @@ -1,69 +0,0 @@ -- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/header_title" -= render "commit_box" -= render "ci_menu" - - -- if @ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - @ci_commit.yaml_errors.split(",").each do |error| - %li= error - -- unless @ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.gray-content-block.second-block - Latest builds - - .pull-right - - if @ci_commit.duration > 0 - %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration} - - - - - if @ci_project && current_user && can?(current_user, :manage_builds, @project) - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel all", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger' - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - - @ci_commit.refs.each do |ref| - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_project.try(:coverage_enabled?), allow_retry: true } - -- if @ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_project.try(:coverage_enabled?) } diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 30a3973828f..069b8b1f169 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,6 +1,9 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" = render "projects/commits/header_title" = render "commit_box" -= render "ci_menu" if @ci_commit +- if @ci_commit + = render "ci_menu" +- else + %div.block-connector = render "projects/diffs/diffs", diffs: @diffs, project: @project -= render "projects/notes/notes_with_form", view: params[:view] += render "projects/notes/notes_with_form" diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 637154f56aa..74a05df24d3 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -1,25 +1,46 @@ %tr.commit_status %td.status - = ci_status_with_icon(commit_status.status) + - if commit_status.target_url + = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do + = ci_icon_for_status(commit_status.status) + = commit_status.status + - else + = ci_status_with_icon(commit_status.status) %td.commit_status-link - if commit_status.target_url = link_to commit_status.target_url do - %strong Build ##{commit_status.id} + %strong ##{commit_status.id} - else - %strong Build ##{commit_status.id} + %strong ##{commit_status.id} - if commit_status.show_warning? %i.fa.fa-warning.text-warning + - if defined?(commit_sha) && commit_sha + %td + = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace" + %td - = commit_status.ref + - if commit_status.ref + = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) + - else + .light none - %td - = commit_status.stage + - if defined?(runner) && runner + %td + - if commit_status.try(:runner) + = runner_link(commit_status.runner) + - else + .light none + + - if defined?(stage) && stage + %td + = commit_status.stage %td = commit_status.name + .pull-right - if commit_status.tags.any? - commit_status.tags.each do |tag| @@ -36,7 +57,7 @@ %td.timestamp - if commit_status.finished_at - %span #{time_ago_in_words commit_status.finished_at} ago + %span #{time_ago_with_tooltip(commit_status.finished_at)} - if defined?(coverage) && coverage %td.coverage @@ -45,10 +66,14 @@ %td .pull-right - - if current_user && can?(current_user, :manage_builds, commit_status.gl_project) - - if commit_status.cancel_url - = link_to commit_status.cancel_url, title: 'Cancel' do - %i.fa.fa-remove.cred + - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url + = link_to commit_status.download_url, title: 'Download artifacts' do + %i.fa.fa-download + - if current_user && can?(current_user, :manage_builds, commit_status.project) + - if commit_status.active? + - if commit_status.cancel_url + = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do + %i.fa.fa-remove.cred - elsif defined?(allow_retry) && allow_retry && commit_status.retry_url = link_to commit_status.retry_url, method: :post, title: 'Retry' do %i.fa.fa-repeat diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index cddd5aa3a83..012825f0fdb 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -5,11 +5,11 @@ - note_count = notes.user.count - ci_commit = project.ci_commit(commit.sha) -- cache_key = [project.path_with_namespace, commit.id, note_count] +- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key.push(ci_commit.status) if ci_commit = cache(cache_key) do - %li.commit.js-toggle-container + %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } .commit-row-title %strong.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" @@ -18,9 +18,9 @@ .pull-right - if ci_commit - = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do - = ci_status_icon(ci_commit) + = render_ci_status(ci_commit) + = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" .notes_count @@ -32,7 +32,7 @@ - if commit.description? .commit-row-description.js-toggle-content %pre - = preserve(gfm(escape_once(commit.description))) + = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) .commit-row-info = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index a849bf84698..fcccb002d7e 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -3,6 +3,11 @@ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits %span.badge= number_with_delimiter(@repository.commit_count) + + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network + = nav_link(controller: :compare) do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do Compare @@ -12,7 +17,7 @@ Branches %span.badge.js-totalbranch-count= @repository.branches.size - = nav_link(controller: :tags) do + = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags %span.badge.js-totaltags-count= @repository.tags.length diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 3854ad5d611..7ffa7317196 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -12,12 +12,12 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) xml.title truncate(commit.title, length: 80) xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email) + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) xml.author do |author| xml.name commit.author_name xml.email commit.author_email end - xml.summary gfm(commit.description) + xml.summary markdown(commit.description, pipeline: :single_line) end end end diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 39755efd2fd..51088a7dea8 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -7,11 +7,11 @@ = render "form" - if @commits.present? - .prepend-top-20 + .prepend-top-default = render "projects/commits/commit_list" = render "projects/diffs/diffs", diffs: @diffs, project: @project - else - .light-well.prepend-top-20 + .light-well.prepend-top-default .center %h4 There isn't anything to compare. diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 91675b3738e..5e182af2669 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,5 +1,5 @@ %div - = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| -if @key.errors.any? .alert.alert-danger %ul @@ -8,16 +8,15 @@ .form-group = f.label :title, class: "control-label" - .col-sm-10= f.text_field :title, class: 'form-control' + .col-sm-10= f.text_field :title, class: 'form-control', autofocus: true, required: true .form-group = f.label :key, class: "control-label" .col-sm-10 %p.light Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh", "README") - = f.text_area :key, class: "form-control thin_area", rows: 5 + = f.text_area :key, class: "form-control thin_area", rows: 5, required: true .form-actions - = f.submit 'Create', class: "btn-create btn" + = f.submit 'Create Deploy Key', class: "btn-create btn" = link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel" - diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml index 01c810aee18..01fab3008a7 100644 --- a/app/views/projects/deploy_keys/new.html.haml +++ b/app/views/projects/deploy_keys/new.html.haml @@ -1,5 +1,5 @@ - page_title "New Deploy Key" -%h3.page-title New Deploy key +%h3.page-title New Deploy Key %hr = render 'form' diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 56b51f038ba..f9d661d59d2 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,9 +1,9 @@ -- if params[:view] == 'parallel' +- if diff_view == 'parallel' - fluid_layout true - diff_files = safe_diff_files(diffs) -.gray-content-block.second-block +.gray-content-block.middle-block.oneline-block .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 410ff6abb43..517f6aef7c5 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -2,22 +2,29 @@ .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"} - if diff_file.diff.submodule? %span - - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) - = submodule_link(submodule_item, @commit.id, project.repository) + = icon('archive fw') + %strong + = submodule_link(blob, @commit.id, project.repository) - else %span + = blob_icon blob.mode, blob.name + = link_to "#diff-#{i}" do + %strong + = diff_file.new_path + - if diff_file.deleted_file - = "#{diff_file.old_path} deleted" + deleted - elsif diff_file.renamed_file - = "#{diff_file.old_path} renamed to #{diff_file.new_path}" - - else - = diff_file.new_path + renamed from + %strong + = diff_file.old_path - if diff_file.mode_changed? - %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + %small + = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-controls - - if blob.text? + - if blob_text_viewable?(blob) = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments @@ -25,15 +32,16 @@ - if editable_diff?(diff_file) = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, - after: ' ', from_merge_request_id: @merge_request.id) + from_merge_request_id: @merge_request.id) + = view_file_btn(diff_commit.id, diff_file, project) .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - - if blob.text? - - if params[:view] == 'parallel' + - if blob_text_viewable?(blob) + - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else = render "projects/diffs/text_file", diff_file: diff_file, index: i @@ -42,4 +50,3 @@ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i - else .nothing-here-block No preview for this file type - diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8e49299223c..ef758b5fb7a 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -14,7 +14,7 @@ = f.label :name, class: 'control-label' do Project name .col-sm-10 - = f.text_field :name, placeholder: "Example Project", class: "form-control", id: "project_name_edit" + = f.text_field :name, class: "form-control", id: "project_name_edit" .form-group @@ -22,12 +22,12 @@ Project description %span.light (optional) .col-sm-10 - = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250 + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - - if @project.repository.exists? && @project.repository.branch_names.any? + - unless @project.empty_repo? .form-group = f.label :default_branch, "Default Branch", class: 'control-label' - .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'}) + .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project @@ -35,7 +35,7 @@ .form-group = f.label :tag_list, "Tags", class: 'control-label' .col-sm-10 - = f.text_field :tag_list, maxlength: 2000, class: "form-control" + = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" %p.help-block Separate tags with commas. %fieldset.features @@ -57,7 +57,16 @@ = f.check_box :merge_requests_enabled %strong Merge Requests %br - %span.descr Submit changes to be merged upstream. + %span.descr Submit changes to be merged upstream + + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :builds_enabled do + = f.check_box :builds_enabled + %strong Builds + %br + %span.descr Test and deploy your changes before merge .form-group .col-sm-offset-2.col-sm-10 @@ -103,6 +112,62 @@ %hr = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + %fieldset.features + %legend + Continuous Integration + .form-group + .col-sm-offset-2.col-sm-10 + %p Get recent application code using the following command: + .radio + = f.label :build_allow_git_fetch do + = f.radio_button :build_allow_git_fetch, 'false' + %strong git clone + %br + %span.descr Slower but makes sure you have a clean dir before every build + .radio + = f.label :build_allow_git_fetch do + = f.radio_button :build_allow_git_fetch, 'true' + %strong git fetch + %br + %span.descr Faster + .form-group + = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' + .col-sm-10 + = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' + %p.help-block per build in minutes + .form-group + = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label' + .col-sm-10 + .input-group + %span.input-group-addon / + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + %p.help-block + We will use this regular expression to find test coverage output in build trace. + Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%\s*$ + %li + phpunit --coverage-text --colors=never (PHP) - + %code ^\s*Lines:\s*\d+.\d+\% + + + %fieldset.features + %legend + Advanced settings + .form-group + = f.label :runners_token, "CI token", class: 'control-label' + .col-sm-10 + = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' + %p.help-block The secure token used to checkout project. + .form-actions = f.submit 'Save changes', class: "btn btn-save" @@ -121,9 +186,11 @@ The project can be committed to. %br %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive', unarchive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-success" + + .form-actions + = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" - else .panel.panel-warning .panel-heading @@ -135,9 +202,11 @@ It is hidden from the dashboard and doesn't show up in searches. %br %strong Archived projects cannot be committed to! - = link_to 'Archive', archive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" + + .form-actions + = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" - else .nothing-here-block Only the project owner can archive a project @@ -162,7 +231,7 @@ Project name .col-sm-9 .form-group - = f.text_field :name, placeholder: "Example Project", class: "form-control" + = f.text_field :name, class: "form-control" .form-group = f.label :path, class: 'control-label' do %span Path @@ -172,12 +241,11 @@ .input-group-addon #{URI.join(root_url, @project.namespace.path)}/ = f.text_field :path, class: 'form-control' - %span.input-group-addon .git %ul %li Be careful. Renaming a project's repository can have unintended side effects. %li You will need to update your local repositories to point to the new location. .form-actions - = f.submit 'Rename', class: "btn btn-warning" + = f.submit 'Rename project', class: "btn btn-warning" - if can?(current_user, :change_namespace, @project) .panel.panel-default.panel.panel-danger @@ -196,7 +264,7 @@ %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. .form-actions - = f.submit 'Transfer', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - else .nothing-here-block Only the project owner can transfer a project @@ -211,7 +279,8 @@ #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}. %br %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + .form-actions + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - else .nothing-here-block Only the project owner can remove the fork relationship. @@ -224,8 +293,8 @@ Removing the project will delete its repository and all related resources including issues, merge requests etc. %br %strong Removed projects cannot be restored! - - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + .form-actions + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } - else .nothing-here-block Only the project owner can remove a project. diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index e06454fd148..503d156661e 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,54 +1,56 @@ -.alert_holder += content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' - + = render "home_panel" -.gray-content-block.center +.gray-content-block.second-block.center %h3.page-title The repository for this project is empty - %p - If you already have files you can push them using command line instructions below. - %br - Otherwise you can start with - = link_to "adding README", new_readme_path, class: 'underlined-link' - file to this project. + - if can?(current_user, :push_code, @project) + %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' + file to this project. -.prepend-top-20 -.empty_wrapper - %h3.page-title-empty - Command line instructions - %div.git-empty - %fieldset - %h5 Git global setup - %pre.light-well - :preserve - git config --global user.name "#{h git_user_name}" - git config --global user.email "#{h git_user_email}" +- if can?(current_user, :download_code, @project) + .prepend-top-20 + .empty_wrapper + %h3.page-title-empty + Command line instructions + %div.git-empty + %fieldset + %h5 Git global setup + %pre.light-well + :preserve + git config --global user.name "#{h git_user_name}" + git config --global user.email "#{h git_user_email}" - %fieldset - %h5 Create a new repository - %pre.light-well - :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} - cd #{h @project.path} - touch README.md - git add README.md - git commit -m "add README" - git push -u origin master + %fieldset + %h5 Create a new repository + %pre.light-well + :preserve + git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + cd #{h @project.path} + touch README.md + git add README.md + git commit -m "add README" + git push -u origin master - %fieldset - %h5 Existing folder or Git repository - %pre.light-well - :preserve - cd existing_folder - git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} - git add . - git commit - git push -u origin master + %fieldset + %h5 Existing folder or Git repository + %pre.light-well + :preserve + cd existing_folder + git init + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit + git push -u origin master - - if can? current_user, :remove_project, @project - .prepend-top-20 - = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + - if can? current_user, :remove_project, @project + .prepend-top-20 + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index f0b0a11c04a..8a2c027a455 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -43,4 +43,3 @@ %i.fa.fa-spinner.fa-spin Forking repository %p Please wait a moment, this page will automatically refresh when ready. - diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index bbfaf422a82..a47643bd09c 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,9 +1,11 @@ -%ul.nav.nav-tabs +%ul.center-top-menu = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do = link_to 'Commits', commits_namespace_project_graph_path - - if @project.gitlab_ci? + = nav_link(action: :languages) do + = link_to 'Languages', languages_namespace_project_graph_path + - if @project.builds_enabled? = nav_link(action: :ci) do = link_to ci_namespace_project_graph_path do Continuous Integration diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 4f69cc64f7c..6fa77cc10c6 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,7 +1,16 @@ - page_title "Continuous Integration", "Graphs" = render "header_title" = render 'head' +.gray-content-block.append-bottom-default + .oneline + A collection of graphs for Continuous Integration + #charts.ci-charts + .row + .col-md-6 + = render 'projects/graphs/ci/overall' + .col-md-6 + = render 'projects/graphs/ci/build_times' + + %hr = render 'projects/graphs/ci/builds' - = render 'projects/graphs/ci/build_times' -= render 'projects/graphs/ci/overall' diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml index c3c2f572414..c58223fd39e 100644 --- a/app/views/projects/graphs/ci/_build_times.haml +++ b/app/views/projects/graphs/ci/_build_times.haml @@ -1,21 +1,22 @@ -%fieldset - %legend +%div + %p.light Commit duration in minutes for last 30 commits - %canvas#build_timesChart.padded{width: 800, height: 300} + %canvas#build_timesChart{height: 200} :javascript var data = { labels : #{@charts[:build_times].labels.to_json}, datasets : [ { - fillColor : "#4A3", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", - pointStrokeColor : "#fff", + fillColor : "rgba(220,220,220,0.5)", + strokeColor : "rgba(220,220,220,1)", + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, data : #{@charts[:build_times].build_times.to_json} } ] } var ctx = $("#build_timesChart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true}); + new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 1b0039fb834..8fca07114fa 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -1,20 +1,30 @@ -%fieldset - %legend - Builds chart for last week - (#{date_from_to(Date.today - 7.days, Date.today)}) +%h4 Build charts +%p + + %span.cgreen + = icon("circle") + success + + %span.cgray + = icon("circle") + all - %canvas#weekChart.padded{width: 800, height: 200} +.prepend-top-default + %p.light + Builds for last week + (#{date_from_to(Date.today - 7.days, Date.today)}) + %canvas#weekChart{height: 200} -%fieldset - %legend - Builds chart for last month +.prepend-top-default + %p.light + Builds for last month (#{date_from_to(Date.today - 30.days, Date.today)}) + %canvas#monthChart{height: 200} - %canvas#monthChart.padded{width: 800, height: 300} - -%fieldset - %legend Builds chart for last year - %canvas#yearChart.padded{width: 800, height: 400} +.prepend-top-default + %p.light + Builds for last year + %canvas#yearChart.padded{height: 250} - [:week, :month, :year].each do |scope| :javascript @@ -22,20 +32,20 @@ labels : #{@charts[scope].labels.to_json}, datasets : [ { - fillColor : "rgba(220,220,220,0.5)", - strokeColor : "rgba(220,220,220,1)", - pointColor : "rgba(220,220,220,1)", + fillColor : "#7f8fa4", + strokeColor : "#7f8fa4", + pointColor : "#7f8fa4", pointStrokeColor : "#EEE", data : #{@charts[scope].total.to_json} }, { - fillColor : "#4A3", - strokeColor : "rgba(151,187,205,1)", - pointColor : "rgba(151,187,205,1)", + fillColor : "#44aa22", + strokeColor : "#44aa22", + pointColor : "#44aa22", pointStrokeColor : "#fff", data : #{@charts[scope].success.to_json} } ] } var ctx = $("##{scope}Chart").get(0).getContext("2d"); - new Chart(ctx).Line(data,{"scaleOverlay": true}); + new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false}); diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml index 9550d719471..4b12e5f2da1 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/graphs/ci/_overall.haml @@ -1,22 +1,19 @@ -- ci_project = @project.gitlab_ci_project -%fieldset - %legend Overall - %p +%h4 Overall stats +%ul + %li Total: - %strong= pluralize ci_project.builds.count(:all), 'build' - %p + %strong= pluralize @project.builds.count(:all), 'build' + %li Successful: - %strong= pluralize ci_project.builds.success.count(:all), 'build' - %p + %strong= pluralize @project.builds.success.count(:all), 'build' + %li Failed: - %strong= pluralize ci_project.builds.failed.count(:all), 'build' - - %p + %strong= pluralize @project.builds.failed.count(:all), 'build' + %li Success ratio: %strong - #{success_ratio(ci_project.builds.success, ci_project.builds.failed)}% - - %p + #{success_ratio(@project.builds.success, @project.builds.failed)}% + %li Commits covered: %strong - = ci_project.commits.count(:all) + = @project.ci_commits.count(:all) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 112be875b6b..fc465ab273b 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,9 +1,13 @@ - page_title "Commits", "Graphs" = render "header_title" -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs_commits' = render 'head' +.gray-content-block.append-bottom-default + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs_commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + %p.lead Commit statistics for %strong #{@ref} @@ -45,26 +49,24 @@ Commits per weekday %canvas#weekday-chart -:coffeescript - responsiveChart = (selector, data) -> - options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false } - - # get selector by context - ctx = selector.get(0).getContext("2d") - # pointing parent container to make chart.js inherit its width - container = $(selector).parent() - - generateChart = -> - selector.attr('width', $(container).width()) - new Chart(ctx).Bar(data, options) - - # enabling auto-resizing - $(window).resize( generateChart ) - - generateChart() +:javascript + var responsiveChart = function (selector, data) { + var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }; + // get selector by context + var ctx = selector.get(0).getContext("2d"); + // pointing parent container to make chart.js inherit its width + var container = $(selector).parent(); + var generateChart = function() { + selector.attr('width', $(container).width()); + return new Chart(ctx).Bar(data, options); + }; + // enabling auto-resizing + $(window).resize(generateChart); + return generateChart(); + }; - chartData = (keys, values) -> - data = { + var chartData = function (keys, values) { + var data = { labels : keys, datasets : [{ fillColor : "rgba(220,220,220,0.5)", @@ -74,13 +76,15 @@ barDatasetSpacing: 1, data : values }] - } + }; + return data; + }; - hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}) - responsiveChart($('#hour-chart'), hourData) + var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json}); + responsiveChart($('#hour-chart'), hourData); - dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}) - responsiveChart($('#weekday-chart'), dayData) + var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json}); + responsiveChart($('#weekday-chart'), dayData); - monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}) - responsiveChart($('#month-chart'), monthData) + var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json}); + responsiveChart($('#month-chart'), monthData); diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml new file mode 100644 index 00000000000..a7fab5b6d72 --- /dev/null +++ b/app/views/projects/graphs/languages.html.haml @@ -0,0 +1,32 @@ +- page_title "Languages", "Graphs" += render "header_title" += render 'head' + +.gray-content-block.append-bottom-default + .oneline + Programming languages used in this repository + +.row + .col-md-8 + %canvas#languages-chart{ height: 400 } + .col-md-4 + %ul.bordered-list + - @languages.each do |language| + %li + %span{ style: "color: #{language[:color]}" } + = icon('circle') + + = language[:label] + .pull-right + = language[:value] + \% + +:javascript + var data = #{@languages.to_json}; + var ctx = $("#languages-chart").get(0).getContext("2d"); + var options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false + } + var myPieChart = new Chart(ctx).Pie(data, options); diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index bd342911e49..882e7d6b6ee 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,9 +1,13 @@ - page_title "Contributors", "Graphs" = render "header_title" -.tree-ref-holder - = render 'shared/ref_switcher', destination: 'graphs' = render 'head' +.gray-content-block.append-bottom-default + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'graphs' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + .loading-graph .center %h3.page-title @@ -24,18 +28,21 @@ -:coffeescript - $.ajax +:javascript + $.ajax({ type: "GET", url: location.href, - success: (data) -> - graph = new ContributorsStatGraph() - graph.init(data) + dataType: "json", + success: function (data) { + var graph = new ContributorsStatGraph(); + graph.init(data); - $("#brush_change").change -> - graph.change_date_header() - graph.redraw_authors() + $("#brush_change").change(function(){ + graph.change_date_header(); + graph.redraw_authors(); + }); $(".stat-graph").fadeIn(); $(".loading-graph").hide(); - dataType: "json" + } + }); diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 85dbfd67862..b18d9197d0b 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -19,7 +19,7 @@ = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' .form-group = f.label :url, "Trigger", class: 'control-label' - .col-sm-10 + .col-sm-10.prepend-top-10 %div = f.check_box :push_events, class: 'pull-left' .prepend-left-20 @@ -55,6 +55,13 @@ %strong Merge Request events %p.light This url will be triggered when a merge request is created + %div + = f.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = f.label :build_events, class: 'list-label' do + %strong Build events + %p.light + This url will be triggered when the build status changes .form-group = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' .col-sm-10 @@ -78,7 +85,7 @@ .clearfix %span.monospace= hook.url %p - - %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger| + - %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"} diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 92a87690c54..6027fb23360 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -1,22 +1,19 @@ - page_title "Import repository" %h3.page-title - - if @project.import_failed? - Import failed. Retry? - - else - Import repository + Import repository %hr +- if @project.import_failed? + .panel.panel-danger + .panel-heading The repository could not be imported. + .panel-body + %pre + :preserve + #{@project.import_error.try(:strip)} + = form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Import existing git repo - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .well.prepend-top-20 - This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. - %br - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"} + = render "shared/import_form", f: f + .form-actions = f.submit 'Start import', class: "btn btn-create", tabindex: 4 diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 06886d215a3..c0d1ce0d120 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -8,7 +8,7 @@ - else Import in progress. - unless @project.forked? - %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)} + %p.monospace git clone --bare #{@project.safe_import_url} %p Please wait while we import the repository for you. Refresh at will. :javascript new ProjectImport(); diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml index aef352029d0..de415ae51a4 100644 --- a/app/views/projects/issues/_closed_by_box.html.haml +++ b/app/views/projects/issues/_closed_by_box.html.haml @@ -1,3 +1,2 @@ -.issue-closed-by-widget - = icon('check') - This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted. +.issue-closed-by-widget.gray-content-block.second-block.white + This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted. diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index d4a98eca473..dc434cf38c4 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,36 +1,9 @@ - content_for :note_actions do - if can?(current_user, :update_issue, @issue) - if @issue.closed? - = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' + = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue' - else - = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue' + = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue' -= render 'shared/show_aside' - -.gray-content-block.second-block - .row - .col-md-9 - .votes-holder.pull-right - #votes= render 'votes/votes_block', votable: @issue - .participants - %span= pluralize(@participants.count, 'participant') - - @participants.each do |participant| - = link_to_member(@project, participant, name: false, size: 24) - .col-md-3 - %span.slead.has_tooltip{title: 'Cross-project reference'} - = cross_project_reference(@project, @issue) - -.row - %section.col-md-9 - .voting_notes#notes= render 'projects/notes/notes_with_form' - %aside.col-md-3 - .issuable-affix - .context - = render 'shared/issuable/context', issuable: @issue - - - if @issue.labels.any? - .issuable-context-title - %label Labels - .issue-show-labels - - @issue.labels.each do |label| - = link_to_label(label) +#notes + = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index f39bb7d2574..6588d9bdbe1 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,9 +1,5 @@ -%div.issue-form-holder - %h3.page-title= @issue.new_record? ? "Create Issue" : "Edit Issue ##{@issue.iid}" - %hr - - = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| - = render 'shared/issuable/form', f: f, issuable: @issue += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f| + = render 'shared/issuable/form', f: f, issuable: @issue :javascript $('.assign-to-me-link').on('click', function(e){ diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 55ce912829d..f9cf4910df3 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,37 +6,42 @@ .issue-title %span.issue-title-text = link_to_gfm issue.title, issue_path(issue), class: "row_title" - .issue-labels - - issue.labels.each do |label| - = link_to_label(label, project: issue.project) - .pull-right.light + %ul.controls.light - if issue.closed? - %span + %li CLOSED + - if issue.assignee - = link_to_member(@project, issue.assignee, name: false) + %li + = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") + - note_count = issue.notes.user.count - if note_count > 0 - - %span - %i.fa.fa-comments - = note_count + %li + = link_to issue_path(issue) + "#notes" do + = icon('comments') + = note_count - else - - %span.issue-no-comments - %i.fa.fa-comments - = 0 + %li + = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do + = icon('comments') + = note_count .issue-info - = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe - - if issue.votes_count > 0 - = render 'votes/votes_inline', votable: issue + #{issue.to_reference} · + opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} + by #{link_to_member(@project, issue.author, avatar: false)} - if issue.milestone - %span - %i.fa.fa-clock-o + = 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.labels.any? + + - issue.labels.each do |label| + = link_to_label(label, project: issue.project) - if issue.tasks? + %span.task-status = issue.task_status diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index a3399c57aa2..e0e89b764d5 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -5,8 +5,9 @@ .nothing-here-block No issues to show - if @issues.present? - .pull-right - %span.issue_counter #{@issues.total_count} - issues for this filter + .issuable-filter-count + %span.pull-right + = number_with_delimiter(@issues.total_count) + issues for this filter = paginate @issues, theme: "gitlab" diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml new file mode 100644 index 00000000000..254968e4f67 --- /dev/null +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -0,0 +1,26 @@ +-if @merge_requests.any? + %h2.merge-requests-title + = pluralize(@merge_requests.count, 'Related Merge Request') + %ul.bordered-list + - has_any_ci = @merge_requests.any?(&:ci_commit) + - @merge_requests.each do |merge_request| + %li + %span.merge-request-ci-status + - if merge_request.ci_commit + = render_ci_status(merge_request.ci_commit) + - elsif has_any_ci + = icon('blank fw') + %span.merge-request-id + \##{merge_request.iid} + %span.merge-request-info + %strong + = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" + - unless @issue.project.id == merge_request.target_project.id + in + - project = merge_request.target_project + = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) + %span.merge-request-status.prepend-left-10 + - if merge_request.merged? + MERGED + - elsif merge_request.closed? + CLOSED diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 53b6f0879c9..20216297d25 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,2 +1,8 @@ - page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" += render "header_title" + +%h3.page-title + Edit Issue ##{@issue.iid} +%hr + = render "form" diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index 153447baa1b..b317a0c1cf4 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1,4 +1,8 @@ - page_title "New Issue" = render "header_title" +%h3.page-title + New Issue +%hr + = render "form" diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f01bf2505da..f931a0d3b92 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,52 +1,67 @@ -- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_title "#{@issue.title} (##{@issue.iid})", "Issues" +- page_description @issue.description +- page_card_attributes @issue.card_attributes + = render "header_title" .issue + .detail-page-header + .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed + .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open + %span.identifier + Issue ##{@issue.iid} + %span.creator + · + opened by #{link_to_member(@project, @issue.author, size: 24)} + · + = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') + - if @issue.updated_at != @issue.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') + + .pull-right + - 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_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_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 - .page-title - .issue-box{ class: issue_box_class(@issue) } - - if @issue.closed? - Closed - - else - Open - %span.issue-id Issue ##{@issue.iid} - %span.creator - · created by #{link_to_member(@project, @issue.author, size: 24)} - · - = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') - - if @issue.updated_at != @issue.created_at - %span - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') - - .pull-right - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do - = icon('plus') - New Issue - - if can?(current_user, :update_issue, @issue) - - if @issue.closed? - = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen' - - else - = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue' - - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do - = icon('pencil-square-o') - Edit - - .gray-content-block.middle-block - %h2.issue-title - = gfm escape_once(@issue.title) + .detail-page-description.gray-content-block.second-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) + = markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field = @issue.description - - if @closed_by_merge_requests.present? - = render 'projects/issues/closed_by_box' - .issue-discussion - = render 'projects/issues/discussion' + + .merge-requests + = render 'merge_requests' + + .gray-content-block.second-block.oneline-block + = render 'votes/votes_block', votable: @issue + + - if @closed_by_merge_requests.present? + = render 'projects/issues/closed_by_box' + + .row + %section.col-md-9 + .issuable-discussion + = render 'projects/issues/discussion' + + %aside.col-md-3 + = render 'shared/issuable/sidebar', issuable: @issue + + = render 'shared/show_aside' diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index b7735aaf3c1..2f0f3fcfb06 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -1,3 +1,3 @@ -$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @issue)}"); -$('.context').effect('highlight') +$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}"); +$('.issuable-sidebar').parent().effect('highlight') new Issue(); diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 4cf13492e99..5ce2a7b985d 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -10,9 +10,9 @@ .form-group = f.label :title, class: 'control-label' .col-sm-10 - = f.text_field :title, class: "form-control js-quick-submit", required: true + = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true .form-group - = f.label :color, "Background Color", class: 'control-label' + = f.label :color, "Background color", class: 'control-label' .col-sm-10 .input-group .input-group-addon.label-color-preview @@ -28,6 +28,8 @@ .form-actions - = f.submit 'Save', class: 'btn btn-save js-save-button' + - if @label.persisted? + = f.submit 'Save changes', class: 'btn btn-save js-save-button' + - else + = f.submit 'Create Label', class: 'btn btn-create js-save-button' = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' - diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index c6ebfa281a1..b70a9fc9fe5 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -7,4 +7,4 @@ - if can? current_user, :admin_label, @project = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' - = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml index 1b4c83ab097..d59563b122a 100644 --- a/app/views/projects/labels/destroy.js.haml +++ b/app/views/projects/labels/destroy.js.haml @@ -1,2 +1,2 @@ - if @project.labels.size == 0 - $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000) + $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index bc4ab0ca27c..675a805e12f 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,11 +1,7 @@ - page_title "Edit", @label.name, "Labels" = render "header_title" -%h3 - Edit label - %span.light #{@label.name} -.back-link - = link_to namespace_project_labels_path(@project.namespace, @project) do - ← To labels list +%h3.page-title + Edit Label %hr = render 'form' diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 97175f8232b..9081bcfe9b3 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -4,6 +4,7 @@ .gray-content-block.top-block - if can? current_user, :admin_label, @project = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do + = icon('plus') New label .oneline Labels can be applied to issues and merge requests. @@ -14,8 +15,8 @@ = render @labels = paginate @labels, theme: 'gitlab' - else - .light-well + .nothing-here-block - if can? current_user, :admin_label, @project - .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels - else - .nothing-here-block No labels created + No labels created diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 342ad4f3f95..e20fd7d6891 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,9 +1,7 @@ - page_title "New Label" = render "header_title" -%h3 New label -.back-link - = link_to namespace_project_labels_path(@project.namespace, @project) do - ← To labels list +%h3.page-title + New Label %hr = render 'form' diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 38e66c3828b..bff3c3b283d 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,33 +1,8 @@ - content_for :note_actions do - 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-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" + = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + = 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 js-note-target-reopen", title: "Reopen merge request" -= render 'shared/show_aside' - -.gray-content-block.second-block - .row - .col-md-9 - .votes-holder.pull-right - #votes= render 'votes/votes_block', votable: @merge_request - = render "projects/merge_requests/show/participants" - .col-md-3 - %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} - = cross_project_reference(@project, @merge_request) - -.row - %section.col-md-9 - = render "projects/notes/notes_with_form" - %aside.col-md-3 - .issuable-affix - .context - = render 'shared/issuable/context', issuable: @merge_request - - - if @merge_request.labels.any? - .issuable-context-title - %label Labels - .merge-request-show-labels - - @merge_request.labels.each do |label| - = link_to_label(label) +#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 9cf389dbe38..3e4ab09c6d4 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,6 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| - .merge-request-form-info - = render 'shared/issuable/form', f: f, issuable: @merge_request + = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript $('.assign-to-me-link').on('click', function(e){ diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 25e4e8ba80d..a051729dc32 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -2,43 +2,60 @@ .merge-request-title %span.merge-request-title-text = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" - .merge-request-labels - - merge_request.labels.each do |label| - = link_to_label(label, project: merge_request.project) - .pull-right.light + %ul.controls.light - if merge_request.merged? - %span - %i.fa.fa-check + %li MERGED - elsif merge_request.closed? - %span - %i.fa.fa-ban + %li + = icon('ban') CLOSED - - note_count = merge_request.mr_and_commit_notes.user.count + + - if merge_request.ci_commit + %li + = render_ci_status(merge_request.ci_commit) + + - if merge_request.open? && merge_request.broken? + %li + = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do + = icon('exclamation-triangle') + - if merge_request.assignee - - = link_to_member(merge_request.source_project, merge_request.assignee, name: false) + %li + = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + + - note_count = merge_request.mr_and_commit_notes.user.count - if note_count > 0 - - %span - %i.fa.fa-comments - = note_count + %li + = link_to merge_request_path(merge_request) + "#notes" do + = icon('comments') + = note_count - else - - %span.merge-request-no-comments - %i.fa.fa-comments - = 0 + %li + = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do + = icon('comments') + = note_count .merge-request-info - = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe - - if merge_request.votes_count > 0 - = render 'votes/votes_inline', votable: merge_request - - if merge_request.milestone_id? + \##{merge_request.iid} · + opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} + by #{link_to_member(@project, merge_request.author, avatar: false)} + - if merge_request.target_project.default_branch != merge_request.target_branch + + = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do + = icon('code-fork') + = merge_request.target_branch + - if merge_request.milestone - %span - %i.fa.fa-clock-o + = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do + = icon('clock-o') = merge_request.milestone.title + - if merge_request.labels.any? + + - merge_request.labels.each do |label| + = link_to_label(label, project: merge_request.project) - if merge_request.tasks? + %span.task-status = merge_request.task_status diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index d86707b3d97..29d09d0a652 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,8 +5,10 @@ .nothing-here-block No merge requests to show - if @merge_requests.present? - .pull-right - %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter + .issuable-filter-count + %span.pull-right + = number_with_delimiter(@merge_requests.total_count) + merge requests for this filter = paginate @merge_requests, theme: "gitlab" diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 452006162db..236a545c840 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -1,4 +1,5 @@ -%p.lead Compare branches for new Merge Request +%h3.page-title + New Merge Request = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors @@ -10,7 +11,7 @@ .panel-body = f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true }) - = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2', required: true}) + = f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } }) .panel-footer .mr_source_commit @@ -22,7 +23,7 @@ - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] = f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true }) - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2', required: true}) + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } }) .panel-footer .mr_target_commit @@ -37,7 +38,7 @@ %h4 Compare failed %p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches. - else - .light-well.append-bottom-10 + .light-well.append-bottom-default .center %h4 There isn't anything to merge. @@ -51,8 +52,8 @@ are the same. - %div - = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn" + .form-actions + = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" :javascript var source_branch = $("#merge_request_source_branch") @@ -77,12 +78,12 @@ }); -:coffeescript - - $(".merge-request-form").on 'submit', -> - if $("#merge_request_source_branch").val() is "" or $('#merge_request_target_branch').val() is "" - $(".mr-compare-errors").html("You must select source and target branch to proceed") - $(".mr-compare-errors").fadeIn() - event.preventDefault() - return - +:javascript + $(".merge-request-form").on('submit', function () { + if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") { + $(".mr-compare-errors").html("You must select source and target branch to proceed"); + $(".mr-compare-errors").fadeIn(); + event.preventDefault(); + return; + } + }); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 6244d3ba0b4..a14943b15d3 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -1,5 +1,5 @@ %h3.page-title - New merge request + New Merge Request %p.slead - source_title, target_title = format_mr_branch_names(@merge_request) From @@ -11,27 +11,31 @@ = 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| - .merge-request-form-info - = render 'shared/issuable/form', f: f, issuable: @merge_request - = f.hidden_field :source_project_id - = f.hidden_field :source_branch - = f.hidden_field :target_project_id - = f.hidden_field :target_branch + = render 'shared/issuable/form', f: f, issuable: @merge_request + = f.hidden_field :source_project_id + = f.hidden_field :source_branch + = f.hidden_field :target_project_id + = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs + %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.commits-tab - = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size + - if @ci_commit + %li.builds-tab.active + = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size %li.diffs-tab.active - = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @diffs.size .tab-content #commits.commits.tab-pane - = render "projects/commits/commits", project: @project + = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane.active - if @diffs.present? = render "projects/diffs/diffs", diffs: @diffs, project: @project @@ -43,6 +47,9 @@ .alert.alert-danger %h4 This comparison includes a huge diff. %p To preserve performance the line changes are not shown. + - if @ci_commit + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" :javascript $('.assign-to-me-link').on('click', function(e){ @@ -57,4 +64,3 @@ diffs_loaded: true, commits_loaded: true }); - diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index eeaa72ed21b..ba7c2c01e93 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,14 +1,18 @@ -- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_description @merge_request.description +- page_card_attributes @merge_request.card_attributes + = render "header_title" - if params[:view] == 'parallel' - fluid_layout true .merge-request{'data-url' => merge_request_path(@merge_request)} + = render "projects/merge_requests/show/mr_title" + .merge-request-details.issuable-details - = render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_box" - .append-bottom-20.mr-source-target.prepend-top-default + .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? .pull-right - if @merge_request.source_branch_exists? @@ -26,44 +30,62 @@ %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal %span Request to merge - %span.label-branch #{source_branch_with_namespace(@merge_request)} + %span.label-branch= source_branch_with_namespace(@merge_request) %span into - %span.label-branch #{@merge_request.target_branch} + = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do + = @merge_request.target_branch = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" - - if @merge_request.open? && @merge_request.can_be_merged? - .light.append-bottom-20 + - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user) + .light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - - if @commits.present? - %ul.merge-request-tabs - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do - Commits - %span.badge= @commits.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do - Changes - %span.badge= @merge_request.diffs.size + - if @commits.present? + %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %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 + %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 + %span.badge= @commits.size + - if @ci_commit + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do + Changes + %span.badge= @merge_request.diffs.size + + .tab-content + #notes.notes.tab-pane.voting_notes + .gray-content-block.second-block.oneline-block + = render 'votes/votes_block', votable: @merge_request + + .row + %section.col-md-9 + .issuable-discussion + = render "projects/merge_requests/discussion" + %aside.col-md-3 + = render 'shared/issuable/sidebar', issuable: @merge_request + = render 'shared/show_aside' - .tab-content - #notes.notes.tab-pane.voting_notes - = render "projects/merge_requests/discussion" - #commits.commits.tab-pane - - # This tab is always loaded via AJAX - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX + #commits.commits.tab-pane + - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX + #diffs.diffs.tab-pane + - # This tab is always loaded via AJAX - .mr-loading-status - = spinner + .mr-loading-status + = spinner :javascript var merge_request; diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml new file mode 100644 index 00000000000..eab5be488b5 --- /dev/null +++ b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml @@ -0,0 +1,2 @@ +:plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}"); diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index 303ca0a880b..fc62bb5bce9 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -2,6 +2,6 @@ = render "header_title" %h3.page-title - = "Edit merge request ##{@merge_request.iid}" + Edit Merge Request ##{@merge_request.iid} %hr = render 'form' diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 086298e5af1..8d5d0394a82 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -6,9 +6,10 @@ .controls = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - - if can? current_user, :create_merge_request, @project + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project .pull-left.hidden-xs - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do %i.fa.fa-plus New Merge Request = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 33321651e32..92ce479d463 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -1,6 +1,10 @@ -- if @status +- case @status +- when :success :plain - merge_request_widget.mergeInProgress(); + merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); +- when :merge_when_build_succeeds + :plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index 9fdde80c6d9..d259968030e 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,7 +1,7 @@ - page_title "New Merge Request" = render "header_title" -- if @merge_request.can_be_created +- if @merge_request.can_be_created && !params[:change_branches] = render 'new_submit' - else = render 'new_compare' diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml new file mode 100644 index 00000000000..307a75d02ca --- /dev/null +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -0,0 +1 @@ += render "projects/commit/builds", link_to_commit: true diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a71b181a6a5..7f904ec42a0 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1 +1,5 @@ +.gray-content-block.middle-block.oneline-block + = icon("sort-amount-desc") + Most recent commits displayed first + = render "projects/commits/commits", project: @merge_request.project diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 626970f39be..d9cfc3d7ae9 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: @merge_request.diffs, project: @merge_request.project + = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index f18cf96c17d..877cc3d744b 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -3,12 +3,13 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 Check out, review and merge locally + %h3 Check out, review, and merge locally .modal-body %p - %strong Step 1. + %strong Step 1. Fetch and check out the branch for this merge request - %pre.dark + = clipboard_button(clipboard_target: 'pre#merge-info-1') + %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch} @@ -24,7 +25,8 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - %pre.dark + = clipboard_button(clipboard_target: 'pre#merge-info-3') + %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve git checkout #{h @merge_request.target_branch} @@ -36,7 +38,8 @@ %p %strong Step 4. Push the result of the merge to GitLab - %pre.dark + = clipboard_button(clipboard_target: 'pre#merge-info-4') + %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} - unless @merge_request.can_be_merged_by?(current_user) diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index b4f62a75890..0f81e5e8914 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,12 +1,12 @@ -.gray-content-block.middle-block - %h2.issue-title - = gfm escape_once(@merge_request.title) +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@merge_request.title), pipeline: :single_line %div - if @merge_request.description.present? .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description) + = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) %textarea.hidden.js-task-list-field = @merge_request.description 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 2bf9cd597a4..fc6fb2a0d42 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -1,10 +1,11 @@ -.page-title - .issue-box{ class: issue_box_class(@merge_request) } +.detail-page-header + .status-box{ class: status_box_class(@merge_request) } = @merge_request.state_human_name - %span.issue-id Merge Request ##{@merge_request.iid} + %span.identifier + Merge Request ##{@merge_request.iid} %span.creator · - created by #{link_to_member(@project, @merge_request.author, size: 24)} + opened by #{link_to_member(@project, @merge_request.author, size: 24)} · = time_ago_with_tooltip(@merge_request.created_at) - if @merge_request.updated_at != @merge_request.created_at @@ -16,9 +17,9 @@ .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-grouped btn-close", title: "Close merge request" - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do + = 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 %i.fa.fa-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-grouped btn-reopen reopen-mr-link", title: "Close merge request" + = 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/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml deleted file mode 100644 index c67afe963e7..00000000000 --- a/app/views/projects/merge_requests/show/_participants.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.participants - %span #{@participants.count} participants - - @participants.each do |participant| - = link_to_member(@project, participant, name: false, size: 24) diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index 25583b2cc6f..93db65ddf79 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -1,3 +1,3 @@ -$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @merge_request)}"); -$('.context').effect('highlight') +$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}"); +$('.issuable-sidebar').parent().effect('highlight') merge_request = new MergeRequest(); diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 10efb811939..b05ab869215 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,30 +1,33 @@ -- ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha) -- if ci_commit - - status = ci_commit.status +- if @ci_commit .mr-widget-heading - .ci_widget{class: "ci-#{status}"} - = ci_status_icon(ci_commit) - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. + .ci_widget{class: "ci-#{@ci_commit.status}"} + = ci_status_icon(@ci_commit) + %span + Build + = ci_status_label(@ci_commit) + for + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" %span.ci-coverage - = link_to "View build details", ci_status_path(ci_commit) + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - # Remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status| + - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} - - if status == :success - - status = "passed" - = icon("check-circle") - - else - = icon("circle") - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - - if ci_build_details_path(@merge_request) - = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + - if details_path = ci_build_details_path(@merge_request) + = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink" .ci_widget = icon("spinner spin") @@ -38,6 +41,7 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. - :coffeescript - $ -> - merge_request_widget.getCiStatus() + :javascript + $(function() { + merge_request_widget.getCiStatus(); + }); diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index f223f687def..d1d602eecdc 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -7,43 +7,42 @@ by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} %div - - if !@merge_request.source_branch_exists? - = succeed '.' do - The changes were merged into - %span.label-branch= @merge_request.target_branch + - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true') + 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"}. The source branch has been removed. - - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) + - elsif @merge_request.can_remove_source_branch?(current_user) .remove_source_branch_widget - %p - = succeed '.' do - The changes were merged into - %span.label-branch= @merge_request.target_branch + %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"}. You can remove the source branch now. = 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-primary btn-sm remove_source_branch" do %i.fa.fa-times Remove Source Branch .remove_source_branch_widget.failed.hide - %p + %p Failed to remove source branch '#{@merge_request.source_branch}'. .remove_source_branch_in_progress.hide %p = icon('spinner spin') - Removing source branch '#{@merge_request.source_branch}'. Please wait. This page will be automatically reload. - - :coffeescript - $('.remove_source_branch').on 'click', -> - $('.remove_source_branch_widget').hide() - $('.remove_source_branch_in_progress').show() - - $(".remove_source_branch").on "ajax:success", (e, data, status, xhr) -> - location.reload() - - $(".remove_source_branch").on "ajax:error", (e, data, status, xhr) -> - $('.remove_source_branch_widget').hide() - $('.remove_source_branch_in_progress').hide() - $('.remove_source_branch_widget.failed').show() - - + Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. + + :javascript + $('.remove_source_branch').on('click', function() { + $('.remove_source_branch_widget').hide(); + $('.remove_source_branch_in_progress').show(); + }); + + $(".remove_source_branch").on("ajax:success", function (e, data, status, xhr) { + location.reload(); + }); + + $(".remove_source_branch").on("ajax:error", function (e, data, status, xhr) { + $('.remove_source_branch_widget').hide(); + $('.remove_source_branch_in_progress').hide(); + $('.remove_source_branch_widget.failed').show(); + }); diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 0aad9bb3e88..55dbae598d3 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -13,6 +13,8 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' + - elsif @merge_request.merge_when_build_succeeds? + = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - elsif @merge_request.can_be_merged? @@ -24,4 +26,4 @@ %i.fa.fa-check Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} = succeed '.' do - != gfm(issues_sentence(@closes_issues)) + != markdown issues_sentence(@closes_issues), pipeline: :gfm diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 613525437ab..d9a1730a8bc 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,25 +1,60 @@ +- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil + = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token .accept-merge-holder.clearfix.js-toggle-container - .accept-action - = f.button class: "btn btn-create accept_merge_request" do - Accept Merge Request - - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? - .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do - = check_box_tag :should_remove_source_branch - Remove source branch - .accept-control.right - = link_to "#", class: "modify-merge-commit-link js-toggle-button" do - = icon('edit') - Modify commit message - .js-toggle-content.hide.prepend-top-20 + .clearfix + .accept-action + - if @ci_commit && @ci_commit.active? + %span.btn-group + = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do + Merge When Build Succeeds + = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do + %span.caret + %span.sr-only + Select Merge Moment + %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %li + = link_to "#", class: "merge_when_build_succeeds" do + = icon('check fw') + Merge When Build Succeeds + %li + = link_to "#", class: "accept_merge_request" do + = icon('warning fw') + Merge Immediately + - else + = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do + Accept Merge Request + - if @merge_request.can_remove_source_branch?(current_user) + .accept-control.checkbox + = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = check_box_tag :should_remove_source_branch + Remove source branch + .accept-control.right + = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + = icon('edit') + Modify commit message + .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, text: @merge_request.merge_commit_message, rows: 14, hint: true - :coffeescript - $('.accept-mr-form').on 'ajax:before', -> - btn = $('.accept_merge_request') - btn.disable() - btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress") + = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" + + :javascript + $('.accept-mr-form').on('ajax:send', function() { + $(".accept-mr-form :input").disable(); + }); + + $('.accept_merge_request').on('click', function() { + $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); + }); + + $('.merge_when_build_succeeds').on('click', function() { + $("#merge_when_build_succeeds").val("1"); + }); + + $('.js-merge-dropdown a').on('click', function(e) { + e.preventDefault(); + $(this).closest("form").submit(); + }); diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index b6b8974297e..e16878ba513 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -2,6 +2,8 @@ = icon("spinner spin") Checking ability to merge automatically… -:coffeescript - $ -> - merge_request_widget.getMergeStatus() +:javascript + $(function() { + merge_request_widget.getMergeStatus(); + }); + diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml new file mode 100644 index 00000000000..2168294c683 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -0,0 +1,26 @@ +%h4 + Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} + to be merged automatically when the build succeeds. +%div + - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present? + %p + = succeed '.' do + The changes will be merged into + %span.label-branch= @merge_request.target_branch + - if should_remove_source_branch + The source branch will be removed. + - else + The source branch will not be removed. + + - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user + - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + - if remove_source_branch_button || user_can_cancel_automatic_merge + .clearfix.prepend-top-10 + - if remove_source_branch_button + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = icon('times') + Remove Source Branch When Merged + + - if user_can_cancel_automatic_merge + = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do + Cancel Automatic Merge diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 255ddab479f..39aa2437e18 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,10 +1,3 @@ -%h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.iid}" -.back-link - = link_to namespace_project_milestones_path(@project.namespace, @project) do - ← To milestones - -%hr - = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f| -if @milestone.errors.any? .alert.alert-danger @@ -16,16 +9,13 @@ .form-group = f.label :title, "Title", class: "control-label" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true - %p.hint Required + = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true .form-group.milestone-description = 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 js-quick-submit' - .hint - .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render 'projects/notes/hints' .clearfix .error-alert .col-md-6 @@ -45,7 +35,7 @@ :javascript - $( ".datepicker" ).datepicker({ + $(".datepicker").datepicker({ dateFormat: "yy-mm-dd", onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 5e93d55b1fb..d6a44c9f0a1 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -18,11 +18,7 @@ .row .col-sm-6 - - if milestone.expired? and not milestone.closed? - %span.cred (Expired) - - if milestone.expires_at - %span - = milestone.expires_at + = render 'shared/milestone_expired', milestone: milestone .col-sm-6 - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do @@ -31,4 +27,4 @@ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do %i.fa.fa-trash-o - Remove + Delete diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index e9dc0b77462..43f8863163d 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,3 +1,9 @@ - page_title "Edit", @milestone.title, "Milestones" = render "header_title" + +%h3.page-title + Edit Milestone ##{@milestone.iid} + +%hr + = render "form" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index a207385bd43..114b06457a5 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,15 +1,18 @@ - page_title "Milestones" = render "header_title" -= render 'shared/milestones_filter' -.gray-content-block - .pull-right - - if can? current_user, :admin_milestone, @project + +.project-issuable-filter + .controls + - if can?(current_user, :admin_milestone, @project) = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do %i.fa.fa-plus New Milestone - .oneline - Milestone allows you to group issues and set due date for it + + = render 'shared/milestones_filter' + +.gray-content-block + Milestone allows you to group issues and set due date for it .milestones %ul.content-list diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 9ba9acb6f77..0d016f78313 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,3 +1,9 @@ - page_title "New Milestone" = render "header_title" + +%h3.page-title + New Milestone + +%hr + = render "form" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 3a898dfbcfd..1670ea8741a 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,46 +1,52 @@ -- page_title @milestone.title, "Milestones" +- page_title @milestone.title, "Milestones" +- page_description @milestone.description + = render "header_title" -%h4.page-title - .issue-box{ class: issue_box_class(@milestone) } +.detail-page-header + .status-box{ class: status_box_class(@milestone) } - if @milestone.closed? Closed - elsif @milestone.expired? Expired - else Open - Milestone ##{@milestone.iid} - %small.creator - = @milestone.expires_at + %span.identifier + Milestone ##{@milestone.iid} + - if @milestone.expires_at + %span.creator + · + = @milestone.expires_at .pull-right - if can?(current_user, :admin_milestone, @project) - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o - Edit - if @milestone.active? = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" - else = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do %i.fa.fa-trash-o - Remove + Delete + + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do + %i.fa.fa-pencil-square-o + Edit + +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@milestone.title), pipeline: :single_line + %div + - if @milestone.description.present? + .description + .wiki + = preserve do + = markdown @milestone.description -%hr - if @milestone.issues.any? && @milestone.can_be_closed? - .alert.alert-success + .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close milestone now. -%h3.issue-title - = gfm escape_once(@milestone.title) -%div - - if @milestone.description.present? - .description - .wiki - = preserve do - = markdown @milestone.description - -%hr -.context +.context.prepend-top-default %p.lead Progress: #{@milestone.closed_items_count} closed @@ -51,8 +57,7 @@ %span.pull-right= @milestone.expires_at = milestone_progress_bar(@milestone) - -%ul.nav.nav-tabs +%ul.center-top-menu.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues @@ -66,17 +71,21 @@ Participants %span.badge= @users.count - .pull-right - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :read_issue, @project) - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped" - .tab-content .tab-pane.active#tab-issues - .row + .gray-content-block.middle-block + .pull-right + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All issues in this milestone + + .row.prepend-top-default .col-md-4 = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned') .col-md-4 @@ -85,7 +94,15 @@ = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') .tab-pane#tab-merge-requests - .row + .gray-content-block.middle-block + .pull-right + - if can?(current_user, :read_merge_request, @project) + = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + + .oneline + All merge requests in this milestone + + .row.prepend-top-default .col-md-3 = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned') .col-md-3 @@ -100,6 +117,10 @@ = render 'merge_request', merge_request: merge_request .tab-pane#tab-participants + .gray-content-block.middle-block + .oneline + All participants to this milestone + %ul.bordered-list - @users.each do |user| %li diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index 415c98ec6a6..28a617538b5 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,3 +1,6 @@ -.append-bottom-20 - = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} - .pull-right.visible-lg.light You can move around the graph by using the arrow keys. +.gray-content-block.append-bottom-default + .tree-ref-holder + = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} + + .oneline + You can move around the graph by using the arrow keys. diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 16005161df6..8065663ca2a 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,5 +1,6 @@ - page_title "Network", @ref -= header_title project_title(@project, "Network", namespace_project_network_path(@project.namespace, @project, current_ref)) += render "projects/commits/header_title" += render "projects/commits/head" = render "head" .project-network .controls diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index daab2326bc7..25233112132 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,5 +1,10 @@ - page_title 'New Project' -- header_title 'New Project' +- header_title "Projects", root_path + +%h3.page-title + New Project +%hr + .project-edit-container .project-edit-errors = render 'projects/errors' @@ -11,19 +16,23 @@ Project path .col-sm-10 .input-group - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true - .input-group-addon - \.git - - - if current_user.can_select_namespace? - .form-group - = f.label :namespace_id, class: 'control-label' do - %span Namespace - .col-sm-10 - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} + - if current_user.can_select_namespace? + .input-group-addon + = root_url + = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1} + .input-group-addon + \/ + - else + .input-group-addon + #{root_url}#{current_user.username}/ + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true + + - if current_user.can_create_group? + .help-block + Want to house several dependent projects under the same namespace? + = link_to "Create a group", new_group_path - if import_sources_enabled? - .project-import.js-toggle-container .form-group %label.control-label Import project from @@ -82,19 +91,7 @@ %span Any repo by URL .js-toggle-content.hide - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Git repository URL - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' - .well.prepend-top-20 - %ul - %li - The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. - %li - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. + = render "shared/import_form", f: f .prepend-botton-10 @@ -103,19 +100,12 @@ Description %span.light (optional) .col-sm-10 - = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3 + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250, tabindex: 3 = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project .form-actions = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 - - - if current_user.can_create_group? - .pull-right - .light.inline - .space-right - Need a group for several dependent projects? - = link_to new_group_path, class: "btn" do - Create a group + = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' .save-project-loader.hide .center @@ -124,9 +114,11 @@ Creating project & repository. %p Please wait a moment, this page will automatically refresh when ready. -:coffeescript - $('.how_to_import_link').bind 'click', (e) -> - e.preventDefault() - import_modal = $(this).next(".modal").show() - $('.modal-header .close').bind 'click', -> - $(".modal").hide() +:javascript + $('.how_to_import_link').bind('click', function (e) { + e.preventDefault(); + var import_modal = $(this).next(".modal").show(); + }); + $('.modal-header .close').bind('click', function() { + $(".modal").hide(); + }); diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index a21c019986a..3ccda1b381c 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -6,6 +6,5 @@ = render 'projects/notes/hints' .note-form-actions - .buttons - = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button' - = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel' + = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button' + = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 13dfa0a1bb3..acb6dc52a8e 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,5 +1,5 @@ = 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 common-note-form gfm-form" }, authenticity_token: true do |f| - = hidden_field_tag :view, params[:view] + = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) = f.hidden_field :commit_id @@ -12,8 +12,7 @@ = render 'projects/notes/hints' .error-alert - .note-form-actions - .buttons.clearfix - = f.submit 'Add Comment', class: "btn btn-green comment-btn btn-grouped js-comment-button" - = yield(:note_actions) - %a.btn.grouped.js-close-discussion-note-form Cancel + .note-form-actions.clearfix + = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" + = yield(:note_actions) + %a.btn.btn-cancel.js-close-discussion-note-form Cancel diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5d184730796..922535e5c4a 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,7 +2,7 @@ .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} - %img.avatar.s40{src: avatar_icon(note.author), alt: ''} + = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header - if note_editable?(note) @@ -35,32 +35,11 @@ - if note.updated_by && note.updated_by != note.author by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} - - if note.superceded?(@notes) - - if note.upvote? - %span.vote.upvote.label.label-gray.strikethrough - = icon('thumbs-up') - \+1 - - if note.downvote? - %span.vote.downvote.label.label-gray.strikethrough - = icon('thumbs-down') - \-1 - - else - - if note.upvote? - %span.vote.upvote.label.label-success - = icon('thumbs-up') - \+1 - - if note.downvote? - %span.vote.downvote.label.label-danger - = icon('thumbs-down') - \-1 - - .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do - = markdown(note.note, {no_header_anchors: true}) - - unless note.system? - -# System notes can't be edited + = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) + - if note_editable?(note) = render 'projects/notes/edit_form', note: note - if note.attachment.url diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 04222b8f7c4..eb378b42603 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -4,7 +4,7 @@ .js-main-target-form - if can? current_user, :create_note, @project - = render "projects/notes/form", view: params[:view] + = render "projects/notes/form", view: diff_view :javascript - new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}") + var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 43e92437cf5..1c2458fa144 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -4,12 +4,13 @@ group members %small (#{members.count}) - .panel-head-actions - = link_to group_group_members_path(@group), class: 'btn btn-sm' do - %i.fa.fa-pencil-square-o - Edit group members - %ul.well-list - - members.each do |member| + - if can?(current_user, :admin_group_member, @group) + .pull-right + = link_to group_group_members_path(@group), class: 'btn' do + = icon('pencil-square-o') + Manage group members + %ul.content-list + - members.limit(20).each do |member| = render 'groups/group_members/group_member', member: member, show_controls: false - if members.count > 20 %li diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 76c46d1d806..05bf3a7ef6a 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -4,7 +4,7 @@ %li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} %span.list-item-name - if member.user - = image_tag avatar_icon(user, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' %strong = link_to user.name, user_path(user) %span.cgray= user.username @@ -14,7 +14,7 @@ %label.label.label-danger %strong Blocked - else - = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: '' + = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' %strong = member.invite_email %span.cgray @@ -24,18 +24,19 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if current_user_can_admin_project + - if can?(current_user, :admin_project_member, @project) = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if current_user_can_admin_project - - unless @project.personal? && user == current_user - .pull-right - %strong= member.human_access + - if can?(current_user, :admin_project_member, @project) + .pull-right + %strong= member.human_access + - if can?(current_user, :update_project_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o + - if can?(current_user, :destroy_project_member, member) - if current_user == user = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 615c425e59a..ccddab13aaf 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,11 +1,21 @@ -- can_admin_project = can?(current_user, :admin_project, @project) - -.panel.panel-default.prepend-top-20 +.panel.panel-default .panel-heading %strong #{@project.name} project members %small (#{members.count}) - %ul.well-list + .pull-right + = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false } + = button_tag class: 'btn', title: 'Search' do + = icon("search") + %ul.content-list - members.each do |project_member| - = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project + = render 'project_member', member: project_member + +:javascript + $('form.member-search-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '?' + $(this).serialize()); + }); diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 82809bec5b8..29225a36364 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,35 +1,21 @@ - page_title "Members" = render "header_title" +- @blank_container = true -.gray-content-block.top-block - .clearfix.js-toggle-container - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false } - = button_tag 'Search', class: 'btn' - - - if can?(current_user, :admin_project_member, @project) - %span.pull-right - = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do - Add members - %i.fa.fa-chevron-down - = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do - Import members - - .js-toggle-content.hide.new-group-member-holder +.project-members-page + - if can?(current_user, :admin_project_member, @project) + .panel.panel-default + .panel-heading + Add new user to project + .pull-right + = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do + Import members + .panel-body + %p.light + Users with access to this project are listed below. = render "new_project_member" -%p.prepend-top-default.light - Users with access to this project are listed below. - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - -= render "team", members: @project_members - -- if @group - = render "group_members", members: @group_members + = render "team", members: @project_members -:coffeescript - $('form.member-search-form').on 'submit', (event) -> - event.preventDefault() - Turbolinks.visit @.action + '?' + $(@).serialize() + - if @group + = render "group_members", members: @group_members diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 811b1858821..2fb3a41d541 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,2 @@ -- can_admin_project = can?(current_user, :admin_project, @project) :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}'); + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}'); diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 52b3a50c1e6..cfd7e1534ca 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -3,7 +3,7 @@ %p.light Keep stable branches secure and force developers to use Merge Requests %hr -.well.append-bottom-20 +.well %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} @@ -22,7 +22,7 @@ .form-group = f.label :name, "Branch", class: 'control-label' .col-sm-10 - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) + = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}}) .form-group .col-sm-offset-2.col-sm-10 .checkbox @@ -33,4 +33,3 @@ .form-actions = f.submit 'Protect', class: "btn-create btn" = render 'branches_list' - diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml new file mode 100644 index 00000000000..bc80f2f29ad --- /dev/null +++ b/app/views/projects/releases/edit.html.haml @@ -0,0 +1,19 @@ +- page_title "Edit", @tag.name, "Tags" += render "projects/commits/header_title" += render "projects/commits/head" + +.gray-content-block + .oneline + .title + Release notes for tag + %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' }) 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 js-quick-submit form-control' + = 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" diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 07c24950ee2..b9486a9b492 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,10 +3,10 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-success col-xs-10', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.col-xs-2.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index f3526ad0747..6ca919f7f80 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = gfm escape_once(truncate(commit.title, length: 40)) + = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index e6b8a2e6fe7..47ec420189d 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -15,10 +15,10 @@ - if runner.belongs_to_one_project? = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - else - - runner_project = @ci_project.runner_projects.find_by(runner_id: runner) - = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - runner_project = @project.runner_projects.find_by(runner_id: runner) + = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - elsif runner.specific? - = form_for [:ci, @ci_project, @ci_project.runner_projects.new] do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit 'Enable for this project', class: 'btn btn-sm' .pull-right diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 316ea747b14..6a37f444bb7 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -3,17 +3,17 @@ .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. %hr - - if @ci_project.shared_runners_enabled - = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do + - if @project.shared_runners_enabled? + = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do Disable shared runners - else - = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do + = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do Enable shared runners for this project - if @shared_runners_count.zero? - This application has no shared runners yet. - Please use specific runners or ask administrator to create one + This GitLab server does not provide any shared runners yet. + Please use specific runners or ask the administrator to create one. - else %h4.underlined-title Available shared runners - #{@shared_runners_count} %ul.bordered-list.available-shared-runners diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index c13625c7e49..30cd1263a12 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -12,7 +12,7 @@ %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: - %code #{@ci_project.token} + %code #{@project.runners_token} %li Start runner! diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index 66851d38316..eba03028af8 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,3 +1,5 @@ +- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" + %h4 Runner ##{@runner.id} %hr = form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| @@ -21,7 +23,7 @@ = label_tag :tag_list, class: 'control-label' do Tags .col-sm-10 - = f.text_field :tag_list, class: 'form-control' + = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' .help-block You can setup jobs to only use runners with specific tags .form-actions - = f.submit 'Save', class: 'btn btn-save' + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 529fb9c296d..315afe4a764 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Runners" .light %p A 'runner' is a process which runs a build. diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index c255cd51bd2..5bf4c09ca25 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -1,13 +1,14 @@ -= content_for :title do - %h3.project-title - Runner ##{@runner.id} - .pull-right - - if @runner.shared? - %span.runner-state.runner-state-shared - Shared - - else - %span.runner-state.runner-state-specific - Specific +- page_title "#{@runner.description} ##{@runner.id}", "Runners" + +%h3.page-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific .table-holder %table.table diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index e1823b51198..1b70880043a 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -4,18 +4,15 @@ %p= @service.description -.back-link - = link_to namespace_project_services_path(@project.namespace, @project) do - ← to services - %hr = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| = render 'shared/service_settings', form: form .form-actions - = form.submit 'Save', class: 'btn btn-save' + = form.submit 'Save changes', class: 'btn btn-save' - if @service.valid? && @service.activated? - disabled = @service.can_test? ? '':'disabled' = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 242684e5c7c..15c49767556 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 585caf674c9..ffbe445b447 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -11,7 +11,7 @@ = render "home_panel" -.project-stats.gray-content-block +.project-stats.gray-content-block.second-block %ul.nav.nav-pills %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do @@ -68,15 +68,4 @@ = render 'projects/last_commit', commit: @repository.commit, project: @project %div{class: "project-show-#{default_project_view}"} - = render default_project_view - -- if current_user - - access = user_max_access_in_project(current_user, @project) - - if access - .prepend-top-20.project-footer - .gray-content-block.footer-block.center - You have #{access} access to this project. - - if @project.project_member_by_id(current_user) - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), - data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do - Leave this project + = render default_project_view
\ No newline at end of file diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml new file mode 100644 index 00000000000..4a515469422 --- /dev/null +++ b/app/views/projects/snippets/_actions.html.haml @@ -0,0 +1,11 @@ += link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do + = icon('plus') + New Snippet +- if can?(current_user, :admin_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do + = icon('trash-o') + Delete +- if can?(current_user, :update_project_snippet, @snippet) + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + = icon('pencil-square-o') + Edit diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index e69f2d99709..dc3ea1fcf12 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -2,6 +2,6 @@ = render "header_title" %h3.page-title - Edit snippet + Edit Snippet %hr = render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 3fed2c9949d..4af963e14da 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,17 +1,13 @@ - page_title "Snippets" = render "header_title" -%h3.page-title - Snippets - - if can? current_user, :create_project_snippet, @project - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do - Add new snippet +.gray-content-block.top-block + .pull-right + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do + = icon('plus') + New Snippet -%p.light - Share code pastes with others out of git repository + .oneline + Share code pastes with others out of git repository -%ul.bordered-list - = render partial: "shared/snippets/snippet", collection: @snippets - - if @snippets.empty? - %li - .nothing-here-block Nothing here. += render 'snippets/snippets' diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 67cd69fd215..e57237991b4 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -2,6 +2,6 @@ = render "header_title" %h3.page-title - New snippet + New Snippet %hr = render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index be7d4d486fa..7c599563ce4 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,40 +1,18 @@ - page_title @snippet.title, "Snippets" = render "header_title" -%h3.page-title - = @snippet.title +.snippet-holder + = render 'shared/snippets/header' - .pull-right - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do - Add new snippet + %article.file-holder + .file-title + = blob_icon 0, @snippet.file_name + %strong + = @snippet.file_name + .file-actions.hidden-xs + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" -%hr + = render 'shared/snippets/blob' -.append-bottom-20 - .pull-right - = "##{@snippet.id}" - %span.light - by - = link_to user_path(@snippet.author) do - = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16" - = @snippet.author_name - - .back-link - = link_to namespace_project_snippets_path(@project.namespace, @project) do - ← project snippets - -.file-holder - .file-title - %i.fa.fa-file - %strong - = @snippet.file_name - .file-actions - .btn-group - - if can?(current_user, :update_project_snippet, @snippet) - = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet' - = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - - if can?(current_user, :admin_project_snippet, @snippet) - = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' - = render 'shared/snippets/blob' - -%div#notes= render "projects/notes/notes_with_form" + %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml new file mode 100644 index 00000000000..667057ef2d8 --- /dev/null +++ b/app/views/projects/tags/_download.html.haml @@ -0,0 +1,17 @@ +%span.btn-group.btn-grouped + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do + %i.fa.fa-download + %span source code + %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %span.caret + %span.sr-only + Select Archive Format + %ul.col-xs-10.dropdown-menu{ role: 'menu' } + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do + %i.fa.fa-download + %span Download zip + %li + = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + %i.fa.fa-download + %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 2ca295fc5f3..28b706c5c7e 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,22 +1,34 @@ - commit = @repository.commit(tag.target) +- release = @releases.find { |release| release.tag == tag.name } %li %div - = link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do %strong - %i.fa.fa-tag + = icon('tag') = tag.name - if tag.message.present? = strip_gpg_signature(tag.message) + .controls - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs' + - if can?(current_user, :download_code, @project) + = render 'projects/tags/download', ref: tag.name, project: @project + + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has_tooltip', title: "Edit release notes" do + = icon("pencil") + - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do - %i.fa.fa-trash-o + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = icon("trash-o") - if commit = render 'projects/branches/commit', commit: commit, project: @project - else %p Cant find HEAD commit for this tag + - if release && release.description.present? + .description.prepend-top-default + .wiki + = preserve do + = markdown release.description diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml deleted file mode 100644 index ada6710f940..00000000000 --- a/app/views/projects/tags/destroy.js.haml +++ /dev/null @@ -1,3 +0,0 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}") -- if @repository.tags.size == 0 - $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 85d76eae3b5..760347de0a9 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -6,7 +6,7 @@ - if can? current_user, :push_code, @project .pull-right = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - %i.fa.fa-add-sign + = icon('plus') New tag .oneline Tags give the ability to mark specific points in history as being important diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 9f5c1be125c..3a2f75fecaa 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -5,33 +5,42 @@ .alert.alert-danger %button{ type: "button", class: "close", "data-dismiss" => "alert"} × = @error + %h3.page-title - %i.fa.fa-code-fork - New tag -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do + New Tag +%hr + += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do .form-group - = label_tag :tag_name, 'Name for new tag', class: 'control-label' + = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :tag_name, params[:tag_name], placeholder: 'v3.0.1', required: true, tabindex: 1, class: 'form-control' + = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref], placeholder: 'master', required: true, tabindex: 2, class: 'form-control' - .light Branch name or commit SHA + = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + .help-block Branch name or commit SHA + .form-group + = label_tag :message, nil, class: 'control-label' + .col-sm-10 + = text_field_tag :message, nil, required: false, tabindex: 3, class: 'form-control' + .help-block Optionally, enter a message to create an annotated tag. + %hr .form-group - = label_tag :message, 'Message', class: 'control-label' + = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 - = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control' - .light (Optional) Entering a message will create an annotated tag. + = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do + = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control' + = 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. .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' :javascript - disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create"); - var availableTags = #{@project.repository.ref_names.to_json}; + var availableRefs = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ - source: availableTags, + source: availableRefs, minLength: 1 }); diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml new file mode 100644 index 00000000000..b594d4f1f27 --- /dev/null +++ b/app/views/projects/tags/show.html.haml @@ -0,0 +1,39 @@ +- page_title @tag.name, "Tags" += render "projects/commits/header_title" += render "projects/commits/head" + +.gray-content-block + .pull-right + - if can?(current_user, :push_code, @project) + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has_tooltip', title: 'Edit release notes' do + = icon("pencil") + = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse files' do + = icon('files-o') + = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse commits' do + = icon('history') + - if can? current_user, :download_code, @project + = render 'projects/tags/download', ref: @tag.name, project: @project + - if can?(current_user, :admin_project, @project) + .pull-right + = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do + %i.fa.fa-trash-o + .title + %strong= @tag.name + - if @tag.message.present? + %span.light + + = strip_gpg_signature(@tag.message) + - if @commit + = render 'projects/branches/commit', commit: @commit, project: @project + - else + Cant find HEAD commit for this tag + + +.append-bottom-default.prepend-top-default + - if @release.description.present? + .description + .wiki + = preserve do + = markdown @release.description + - else + This tag has no release notes. diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index ee4c9d1693d..1927883513a 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -12,7 +12,7 @@ %i.fa.fa-angle-right %small.light - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" – = truncate(@commit.title, length: 50) = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' @@ -29,8 +29,8 @@ - if tree.readme = render "projects/tree/readme", readme: tree.readme -- if allowed_tree_edit? - = render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post +- if can_edit_tree? + = render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post = render 'projects/blob/new_dir' :javascript diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 1115ca6b4ca..3343288ad2b 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -11,22 +11,65 @@ = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' - - if allowed_tree_edit? + + - if current_user %li - %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} + - if !on_top_of_branch? + %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} = icon('plus') - %ul.dropdown-menu - %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do - = icon('pencil fw') - Create file - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do - = icon('file fw') - Upload file - %li.divider - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do - = icon('folder fw') - New directory + - else + %span.dropdown + %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} + = icon('plus') + %ul.dropdown-menu + - if can_edit_tree? + %li + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do + = icon('pencil fw') + New file + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do + = icon('file fw') + Upload file + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do + = icon('folder fw') + New directory + - elsif can?(current_user, :fork_project, @project) + %li + - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('pencil fw') + New file + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to upload a file again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('file fw') + Upload file + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to create a new directory again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id, + continue: continue_params) + = link_to fork_path, method: :post do + = icon('folder fw') + New directory + + %li.divider + %li + = link_to new_namespace_project_branch_path(@project.namespace, @project) do + = icon('code-fork fw') + New branch + %li + = link_to new_namespace_project_tag_path(@project.namespace, @project) do + = icon('tags fw') + New tag diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 18a37302c3e..bd346c4b8e6 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -1,3 +1,4 @@ +- page_title "Triggers" %h3.page-title Triggers @@ -35,7 +36,8 @@ :plain curl -X POST \ -F token=TOKEN \ - #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')} + -F ref=REF_NAME \ + #{builds_trigger_url(@project.id)} %h3 Use .gitlab-ci.yml @@ -50,7 +52,7 @@ trigger: type: deploy script: - - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}" + - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" %h3 Pass build variables @@ -64,5 +66,6 @@ :plain curl -X POST \ -F token=TOKEN \ + -F "ref=REF_NAME" \ -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')} + #{builds_trigger_url(@project.id)} diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index 29416a94ff6..e80dffc1ced 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -1,3 +1,4 @@ +- page_title "Variables" %h3.page-title Secret Variables @@ -9,13 +10,13 @@ %hr -= nested_form_for @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| += nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - if @project.errors.any? #error_explanation - %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:" + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" .alert.alert-error %ul - - @ci_project.errors.full_messages.each do |msg| + - @project.errors.full_messages.each do |msg| %li= msg = f.fields_for :variables do |variable_form| diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 261d4a92d7d..1d257818dcd 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' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f| -if @page.errors.any? #error_explanation .alert.alert-danger @@ -11,24 +11,20 @@ .col-sm-10 = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control" - .row - .col-sm-offset-2.col-sm-10 - %p.cgray - To link to a (new) page you can just type - %code [Link Title](page-slug) - \. - - .form-group.wiki-content + .form-group = 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 js-quick-submit' - .col-sm-12.hint - .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} - .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + = render 'projects/notes/hints' .clearfix .error-alert + + .help-block + To link to a (new) page, simply type + %code [Link Title](page-slug) + \. .form-group = f.label :commit_message, class: 'control-label' .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 14f25822259..29bf5d62abe 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,9 +1,4 @@ %span.pull-right - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new btn-grouped", "data-toggle" => "modal" do - %i.fa.fa-plus - New Page - - if (@page && @page.persisted?) = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do Page History @@ -11,5 +6,7 @@ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit - -= render 'projects/wikis/new' + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do + = icon('trash') + Delete diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index fffb4eb31ab..e6e6ad5bc4b 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,10 +1,19 @@ -%ul.center-top-menu - = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) +.project-issuable-filter + .controls + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + %i.fa.fa-plus + New Page - = nav_link(path: 'wikis#pages') do - = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + = render 'projects/wikis/new' - = nav_link(path: 'wikis#git_access') do - = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do - Git Access + %ul.center-top-menu + = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) + + = nav_link(path: 'wikis#pages') do + = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) + + = nav_link(path: 'wikis#git_access') do + = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do + Git Access diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index dace172438c..f0547e9c057 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -12,5 +12,5 @@ The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and / %p.hint Please don't use spaces. - .modal-footer - = link_to 'Build', '#', class: 'build-new-wiki btn btn-create' + .form-actions + = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 0b709c3695b..23f64fbbd10 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,16 +1,16 @@ -- page_title "Edit", @page.title, "Wiki" +- page_title "Edit", @page.title.capitalize, "Wiki" = render "header_title" = render 'nav' -.pull-right - = render 'main_links' -%h3.page-title - Editing - - %span.light #{@page.title} -%hr -= render 'form' +.gray-content-block + .pull-right + = render 'main_links' + + %h3.page-title.oneline + %span.light Edit Page + - if @page.persisted? + = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) + - else + = @page.title -.pull-right - - if @page.persisted? && can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-sm btn-remove" do - Delete this page += render 'form' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 6417ef4a38b..11c8c4f0eba 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -5,7 +5,7 @@ .gray-content-block .row .col-sm-6 - %h3.page-title + %h3.page-title.oneline Git access for %strong= @project_wiki.path_with_namespace diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index d179a1abec1..aae1ad69ad9 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,11 +1,10 @@ -- page_title "All Pages", "Wiki" +- page_title "Pages", "Wiki" = render "header_title" = render 'nav' .gray-content-block - = render 'main_links' - %h3.page-title - All Pages + All pages in this wiki are listed below. + %ul.content-list - @wiki_pages.each do |wiki_page| %li diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 55fbf5a8b6e..309d40f52bc 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -5,11 +5,12 @@ .gray-content-block = render 'main_links' - %h3.page-title + %h3.page-title.oneline = @page.title.capitalize - .wiki-last-edit-by - Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + %span.wiki-last-edit-by + · + last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} - if @page.historical? .warning_message @@ -21,8 +22,3 @@ .wiki = preserve do = render_wiki_content(@page) - -.gray-content-block.footer-block - .wiki-last-edit-by - Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} - diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index d637abfa76b..481451edb23 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -42,6 +42,13 @@ Wiki %span.badge = @search_results.wiki_blobs_count + %li{class: ("active" if @scope == 'commits')} + = link_to search_filter_path(scope: 'commits') do + = icon('history fw') + %span + Commits + %span.badge + = @search_results.commits_count - elsif @show_snippets %li{class: ("active" if @scope == 'snippet_blobs')} diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml new file mode 100644 index 00000000000..4e6c3965dc6 --- /dev/null +++ b/app/views/search/results/_commit.html.haml @@ -0,0 +1,2 @@ +.search-result-row + = render 'projects/commits/commit', project: @project, commit: commit diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index ce8ddff9556..45d700781f3 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -6,7 +6,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(issue.description)) + = search_md_sanitize(markdown(issue.description, { project: issue.project })) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 2e4aab36301..687a59c270f 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,27 +1,27 @@ - project = project || @project -.git-clone-holder.input-group - .input-group-addon.git-protocols - .input-group-btn - %button{ | - type: 'button', | - class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | - :"data-clone" => project.ssh_url_to_repo, | - :"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH.", - :"data-html" => "true", - :"data-container" => "body"} - SSH - .input-group-btn - %button{ | - type: 'button', | - class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | - :"data-clone" => project.http_url_to_repo, | - :"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}.", - :"data-html" => "true", - :"data-container" => "body"} - = gitlab_config.protocol.upcase + +.git-clone-holder + .btn-group.clone-options + %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'} + %span + = default_clone_protocol.upcase + = icon('angle-down') + %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown + %li + %a#ssh-selector{href: @project.ssh_url_to_repo} + SSH + %li + %a#http-selector{href: @project.http_url_to_repo} + HTTPS + = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true - - if project.kind_of?(Project) - .input-group-addon - .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } - = visibility_level_icon(project.visibility_level) - + .input-group-btn + = clipboard_button(clipboard_target: '#project_clone') + +:javascript + $('ul.clone-options-dropdown a').on('click',function(e){ + e.preventDefault(); + var $this = $(this); + $('a.clone-dropdown-btn span').text($this.text()); + $('#project_clone').val($this.attr('href')); + }); diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index cc3f1268f8b..7c57924277e 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,13 +1,15 @@ .form-group.commit_message-group - = label_tag 'commit_message', class: 'control-label' do + - nonce = SecureRandom.hex + = label_tag "commit_message-#{nonce}", class: 'control-label' do Commit message .col-sm-10 .commit-message-container .max-width-marker = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text]), - class: 'form-control js-quick-submit', placeholder: local_assigns[:placeholder], - required: true, rows: (local_assigns[:rows] || 3) + class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder], + required: true, rows: (local_assigns[:rows] || 3), + id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint Try to keep the first line under 52 characters diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 5f51b0d450f..34241cd8aad 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -3,7 +3,8 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h4 Confirmation required + %h3.page-title + Confirmation required .modal-body %p.cred.lead.js-confirm-text @@ -14,9 +15,9 @@ %br Please type %code.js-confirm-danger-match #{phrase} - to proceed or close this modal to cancel + to proceed or close this modal to cancel. .form-group = text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input' - .form-group + .form-actions = submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit" diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 57c3aff3e18..2bc98983d67 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -8,5 +8,6 @@ %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} %i.fa.fa-link = i - :preserve - #{highlight(blob.name, blob.data)} + .blob-content{data: {blob_id: blob.id}} + :preserve + #{highlight(blob.name, blob.data)} diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index c0a9923348e..67072b9fc2a 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -23,7 +23,7 @@ %li It will change the git path to repositories under this group. .form-group.group-description-holder - = f.label :description, 'Details', class: 'control-label' + = f.label :description, class: 'control-label' .col-sm-10 = f.text_area :description, maxlength: 250, class: 'form-control js-gfm-input', rows: 4 diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml new file mode 100644 index 00000000000..285af56ad73 --- /dev/null +++ b/app/views/shared/_import_form.html.haml @@ -0,0 +1,16 @@ +.form-group.import-url-data + = f.label :import_url, class: 'control-label' do + %span Git repository URL + .col-sm-10 + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + + .well.prepend-top-20 + %ul + %li + The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>. + %li + If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + %li + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 0dbb6a04393..4b4c9e9eabe 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -3,8 +3,10 @@ .panel.panel-default.panel-small - project = group[0] .panel-heading - = link_to_project project - = link_to 'show all', namespace_project_issues_path(project.namespace, project), class: 'pull-right' + = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project) + - if can?(current_user, :create_issue, project) + .pull-right + = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project) %ul.well-list.issues-list - group[1].each do |issue| @@ -12,4 +14,3 @@ = paginate @issues, theme: "gitlab" - else .nothing-here-block No issues to show - diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index da49c48acd3..3d279ec228c 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -5,13 +5,13 @@ <g id="Fill-1-+-Group-24"> <g id="Group-24"> <g id="Group"> - <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path> - <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path> - <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path> - <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path> - <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path> - <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path> - <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path> + <path id="tanuki-right-ear" d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" fill="#E24329" class="tanuki-shape"></path> + <path id="tanuki-right-cheek" d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path> + <path id="tanuki-right-eye" d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path> + <path id="tanuki-nose" d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" fill="#E24329" class="tanuki-shape"></path> + <path id="tanuki-left-eye" d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path> + <path id="tanuki-left-cheek" d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path> + <path id="tanuki-left-ear" d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" fill="#E24329" class="tanuki-shape"></path> </g> </g> </g> diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index c02c5af008a..be17a511b26 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -3,8 +3,11 @@ .panel.panel-default.panel-small - project = group[0] .panel-heading - = link_to_project project - = link_to 'show all', namespace_project_merge_requests_path(project.namespace, project), class: 'pull-right' + = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project) + - if can?(current_user, :create_merge_request, project) + .pull-right + = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project) + %ul.well-list.mr-list - group[1].each do |merge_request| = render 'projects/merge_requests/merge_request', merge_request: merge_request diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml new file mode 100644 index 00000000000..b8eef15fbec --- /dev/null +++ b/app/views/shared/_milestone_expired.html.haml @@ -0,0 +1,5 @@ +- if milestone.expired? and not milestone.closed? + %span.cred (Expired) +- if milestone.expires_at + %span + = milestone.expires_at diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml new file mode 100644 index 00000000000..0c8ac48bb58 --- /dev/null +++ b/app/views/shared/_new_commit_form.html.haml @@ -0,0 +1,22 @@ += render 'shared/commit_message_container', placeholder: placeholder + +- if @project.empty_repo? + = hidden_field_tag 'target_branch', @ref +- else + - if can?(current_user, :push_code, @project) + .form-group.branch + = label_tag 'target_branch', 'Target branch', class: 'control-label' + .col-sm-10 + = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch" + + .js-create-merge-request-container + .checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + Start a <strong>new merge request</strong> with these changes + - else + = hidden_field_tag 'target_branch', @target_branch || tree_edit_branch + = hidden_field_tag 'create_merge_request', 1 + + = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml new file mode 100644 index 00000000000..c4431d66927 --- /dev/null +++ b/app/views/shared/_new_project_item_select.html.haml @@ -0,0 +1,20 @@ +- if @projects.any? + .prepend-left-10.new-project-item-select-holder + = project_select_tag :project_path, class: "new-project-item-select", data: { include_groups: local_assigns[:include_groups] } + %a.btn.btn-new.new-project-item-select-button + = icon('plus') + = local_assigns[:label] + %b.caret + + :javascript + $('.new-project-item-select-button').on('click', function() { + $('.new-project-item-select').select2('open'); + }); + + var relativePath = '#{local_assigns[:path]}'; + + $('.new-project-item-select').on('click', function() { + window.location = $(this).val() + '/' + relativePath; + }); + + new ProjectSelect() diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml new file mode 100644 index 00000000000..960ff00b49d --- /dev/null +++ b/app/views/shared/_project_limit.html.haml @@ -0,0 +1,8 @@ +- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? + .project-limit-message.alert.alert-warning.hidden-xs + You won't be able to create new projects because you have reached your project limit. + + .pull-right + = link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link' + | + = link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link' diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 16a98a7233c..28d6f421fea 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -59,6 +59,15 @@ %strong Merge Request events %p.light This url will be triggered when a merge request is created + - if @service.supported_events.include?("build") + %div + = form.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = form.label :build_events, class: 'list-label' do + %strong Build events + %p.light + This url will be triggered when a build status changes + - @service.fields.each do |field| - type = field[:type] diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml deleted file mode 100644 index cba18c14568..00000000000 --- a/app/views/shared/issuable/_context.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| - %div.prepend-top-20 - .issuable-context-title - %label - Assignee: - - if issuable.assignee - %strong= link_to_member(@project, issuable.assignee, size: 24) - - else - none - .issuable-context-selectbox - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true) - - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Milestone: - - if issuable.milestone - %span.back-to-milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %strong - = icon('clock-o') - = issuable.milestone.title - - else - none - .issuable-context-selectbox - - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'}) - = hidden_field_tag :issuable_context - = f.submit class: 'btn hide' - - - if current_user - - subscribed = issuable.subscribed?(current_user) - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Subscription: - %button.btn.btn-block.subscribe-button{:type => 'button'} - = icon('eye') - %span= subscribed ? 'Unsubscribe' : 'Subscribe' - - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - .subscription-status{data: {status: subscribtion_status}} - .description-block.unsubscribed{class: ( 'hidden' if subscribed )} - You're not receiving notifications from this thread. - .description-block.subscribed{class: ( 'hidden' unless subscribed )} - You're receiving notifications because you're subscribed to this thread. - -:coffeescript - new Subscription("#{toggle_subscription_path(issuable)}") - new IssuableContext() diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0e4e9c0987a..be06738eac9 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -30,12 +30,12 @@ class: "check_all_issues left" .issues-other-filters .filter-item.inline - = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true, current_user: true) + = users_select_tag(:author_id, selected: params[:author_id], + placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) .filter-item.inline - = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true, current_user: true) + = users_select_tag(:assignee_id, selected: params[:assignee_id], + placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true) .filter-item.inline.milestone-filter = select_tag('milestone_title', projects_milestones_options, @@ -53,16 +53,20 @@ - if controller.controller_name == 'issues' .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + .filter-item.inline + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" }) + .filter-item.inline + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) + .filter-item.inline + = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] - = button_tag "Update issues", class: "btn update_selected_issues btn-save" - -:coffeescript - new UsersSelect() + .filter-item.inline + = button_tag "Update issues", class: "btn update_selected_issues btn-save" - $('form.filter-form').on 'submit', (event) -> - event.preventDefault() - Turbolinks.visit @.action + '&' + $(@).serialize() +:javascript + new UsersSelect(); + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 594e54f404c..90dc0062481 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -6,8 +6,7 @@ %span= msg %br .form-group - = f.label :title, class: 'control-label' do - %strong= 'Title *' + = 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 js-quick-submit', required: true @@ -20,46 +19,35 @@ - else Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a <strong>Work In Progress</strong> merge request from being merged before it's ready. -.form-group.issuable-description +.form-group.detail-page-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 = 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 js-quick-submit' - .col-sm-12.hint - .pull-left - Parsed with - #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. - .pull-right - Attach files by dragging & dropping - or #{link_to 'selecting them', '#', class: 'markdown-selector' }. - + = render 'projects/notes/hints' .clearfix .error-alert - %hr - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + %hr .form-group .issue-assignee - = f.label :assignee_id, class: 'control-label' do - %i.fa.fa-user - Assign to + = f.label :assignee_id, "Assignee", class: 'control-label' .col-sm-10 = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select a user', class: 'custom-form-control', null_user: true, + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true) + first_user: true, current_user: true, include_blank: true) = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group .issue-milestone - = f.label :milestone_id, class: 'control-label' do - %i.fa.fa-clock-o - Milestone + = f.label :milestone_id, "Milestone", class: 'control-label' .col-sm-10 - if milestone_options(issuable).present? = f.select(:milestone_id, milestone_options(issuable), - { include_blank: 'Select milestone' }, { class: 'select2' }) + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - else .prepend-top-10 %span.light No open milestones available. @@ -67,13 +55,11 @@ - 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 - = f.label :label_ids, class: 'control-label' do - %i.fa.fa-tag - Labels + = 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' + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else .prepend-top-10 %span.light No labels yet. @@ -85,31 +71,30 @@ %hr - if @merge_request.new_record? .form-group - = f.label :source_branch, class: 'control-label' do - %i.fa.fa-code-fork - Source Branch + = 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 }) .form-group - = f.label :target_branch, class: 'control-label' do - %i.fa.fa-code-fork - Target Branch + = f.label :target_branch, class: 'control-label' .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) + = 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) -.form-actions - - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted? - %p - Please review the - %strong #{link_to 'guidelines for contribution', guide_url} - to this repository. +- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) +.gray-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if issuable.new_record? - = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' + = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else = f.submit 'Save changes', class: 'btn btn-save' + + - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) + .inline.prepend-left-10 + Please review the + %strong #{link_to 'contribution guidelines', guide_url} + for this project. + - if issuable.new_record? - cancel_project = issuable.source_project - else diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml new file mode 100644 index 00000000000..da6bacbb74a --- /dev/null +++ b/app/views/shared/issuable/_participants.html.haml @@ -0,0 +1,5 @@ +.block.participants + .title + = pluralize participants.count, "participant" + - participants.each do |participant| + = link_to_member(@project, participant, name: false, size: 24) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml new file mode 100644 index 00000000000..79c5cc7f40a --- /dev/null +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -0,0 +1,83 @@ +.issuable-sidebar.issuable-affix + = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| + .block.assignee + .title + %label + Assignee + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .pull-right + = link_to 'Edit', '#', class: 'edit-link' + .value + - if issuable.assignee + %strong= link_to_member(@project, issuable.assignee, size: 24) + - else + .light None + + .selectbox + = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) + + .block.milestone + .title + %label + Milestone + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .pull-right + = link_to 'Edit', '#', class: 'edit-link' + .value + - if issuable.milestone + %span.back-to-milestone + = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do + %strong + = icon('clock-o') + = issuable.milestone.title + - else + .light None + .selectbox + = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) + = hidden_field_tag :issuable_context + = f.submit class: 'btn hide' + + - if issuable.project.labels.any? + .block + .title + %label Labels + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .pull-right + = link_to 'Edit', '#', class: 'edit-link' + .value.issuable-show-labels + - if issuable.labels.any? + - issuable.labels.each do |label| + = link_to_label(label) + - else + .light None + .selectbox + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } + + .block + .title + Cross-project reference + .cross-project-reference + %span#cross-project-reference + = cross_project_reference(@project, issuable) + = clipboard_button(clipboard_target: 'span#cross-project-reference') + + = render "shared/issuable/participants", participants: issuable.participants(current_user) + + - if current_user + - subscribed = issuable.subscribed?(current_user) + .block.light + .title + %label.light Notifications + - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' + %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'} + %span= subscribed ? 'Unsubscribe' : 'Subscribe' + .subscription-status{data: {status: subscribtion_status}} + .unsubscribed{class: ( 'hidden' if subscribed )} + You're not receiving notifications from this thread. + .subscribed{class: ( 'hidden' unless subscribed )} + You're receiving notifications because you're subscribed to this thread. + + :javascript + new Subscription("#{toggle_subscription_path(issuable)}"); + new IssuableContext(); diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 357cfd6a370..e5ffe1e29ae 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -17,5 +17,5 @@ = link_to '#', class: 'js-expand' do Show all -:coffeescript - new ProjectsList() +:javascript + new ProjectsList(); diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index aee839b44e7..86249851a82 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -5,7 +5,7 @@ - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" unless project.description.present? %li.project-row{ class: css_class } - = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do + = cache [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] do = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar @@ -21,9 +21,7 @@ .project-controls - if ci && !project.empty_repo? && project.commit - if ci_commit = project.ci_commit(project.commit.sha) - = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do - = ci_status_icon(ci_commit) + = render_ci_status(ci_commit) - if stars %span diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 913b6744844..1041eccd1df 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,5 +1,5 @@ .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form" } do |f| + = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| - if @snippet.errors.any? .alert.alert-danger %ul @@ -8,7 +8,8 @@ .form-group = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true + .col-sm-10 + = f.text_field :title, class: 'form-control', required: true, autofocus: true = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet @@ -27,7 +28,7 @@ - if @snippet.new_record? = f.submit 'Create snippet', class: "btn-create btn" - else - = f.submit 'Save', class: "btn-save btn" + = f.submit 'Save changes', class: "btn-save btn" - if @snippet.project_id = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml new file mode 100644 index 00000000000..aa5acee9c14 --- /dev/null +++ b/app/views/shared/snippets/_header.html.haml @@ -0,0 +1,25 @@ +.detail-page-header + .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} + = visibility_level_icon(@snippet.visibility_level, fw: false) + = visibility_level_label(@snippet.visibility_level) + %span.identifier + Snippet ##{@snippet.id} + %span.creator + · created by #{link_to_member(@project, @snippet.author, size: 24)} + · + = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') + - if @snippet.updated_at != @snippet.created_at + %span + · + = icon('edit', title: 'edited') + = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') + + .pull-right + - if @snippet.project_id? + = render "projects/snippets/actions" + - else + = render "snippets/actions" + +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(@snippet.title), pipeline: :single_line diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 69a713ad9aa..c6294caddc7 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -18,4 +18,3 @@ = image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: '' = snippet.author_name authored #{time_ago_with_tooltip(snippet.created_at)} - diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml new file mode 100644 index 00000000000..cfd11e45b6a --- /dev/null +++ b/app/views/sherlock/file_samples/show.html.haml @@ -0,0 +1,55 @@ +- page_title t('sherlock.title'), t('sherlock.transaction'), + t('sherlock.file_sample') + +- header_title t('sherlock.title'), sherlock_transactions_path + +.gray-content-block + .pull-right + = link_to(sherlock_transaction_path(@transaction), class: 'btn') do + %i.fa.fa-arrow-left + = t('sherlock.transaction') + .oneline + = t('sherlock.file_sample') + = @file_sample.id + +.prepend-top-default + %p + %span.light + #{t('sherlock.time')}: + %strong + = @file_sample.duration.round(2) + = t('sherlock.milliseconds') + %p + %span.light + #{t('sherlock.events')}: + %strong + = @file_sample.events + +%article.file-holder + .file-title + %i.fa.fa-file-text-o.fa-fw + %strong + = @file_sample.file + .code.file-content.js-syntax-highlight + .line-numbers + %table.sherlock-line-samples-table + %thead + %tr + %th= t('sherlock.line_capitalized') + %th= t('sherlock.events') + %th= t('sherlock.time') + %th= t('sherlock.percent') + %tbody + - @file_sample.line_samples.each_with_index do |sample, index| + %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''} + %td= index + 1 + %td= sample.events + %td + = sample.duration.round(2) + = t('sherlock.milliseconds') + %td + = sample.percentage_of(@file_sample.duration).round + = t('sherlock.percent') + + .sherlock-file-sample + = highlight(@file_sample.file, @file_sample.source) diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml new file mode 100644 index 00000000000..5c9294c0ab5 --- /dev/null +++ b/app/views/sherlock/queries/_backtrace.html.haml @@ -0,0 +1,27 @@ +.prepend-top-default + .panel.panel-default + .panel-heading + %strong + = t('sherlock.application_backtrace') + %ul.well-list + - @query.application_backtrace.each do |location| + %li + = location.path + %small.light + = t('sherlock.line') + = location.line + + .panel.panel-default + .panel-heading + %strong + = t('sherlock.full_backtrace') + %ul.well-list + - @query.backtrace.each do |location| + %li + - if location.application? + %strong= location.path + - else + = location.path + %small.light + = t('sherlock.line') + = location.line diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml new file mode 100644 index 00000000000..549b47430e6 --- /dev/null +++ b/app/views/sherlock/queries/_general.html.haml @@ -0,0 +1,50 @@ +.prepend-top-default + .panel.panel-default + .panel-heading + %strong + = t('sherlock.general') + %ul.well-list + %li + %span.light + #{t('sherlock.time')}: + %strong + = @query.duration.round(4) + = t('sherlock.milliseconds') + %li + %span.light + #{t('sherlock.origin')}: + %strong + = @query.last_application_frame.path + %small.light + = t('sherlock.line') + = @query.last_application_frame.line + + .panel.panel-default + .panel-heading + .pull-right + %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %i.fa.fa-clipboard + %pre.hidden + = @query.formatted_query + %strong + = t('sherlock.query') + %ul.well-list + %li + .code.js-syntax-highlight.sherlock-code + :preserve + #{highlight("#{@query.id}.sql", @query.formatted_query)} + + .panel.panel-default + .panel-heading + .pull-right + %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button} + %i.fa.fa-clipboard + %pre.hidden + = @query.explain + %strong + = t('sherlock.query_plan') + %ul.well-list + %li + .code.js-syntax-highlight.sherlock-code + %pre + %code= @query.explain diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml new file mode 100644 index 00000000000..4a84348ac82 --- /dev/null +++ b/app/views/sherlock/queries/show.html.haml @@ -0,0 +1,26 @@ +- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query') +- header_title t('sherlock.title'), sherlock_transactions_path + +%ul.center-top-menu + %li.active + %a(href="#tab-general" data-toggle="tab") + = t('sherlock.general') + %li + %a(href="#tab-backtrace" data-toggle="tab") + = t('sherlock.backtrace') + +.gray-content-block + .pull-right + = link_to(sherlock_transaction_path(@transaction), class: 'btn') do + %i.fa.fa-arrow-left + = t('sherlock.transaction') + .oneline + = t('sherlock.query') + = @query.id + +.tab-content + .tab-pane.active#tab-general + = render(partial: 'general') + + .tab-pane#tab-backtrace + = render(partial: 'backtrace') diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml new file mode 100644 index 00000000000..4349c9b7ace --- /dev/null +++ b/app/views/sherlock/transactions/_file_samples.html.haml @@ -0,0 +1,24 @@ +- if @transaction.file_samples.empty? + .nothing-here-block + = t('sherlock.no_file_samples') +- else + .table-holder + %table.table + %thead + %tr + %th= t('sherlock.time_inclusive') + %th= t('sherlock.count') + %th= t('sherlock.path') + %th + %tbody + - @transaction.sorted_file_samples.each do |sample| + %tr + %td + = sample.duration.round(2) + = t('sherlock.milliseconds') + %td= @transaction.view_counts.fetch(sample.file, 1) + %td= sample.relative_path + %td + = link_to(t('sherlock.view'), + sherlock_transaction_file_sample_path(@transaction, sample), + class: 'btn btn-xs') diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml new file mode 100644 index 00000000000..8533b130da6 --- /dev/null +++ b/app/views/sherlock/transactions/_general.html.haml @@ -0,0 +1,39 @@ +.prepend-top-default + .panel.panel-default + .panel-heading + %strong + = t('sherlock.general') + %ul.well-list + %li + %span.light + #{t('sherlock.id')}: + %strong + = @transaction.id + %li + %span.light + #{t('sherlock.type')}: + %strong + = @transaction.type + %li + %span.light + #{t('sherlock.path')}: + %strong + = @transaction.path + %li + %span.light + #{t('sherlock.time')}: + %strong + = @transaction.duration.round(2) + = t('sherlock.seconds') + %li + %span.light + #{t('sherlock.query_time')} + %strong + = @transaction.query_duration.round(2) + = t('sherlock.seconds') + %li + %span.light + #{t('sherlock.finished_at')}: + %strong + = time_ago_in_words(@transaction.finished_at) + = t('sherlock.ago') diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml new file mode 100644 index 00000000000..b7e0162e80d --- /dev/null +++ b/app/views/sherlock/transactions/_queries.html.haml @@ -0,0 +1,24 @@ +- if @transaction.queries.empty? + .nothing-here-block + = t('sherlock.no_queries') +- else + .table-holder + %table.table#sherlock-queries + %thead + %tr + %th= t('sherlock.time') + %th= t('sherlock.query') + %td + %tbody + - @transaction.sorted_queries.each do |query| + %tr + %td + = query.duration.round(2) + = t('sherlock.milliseconds') + %td + .code.js-syntax-highlight.sherlock-code + = highlight("#{query.id}.sql", query.formatted_query) + %td + = link_to(t('sherlock.view'), + sherlock_transaction_query_path(@transaction, query), + class: 'btn btn-xs') diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml new file mode 100644 index 00000000000..010e1a2a902 --- /dev/null +++ b/app/views/sherlock/transactions/index.html.haml @@ -0,0 +1,42 @@ +- page_title t('sherlock.title') +- header_title t('sherlock.title'), sherlock_transactions_path + +.gray-content-block + .pull-right + = link_to(destroy_all_sherlock_transactions_path, + class: 'btn btn-danger', + method: :delete) do + %i.fa.fa-trash + = t('sherlock.delete_all_transactions') + .oneline= t('sherlock.introduction') + +- if @transactions.empty? + .nothing-here-block= t('sherlock.no_transactions') +- else + .table-holder + %table.table + %thead + %tr + %th= t('sherlock.type') + %th= t('sherlock.path') + %th= t('sherlock.time') + %th= t('sherlock.queries') + %th= t('sherlock.finished_at') + %th + %tbody + - @transactions.each do |trans| + %tr + %td= trans.type + %td + %span{title: trans.path} + = truncate(trans.path, length: 70) + %td + = trans.duration.round(2) + = t('sherlock.seconds') + %td= trans.queries.length + %td + = time_ago_in_words(trans.finished_at) + = t('sherlock.ago') + %td + = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do + = t('sherlock.view') diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml new file mode 100644 index 00000000000..3c8ffb06648 --- /dev/null +++ b/app/views/sherlock/transactions/show.html.haml @@ -0,0 +1,36 @@ +- page_title t('sherlock.title'), t('sherlock.transaction') +- header_title t('sherlock.title'), sherlock_transactions_path + +%ul.center-top-menu + %li.active + %a(href="#tab-general" data-toggle="tab") + = t('sherlock.general') + %li + %a(href="#tab-queries" data-toggle="tab") + = t('sherlock.queries') + %span.badge + #{@transaction.queries.length} + %li + %a(href="#tab-file-samples" data-toggle="tab") + = t('sherlock.file_samples') + %span.badge + #{@transaction.file_samples.length} + +.gray-content-block + .pull-right + = link_to(sherlock_transactions_path, class: 'btn') do + %i.fa.fa-arrow-left + = t('sherlock.all_transactions') + .oneline + = t('sherlock.transaction') + = @transaction.id + +.tab-content + .tab-pane.active#tab-general + = render(partial: 'general') + + .tab-pane#tab-queries + = render(partial: 'queries') + + .tab-pane#tab-file-samples + = render(partial: 'file_samples') diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml new file mode 100644 index 00000000000..1979ae6d5bc --- /dev/null +++ b/app/views/snippets/_actions.html.haml @@ -0,0 +1,11 @@ += link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do + = icon('plus') + New Snippet +- if can?(current_user, :update_personal_snippet, @snippet) + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + = icon('pencil-square-o') + Edit +- if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do + = icon('trash-o') + Delete diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 1a380035661..82f44a9a5c3 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,5 +1,5 @@ - page_title "Edit", @snippet.title, "Snippets" %h3.page-title - Edit snippet + Edit Snippet %hr = render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index a74d5e792ad..79e2392490d 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,5 +1,5 @@ - page_title "New Snippet" %h3.page-title - New snippet + New Snippet %hr = render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 97374e073dc..a2b36568770 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,41 +1,14 @@ - page_title @snippet.title, "Snippets" -%h4.page-title - = @snippet.title - - if @snippet.private? - %span.label.label-success - %i.fa.fa-lock - private - - .pull-right - = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do - Add new snippet - -.append-bottom-10.prepend-top-10 - .pull-right - %span.light - created by - = link_to user_snippets_path(@snippet.author) do - = @snippet.author_name - - .back-link - - if @snippet.author == current_user - = link_to dashboard_snippets_path do - ← your snippets - - else - = link_to explore_snippets_path do - ← explore snippets - -.file-holder - .file-title - %i.fa.fa-file - %strong - = @snippet.file_name - .file-actions - .btn-group - - if can?(current_user, :update_personal_snippet, @snippet) - = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet' - = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' - = render 'shared/snippets/blob' +.snippet-holder + = render 'shared/snippets/header' + + %article.file-holder + .file-title + = blob_icon 0, @snippet.file_name + %strong + = @snippet.file_name + .file-actions.hidden-xs + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 50232dc7186..2fe5b7fac83 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.id user_url(@user) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 4ea4a1f92c2..0bca8177e14 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,6 @@ -- page_title @user.name -- header_title @user.name, user_path(@user) +- page_title @user.name +- page_description @user.bio +- header_title @user.name, user_path(@user) = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") @@ -24,22 +25,27 @@ .cover-desc - unless @user.public_email.blank? - = link_to @user.public_email, "mailto:#{@user.public_email}" + .profile-link-holder + = link_to @user.public_email, "mailto:#{@user.public_email}" - unless @user.skype.blank? - · - = link_to "Skype", "skype:#{@user.skype}" + .profile-link-holder + = link_to "skype:#{@user.skype}", title: "Skype" do + = icon('skype') - unless @user.linkedin.blank? - · - = link_to "LinkedIn", "http://www.linkedin.com/in/#{@user.linkedin}" + .profile-link-holder + = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do + = icon('linkedin-square') - unless @user.twitter.blank? - · - = link_to "Twitter", "http://www.twitter.com/#{@user.twitter}" + .profile-link-holder + = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do + = icon('twitter-square') - unless @user.website_url.blank? - · - = link_to @user.short_website_url, @user.full_website_url + .profile-link-holder + = link_to @user.short_website_url, @user.full_website_url - unless @user.location.blank? - · - = @user.location + .profile-link-holder + = icon('map-marker') + = @user.location .cover-controls @@ -47,7 +53,7 @@ = link_to profile_path, class: 'btn btn-gray' do = icon('pencil') - elsif current_user - .report-abuse + %span.report-abuse - if @user.abuse_report %button.btn.btn-danger{ title: 'Already reported for abuse', data: { toggle: 'tooltip', placement: 'left', container: 'body' }} @@ -56,6 +62,10 @@ = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray', title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do = icon('exclamation-circle') + - if current_user + + = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do + = icon('rss') .gray-content-block.second-block .user-calendar @@ -64,27 +74,47 @@ .user-calendar-activities -.row.prepend-top-20 - %section.col-md-7 - - if @groups.any? - .prepend-top-20 - %h4 Groups - = render 'groups', groups: @groups - %hr - - %h4 - User Activity - - - if current_user - %span.rss-icon.pull-right - = link_to user_path(@user, :atom, { private_token: current_user.private_token }) do - %strong - %i.fa.fa-rss +%ul.center-top-menu.no-top.no-bottom.bottom-border.wide + %li.active + = link_to "#activity", 'data-toggle' => 'tab' do + Activity + - if @groups.any? + %li + = link_to "#groups", 'data-toggle' => 'tab' do + Groups + - if @contributed_projects.present? + %li + = link_to "#contributed", 'data-toggle' => 'tab' do + Contributed projects + - if @projects.present? + %li + = link_to "#personal", 'data-toggle' => 'tab' do + Personal projects +.tab-content + .tab-pane.active#activity .content_list = spinner - %aside.col-md-5 - = render 'projects', projects: @projects, contributed_projects: @contributed_projects -:coffeescript - $(".user-calendar").load("#{user_calendar_path}") + - if @groups.any? + .tab-pane#groups + %ul.content-list + - @groups.each do |group| + = render 'shared/groups/group', group: group + + - if @contributed_projects.present? + .tab-pane#contributed + .contributed-projects + = render 'shared/projects/list', + projects: @contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: true + + - if @projects.present? + .tab-pane#personal + .personal-projects + = render 'shared/projects/list', + projects: @projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: true + +:javascript + $(".user-calendar").load("#{user_calendar_path}"); diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 36ea6742064..ce0a0113403 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,10 +1,46 @@ -.votes.votes-block - .btn-group - - unless votable.upvotes.zero? - .btn.btn-sm.disabled.cgreen - %i.fa.fa-thumbs-up - = votable.upvotes - - unless votable.downvotes.zero? - .btn.btn-sm.disabled.cred - %i.fa.fa-thumbs-down - = votable.downvotes +.awards.votes-block + - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| + .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)} + = emoji_icon(emoji) + .counter + = notes.count + + - if current_user + .awards-controls + %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"} + = icon('smile-o') + .emoji-menu + .emoji-menu-content + = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" + - AwardEmoji.emoji_by_category.each do |category, emojis| + %h5= AwardEmoji::CATEGORIES[category] + %ul + - emojis.each do |emoji| + %li + = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) + +- if current_user + :coffeescript + post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}" + noteable_type = "#{votable.class.name.underscore}" + noteable_id = "#{votable.id}" + aliases = #{AwardEmoji.aliases.to_json} + + window.awards_handler = new AwardsHandler( + post_emoji_url, + noteable_type, + noteable_id, + aliases + ) + + $(".awards").on "click", ".emoji-menu-content li", (e) -> + emoji = $(this).find(".emoji-icon").data("emoji") + awards_handler.addAward(emoji) + + $(".awards").on "click", ".award", (e) -> + emoji = $(this).find(".icon").data("emoji") + awards_handler.addAward(emoji) + + $(".award").tooltip() + + $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false}) diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml deleted file mode 100644 index 2cb3ae04e1a..00000000000 --- a/app/views/votes/_votes_inline.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.votes.votes-inline - - unless votable.upvotes.zero? - %span.upvotes.cgreen - + #{votable.upvotes} - - unless votable.downvotes.zero? - \/ - - unless votable.downvotes.zero? - %span.downvotes.cred - \- #{votable.downvotes} diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb new file mode 100644 index 00000000000..1c7a04a66a8 --- /dev/null +++ b/app/workers/build_email_worker.rb @@ -0,0 +1,19 @@ +class BuildEmailWorker + include Sidekiq::Worker + + def perform(build_id, recipients, push_data) + recipients.each do |recipient| + begin + case push_data['build_status'] + when 'success' + Notify.build_success_email(build_id, recipient).deliver_now + when 'failed' + Notify.build_fail_email(build_id, recipient).deliver_now + end + # These are input errors and won't be corrected even if Sidekiq retries + rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e + logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}") + end + end + end +end diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb deleted file mode 100644 index ebb43570e2a..00000000000 --- a/app/workers/ci/hip_chat_notifier_worker.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class HipChatNotifierWorker - include Sidekiq::Worker - - def perform(message, options={}) - room = options.delete('room') - token = options.delete('token') - server = options.delete('server') - name = options.delete('service_name') - client_opts = { - api_version: 'v2', - server_url: server - } - - client = HipChat::Client.new(token, client_opts) - client[room].send(name, message, options.symbolize_keys) - end - end -end diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb deleted file mode 100644 index 3bbb9b4bec7..00000000000 --- a/app/workers/ci/slack_notifier_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Ci - class SlackNotifierWorker - include Sidekiq::Worker - - def perform(webhook_url, message, options={}) - notifier = Slack::Notifier.new(webhook_url) - notifier.ping(message, options) - end - end -end diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb deleted file mode 100644 index 0bb83845572..00000000000 --- a/app/workers/ci/web_hook_worker.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Ci - class WebHookWorker - include Sidekiq::Worker - - def perform(hook_id, data) - Ci::WebHook.find(hook_id).execute data - end - end -end diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 5a921a73fe9..f2649e38eb3 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -46,6 +46,6 @@ class EmailReceiverWorker return end - EmailRejectionMailer.delay.rejection(reason, raw, can_retry) + EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later end end diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 916a99bb273..c4d8595d45d 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -53,7 +53,7 @@ class EmailsOnPushWorker reverse_compare: reverse_compare, send_from_committer_email: send_from_committer_email, disable_diffs: disable_diffs - ).deliver + ).deliver_now # These are input errors and won't be corrected even if Sidekiq retries rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 5d1a8555b7d..c87c0a252b1 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -8,16 +8,7 @@ class MergeWorker current_user = User.find(current_user_id) merge_request = MergeRequest.find(merge_request_id) - result = MergeRequests::MergeService.new(merge_request.target_project, current_user). - execute(merge_request, params[:commit_message]) - - if result[:status] == :success && params[:should_remove_source_branch].present? - DeleteBranchService.new(merge_request.source_project, current_user). - execute(merge_request.source_branch) - - merge_request.source_project.repository.expire_branch_names - end - - result + MergeRequests::MergeService.new(merge_request.target_project, current_user, params). + execute(merge_request) end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index acd1c43f06b..2f991c52339 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -13,22 +13,20 @@ class RepositoryForkWorker end result = gitlab_shell.fork_repository(source_path, target_path) - unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") + project.update(import_error: "The project could not be forked.") project.import_fail - project.save return end - if project.valid_repo? - ProjectCacheWorker.perform_async(project.id) - project.import_finish - else - project.import_fail + unless project.valid_repo? logger.error("Project #{id} had an invalid repository after fork") + project.update(import_error: "The forked repository is invalid.") + project.import_fail + return end - project.save + project.import_finish end end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index ea2808045eb..d18c0706b30 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -7,37 +7,49 @@ class RepositoryImportWorker def perform(project_id) project = Project.find(project_id) - unless project.import_url == Project::UNKNOWN_IMPORT_URL - import_result = gitlab_shell.send(:import_repository, - project.path_with_namespace, - project.import_url) - return project.import_fail unless import_result - else + if project.import_url == Project::UNKNOWN_IMPORT_URL + # In this case, we only want to import issues, not a repository. unless project.create_repository - return project.import_fail + project.update(import_error: "The repository could not be created.") + project.import_fail + return + end + else + begin + gitlab_shell.import_repository(project.path_with_namespace, project.import_url) + rescue Gitlab::Shell::Error => e + project.update(import_error: e.message) + project.import_fail + return end end - data_import_result = case project.import_type - when 'github' - Gitlab::GithubImport::Importer.new(project).execute - when 'gitlab' - Gitlab::GitlabImport::Importer.new(project).execute - when 'bitbucket' - Gitlab::BitbucketImport::Importer.new(project).execute - when 'google_code' - Gitlab::GoogleCodeImport::Importer.new(project).execute - when 'fogbugz' - Gitlab::FogbugzImport::Importer.new(project).execute - else - true - end - return project.import_fail unless data_import_result + data_import_result = + case project.import_type + when 'github' + Gitlab::GithubImport::Importer.new(project).execute + when 'gitlab' + Gitlab::GitlabImport::Importer.new(project).execute + when 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute + when 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute + when 'fogbugz' + Gitlab::FogbugzImport::Importer.new(project).execute + else + true + end - Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' + unless data_import_result + project.update(import_error: "The remote issue data could not be imported.") + project.import_fail + return + end + + if project.import_type == 'bitbucket' + Gitlab::BitbucketImport::KeyDeleter.new(project).execute + end project.import_finish - project.save - ProjectCacheWorker.perform_async(project.id) end end diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb new file mode 100644 index 00000000000..ca594e77e7c --- /dev/null +++ b/app/workers/stuck_ci_builds_worker.rb @@ -0,0 +1,18 @@ +class StuckCiBuildsWorker + include Sidekiq::Worker + + BUILD_STUCK_TIMEOUT = 1.day + + def perform + Rails.logger.info 'Cleaning stuck builds' + + builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago) + builds.find_each(batch_size: 50).each do |build| + Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" + build.drop + end + + # Update builds that failed to drop + builds.update_all(status: 'failed') + end +end |