diff options
166 files changed, 1973 insertions, 733 deletions
diff --git a/.scss-lint.yml b/.scss-lint.yml index 937d3407b60..835a4a88c44 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -7,21 +7,44 @@ exclude: - 'app/assets/stylesheets/pages/emojis.scss' linters: + # Reports when you use improper spacing around ! (the "bang") in !default, + # !global, !important, and !optional flags. BangFormat: enabled: false + # Whether or not to prefer `border: 0` over `border: none`. BorderZero: enabled: false + # Reports when you define a rule set using a selector with chained classes + # (a.k.a. adjoining classes). + ChainedClasses: + enabled: false + + # Prefer hexadecimal color codes over color keywords. + # (e.g. `color: green` is a color keyword) ColorKeyword: enabled: false + # Prefer color literals (keywords or hexadecimal codes) to be used only in + # variable declarations. They should be referred to via variables everywhere + # else. ColorVariable: enabled: false + # Which form of comments to prefer in CSS. Comment: enabled: false + + # Reports @debug statements (which you probably left behind accidentally). + DebugStatement: + enabled: false + # Rule sets should be ordered as follows: + # - @extend declarations + # - @include declarations without inner @content + # - properties, @include declarations with inner @content + # - nested rule sets. DeclarationOrder: enabled: false @@ -32,15 +55,25 @@ linters: DisableLinterReason: enabled: true + # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: false + # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: enabled: false + # Reports when you have an empty rule set. EmptyRule: enabled: false + # Reports when you have an @extend directive. + ExtendDirective: + enabled: false + + # Files should always have a final newline. This results in better diffs + # when adding lines to the file, since SCM systems such as git won't + # think that you touched the last line. FinalNewline: enabled: false @@ -53,12 +86,17 @@ linters: HexNotation: enabled: true + # Avoid using ID selectors. IdSelector: enabled: false + # The basenames of @imported SCSS partials should not begin with an + # underscore and should not include the filename extension. ImportPath: enabled: false + # Avoid using !important in properties. It is usually indicative of a + # misunderstanding of CSS specificity and can lead to brittle code. ImportantRule: enabled: false @@ -67,33 +105,51 @@ linters: enabled: true width: 2 + # Don't write leading zeros for numeric values with a decimal point. LeadingZero: enabled: false + # Reports when you define the same selector twice in a single sheet. MergeableSelector: enabled: false + # Functions, mixins, variables, and placeholders should be declared + # with all lowercase letters and hyphens instead of underscores. NameFormat: enabled: false + # Avoid nesting selectors too deeply. NestingDepth: enabled: false + # Always use placeholder selectors in @extend. PlaceholderInExtend: enabled: false + # Sort properties in a strict order. PropertySortOrder: enabled: false + # Reports when you use an unknown or disabled CSS property + # (ignoring vendor-prefixed properties). PropertySpelling: enabled: false + # Configure which units are allowed for property values. + PropertyUnits: + enabled: false + + # Pseudo-elements, like ::before, and ::first-letter, should be declared + # with two colons. Pseudo-classes, like :hover and :first-child, should + # be declared with one colon. PseudoElement: enabled: false + # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: enabled: false + # Don't write selectors with a depth of applicability greater than 3. SelectorDepth: enabled: false @@ -113,9 +169,12 @@ linters: enabled: true allow_single_line_rule_sets: true + # Split selectors onto separate lines after each comma, and have each + # individual selector occupy a single line. SingleLinePerSelector: enabled: false + # Commas in lists should be followed by a space. SpaceAfterComma: enabled: false @@ -128,28 +187,75 @@ linters: # colon. SpaceAfterPropertyName: enabled: true + + # Variables should be formatted with a single space separating the colon + # from the variable's value. + SpaceAfterVariableColon: + enabled: false + + # Variables should be formatted with no space between the name and the + # colon. + SpaceAfterVariableName: + enabled: false + # Operators should be formatted with a single space on both sides of an + # infix operator. SpaceAroundOperator: enabled: false + # Opening braces should be preceded by a single space. SpaceBeforeBrace: + enabled: true + + # Parentheses should not be padded with spaces. + SpaceBetweenParens: enabled: false + # Enforces that string literals should be written with a consistent form + # of quotes (single or double). StringQuotes: enabled: false + # Property values, @extend, @include, and @import directives, and variable + # declarations should always end with a semicolon. TrailingSemicolon: enabled: false + # Reports lines containing trailing whitespace. TrailingWhitespace: enabled: false + # Don't write trailing zeros for numeric values with a decimal point. + TrailingZero: + enabled: false + + # Don't use the `all` keyword to specify transition properties. + TransitionAll: + enabled: false + + # Numeric values should not contain unnecessary fractional portions. UnnecessaryMantissa: enabled: false + # Do not use parent selector references (&) when they would otherwise + # be unnecessary. UnnecessaryParentReference: enabled: false + + # URLs should be valid and not contain protocols or domain names. + UrlFormat: + enabled: false + + # URLs should always be enclosed within quotes. + UrlQuotes: + enabled: false + + # Properties, like color and font, are easier to read and maintain + # when defined using variables rather than literals. + VariableForProperty: + enabled: false + # Avoid vendor prefixes. Or rather: don't write them yourself. VendorPrefix: enabled: false diff --git a/CHANGELOG b/CHANGELOG index 313a584d1d0..e1b6b32cff3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,19 +1,42 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - All images in discussions and wikis now link to their source files !3464 (Connor Shea). + - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API - Fix avatar stretching by providing a cropping feature - - Fix raw/rendered diff producing different results on merge requests !3450 + - Add endpoints to archive or unarchive a project !3372 - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) + - Add default scope to projects to exclude projects pending deletion - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) + - Fix creation of merge requests for orphaned branches (Stan Hu) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) + - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) + - Improved UX of the navigation sidebar + - Build status notifications + +v 8.6.4 + - Don't attempt to fetch any tags from a forked repo (Stan Hu) + +v 8.6.3 + - Mentions on confidential issues doesn't create todos for non-members. !3374 + - Destroy related todos when an Issue/MR is deleted. !3376 + - Fix error 500 when target is nil on todo list. !3376 + - Fix copying uploads when moving issue to another project. !3382 + - Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432 + - Fix raw/rendered diff producing different results on merge requests. !3450 + - Fix commit comment alignment (Stan Hu). !3466 + - Fix Error 500 when searching for a comment in a project snippet. !3468 + - Allow temporary email as notification email. !3477 + - Fix issue with dropdowns not selecting values. !3478 + - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280 v 8.6.2 - Fix dropdown alignment. !3298 @@ -108,6 +131,7 @@ v 8.6.0 - Add main language of a project in the list of projects (Tiago Botelho) - Add #upcoming filter to Milestone filter (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages + - Remove fork link closes all merge requests opened on source project (Florent Baldino) - Move group activity to separate page - Create external users which are excluded of internal and private projects unless access was explicitly granted - Continue parameters are checked to ensure redirection goes to the same instance @@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' -gem 'request_store', '~> 1.2.0' +gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index bd41cc84198..229089f431d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -652,7 +652,7 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.7) redis (>= 2.2) - request_store (1.2.1) + request_store (1.3.0) rerun (0.11.0) listen (~> 3.0) responders (2.1.1) @@ -1011,7 +1011,7 @@ DEPENDENCIES redcarpet (~> 3.3.3) redis-namespace redis-rails (~> 4.0.0) - request_store (~> 1.2.0) + request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 1.10.1) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 47b080406d4..6a670d5e887 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,5 +1,5 @@ class @AwardsHandler - constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> + constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) -> $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() @@ -34,7 +34,7 @@ class @AwardsHandler $("#emoji_search").focus() else $('.js-add-award').addClass "is-loading" - $.get "/emojis", (response) => + $.get @get_emojis_url, (response) => $('.js-add-award').removeClass "is-loading" $(".js-award-holder").append response setTimeout => diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 0fea2a69cb7..4f032a82e58 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -343,7 +343,7 @@ class GitLabDropdown selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") - + if el.hasClass(ACTIVE_CLASS) el.removeClass(ACTIVE_CLASS) field.remove() @@ -373,6 +373,8 @@ class GitLabDropdown input = $(input) .attr('id', @options.inputId) @dropdown.before input + else + field.val value return selectedObject diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee new file mode 100644 index 00000000000..3f9ca39912c --- /dev/null +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -0,0 +1,30 @@ +((w) -> + notificationGranted = (message, opts, onclick) -> + notification = new Notification(message, opts) + + if onclick + notification.onclick = onclick + + notifyPermissions = -> + if 'Notification' of window + Notification.requestPermission() + + notifyMe = (message, body, icon, onclick) -> + opts = + body: body + icon: icon + # Let's check if the browser supports notifications + if !('Notification' of window) + # do nothing + else if Notification.permission == 'granted' + # If it's okay let's create a notification + notificationGranted message, opts, onclick + else if Notification.permission != 'denied' + Notification.requestPermission (permission) -> + # If the user accepts, let's create a notification + if permission == 'granted' + notificationGranted message, opts, onclick + + w.notify = notifyMe + w.notifyPermissions = notifyPermissions +) window diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 738ffc8343b..7102a0673e9 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -2,13 +2,18 @@ class @MergeRequestWidget # Initialize MergeRequestWidget behavior # # check_enable - Boolean, whether to check automerge status - # url_to_automerge_check - String, URL to use to check automerge status - # current_status - String, current automerge status - # ci_enable - Boolean, whether a CI service is enabled - # url_to_ci_check - String, URL to use to check CI status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status # + constructor: (@opts) -> - modal = $('#modal_merge_info').modal(show: false) + $('#modal_merge_info').modal(show: false) + @firstCICheck = true + @readyForCICheck = true + clearInterval @fetchBuildStatusInterval + + @pollCIStatus() + notifyPermissions() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -27,18 +32,57 @@ class @MergeRequestWidget dataType: 'json' getMergeStatus: -> - $.get @opts.url_to_automerge_check, (data) -> + $.get @opts.merge_check_url, (data) -> $('.mr-state-widget').replaceWith(data) - getCiStatus: -> - if @opts.ci_enable - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status + ciLabelForStatus: (status) -> + if status == 'success' + 'passed' + else + status + + pollCIStatus: -> + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + + @getCIStatus(true) + + @readyForCICheck = false + ), 5000 + + getCIStatus: (showNotification) -> + _this = @ + $('.ci-widget-fetching').show() + + $.getJSON @opts.ci_status_url, (data) => + @readyForCICheck = true + + if @firstCICheck + @firstCICheck = false + @opts.ci_status = data.status + + if data.status isnt @opts.ci_status + @showCIStatus data.status if data.coverage - this.showCiCoverage data.coverage - , 'json' + @showCICoverage data.coverage + + if showNotification + message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) + message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) + + notify( + "Build #{@ciLabelForStatus(data.status)}", + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit _this.opts.builds_path + ) + + @opts.ci_status = data.status - showCiState: (state) -> + showCIStatus: (state) -> $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -52,7 +96,7 @@ class @MergeRequestWidget $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') - showCiCoverage: (coverage) -> + showCICoverage: (coverage) -> text = 'Coverage ' + coverage + '%' $('.ci_widget:visible .ci-coverage').text(text) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index e2a7d5fbba2..f73127f49f0 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -84,12 +84,16 @@ class @MilestoneSelect # display:block overrides the hide-collapse rule $value.removeAttr('style') - clicked: (e) -> + clicked: (selected) -> if $dropdown.hasClass 'js-filter-bulk-update' return - if $dropdown.hasClass 'js-filter-submit' - $dropdown.parents('form').submit() + if $dropdown.hasClass('js-filter-submit') + if selected.name? + selectedMilestone = selected.name + else + selectedMilestone = '' + Issues.filterResults $dropdown.closest('form') else selected = $selectbox .find('input[type="hidden"]') @@ -117,4 +121,4 @@ class @MilestoneSelect else $value.html(milestoneLinkNoneTemplate) $sidebarCollapsedValue.find('span').text('No') - )
\ No newline at end of file + ) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 860d4f438d0..e1778511240 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) setTimeout ( -> diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 469f4f296ae..542a53f0377 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -13,10 +13,10 @@ // Toggle between two states. .js-toggler-container { - .turn-on { display: block; } + .turn-on { display: block; } .turn-off { display: none; } &.on { - .turn-on { display: none; } + .turn-on { display: none; } .turn-off { display: block; } } } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index e3192823a1a..0b3af592d4a 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,3 +1,9 @@ +.calender-block { + @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { + overflow-x: scroll; + } +} + .user-calendar-activities { .calendar_onclick_hr { padding: 0; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 9b676d759e0..db1a8b1bf78 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -121,7 +121,7 @@ p.time { text-shadow: none; } -.thin_area{ +.thin_area { height: 150px; } @@ -148,7 +148,7 @@ li.note { } } -.wiki_content code, .readme code{ +.wiki_content code, .readme code { background-color: inherit; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b05c5df1bd8..9209347f9bc 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -3,7 +3,7 @@ vertical-align: top; } -@media (min-width: $screen-sm-min) { +@media (min-width: $screen-sm-min) { .issues-filters, .issues_bulk_update { .dropdown-menu-toggle { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 91b6451e68a..54cb5461113 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -91,7 +91,7 @@ label { } .form-control::-webkit-input-placeholder { - color: #7f8fa4; + color: $gl-placeholder-color; } .input-group { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c83cf881596..fa9038ebaca 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -33,10 +33,15 @@ background: $color; } + .complex-sidebar .nav-primary { + border-right: 1px solid lighten($color, 3%); + } + .sidebar-wrapper { background: $color-darker; .sidebar-user { + border-top: 1px solid lighten($color, 3%); background: $color-darker; color: $color-light; @@ -62,7 +67,6 @@ .count { color: $color-light; - background: $color-dark; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index a450167b04f..724980b2208 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -36,7 +36,7 @@ header { padding: 0; .nav > li > a { - color: #7f8fa4; + color: $gl-icon-color; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -62,7 +62,7 @@ header { background-color: #eee; } &.active { - color: #7f8fa4; + color: $gl-icon-color; } } } @@ -81,14 +81,14 @@ header { font-size: 19px; line-height: $header-height; font-weight: normal; - color: #4c4e54; + color: $gl-text-color; overflow: hidden; text-overflow: ellipsis; vertical-align: top; white-space: nowrap; a { - color: #4c4e54; + color: $gl-text-color; &:hover { text-decoration: underline; } @@ -123,11 +123,11 @@ header { } @mixin collapsed-header { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; } .header-collapsed { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; @media (min-width: $screen-md-min) { @include collapsed-header; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 5ea4f9a49db..66180f38a4f 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -107,7 +107,7 @@ } .page-title { - .note_created_ago, .new-issue-link { + .note-created-ago, .new-issue-link { display: none; } } @@ -116,7 +116,7 @@ display: none; } - aside:not(.right-sidebar){ + aside:not(.right-sidebar) { display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 95bdd6d1ea3..94f5a12ff6a 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -56,6 +56,17 @@ } } + .nav-search { + display: inline-block; + width: 50%; + padding: 11px 0; + + /* Small devices (phones, tablets, 768px and lower) */ + @media (max-width: $screen-sm-min) { + width: 100%; + } + } + .nav-links { display: inline-block; width: 50%; @@ -100,6 +111,7 @@ > form { display: inline-block; + margin-top: -1px; } .icon-label { @@ -110,7 +122,7 @@ height: 34px; display: inline-block; position: relative; - top: 1px; + top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index e82d052f45a..b2fab387e17 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -51,7 +51,7 @@ padding: 10px 15px; } -.select2-drop{ +.select2-drop { color: #7f8fa4; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 18189e985c4..1d49249dd80 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -144,7 +144,7 @@ } a { - padding: 7px 15px; + padding: 7px 12px; font-size: $gl-font-size; line-height: 24px; color: $gray; @@ -169,10 +169,12 @@ } .count { - float: right; - background: #eee; - padding: 0 8px; - @include border-radius(6px); + &:before { + content: '('; + } + &:after { + content: ')'; + } } &.back-link i { @@ -191,6 +193,27 @@ } } +.expand-nav a { + color: $gl-icon-color; + width: 60px; + position: fixed; + top: 0; + left: 0; + font-size: 20px; + background: #fff; + height: 59px; + text-align: center; + line-height: 59px; + border-bottom: 1px solid #eee; + transition-duration: .3s; + outline: none; + z-index: 100; + + &:hover { + text-decoration: none; + } +} + .collapse-nav a { width: $sidebar_width; position: fixed; @@ -210,55 +233,12 @@ } .page-sidebar-collapsed { - padding-left: $sidebar_collapsed_width; - .sidebar-wrapper { - width: $sidebar_collapsed_width; - - .header-logo { - width: $sidebar_collapsed_width; - - a { - padding-left: ($sidebar_collapsed_width - 36) / 2; - - .gitlab-text-container { - display: none; - } - } - } - - .nav-sidebar { - width: $sidebar_collapsed_width; - - li { - width: auto; - - a { - span { - display: none; - } - } - } - } - - .collapse-nav a { - width: $sidebar_collapsed_width; - } - - .sidebar-user { - padding-left: ($sidebar_collapsed_width - 36) / 2; - width: $sidebar_collapsed_width; - - .username { - display: none; - } - } + display: none; } } .page-sidebar-expanded { - padding-left: $sidebar_collapsed_width; - @media (min-width: $screen-md-min) { padding-left: $sidebar_width; } @@ -309,3 +289,48 @@ padding-right: $sidebar_collapsed_width; } } + +.complex-sidebar { + display: inline-block; + + .nav-primary { + width: 61px; + float: left; + height: 100vh; + + .nav-sidebar { + width: 60px; + + li a { + width: 60px; + + span { + display: none; + } + } + } + } + + .nav-secondary { + $nav-secondary-width: 168px; + + float: left; + width: $nav-secondary-width; + + .nav-sidebar { + width: $nav-secondary-width; + + li { + width: $nav-secondary-width; + + a { + width: $nav-secondary-width; + + i { + display: none; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index f63ac033234..c72af5dad0a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -56,8 +56,8 @@ $component-active-bg: $brand-info; //## $input-color: $text-color; -$input-border: #e7e9ed; -$input-border-focus: #7f8fa4; +$input-border: $border-color; +$input-border-focus: $focus-border-color; $legend-color: $text-color; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b1886fbe67b..c3c7bc9fdbe 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -138,6 +138,12 @@ } } + a.no-attachment-icon { + &:before { + display: none; + } + } + /* Link to current header. */ h1, h2, h3, h4, h5, h6 { position: relative; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index bb49ae396c7..98fe794d362 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -11,6 +11,7 @@ $gutter_inner_width: 258px; * UI elements */ $border-color: #efeff1; +$focus-border-color: #3aabf0; $table-border-color: #eef0f2; $background-color: #faf9f9; @@ -26,6 +27,7 @@ $gl-text-orange: #d90; $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; +$gl-icon-color: $gl-placeholder-color; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; @@ -178,7 +180,7 @@ $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; -$dropdown-input-focus-border: rgb(58, 171, 240); +$dropdown-input-focus-border: $focus-border-color; $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); @@ -207,3 +209,11 @@ $location-badge-bg: $gray-normal; $location-icon-color: #e7e9ed; $location-active-color: $gl-text-color; $location-active-bg: $search-input-border-color; + +/* + * Notes + */ +$notes-light-color: #8e8e8e; +$notes-action-color: #c3c3c3; +$notes-role-color: #8e8e8e; +$notes-role-border-color: #e4e4e4; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 28994e60baa..37bf38fa65d 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -37,7 +37,7 @@ height: 300px; overflow-y: scroll; - input.emoji-search{ + input.emoji-search { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); background-repeat: no-repeat; background-position: right 5px center; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 2a7b5cfc7fd..67a9d7d2cf7 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -42,7 +42,7 @@ } } - .loading{ + .loading { font-size: 20px; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 971656feb42..082911bd118 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -1,15 +1,15 @@ -.commit-title{ +.commit-title { display: block; } -.commit-author, .commit-committer{ +.commit-author, .commit-committer { display: block; color: #999; font-weight: normal; font-style: italic; } -.commit-author strong, .commit-committer strong{ +.commit-author strong, .commit-committer strong { font-weight: bold; font-style: normal; } @@ -74,7 +74,7 @@ color: $gl-text-red; } } - .edit-file{ + .edit-file { a { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index b6011fe7679..8272615768d 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,4 +1,4 @@ -.commits-compare-switch{ +.commits-compare-switch { @include btn-default; @include btn-white; background: image-url("switch_icon.png") no-repeat center center; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 43be5e38ba8..0f0592a0ab8 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,5 +1,5 @@ .file-editor { - #editor{ + #editor { border: none; @include border-radius(0); height: 500px; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 84eefd01cfe..c66efe978cd 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -43,10 +43,6 @@ .md { color: #7f8fa4; font-size: $gl-font-size; - - iframe.twitter-share-button { - vertical-align: bottom; - } } pre { diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 6d2bd33b28b..6926448519e 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,9 +1,9 @@ .ci-body { - .incorrect-syntax{ + .incorrect-syntax { font-size: 19px; color: red; } - .correct-syntax{ + .correct-syntax { font-size: 19px; color: #47a447; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 777bcbca5c3..403171d4532 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -36,7 +36,7 @@ } } - .login-box{ + .login-box { background: #fafafa; border-radius: 10px; box-shadow: 0 0 2px #ccc; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4bd2016bdcf..92fcaaeeacf 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -22,7 +22,7 @@ ul.notes { margin-left: 55px; } - .note_created_ago, .note-updated-at { + .note-created-ago, .note-updated-at { white-space: nowrap; } @@ -39,53 +39,6 @@ ul.notes { } } - .discussion-header, - .note-header { - @extend .cgray; - - a:hover { - text-decoration: none; - } - - .avatar { - float: left; - margin-right: 10px; - } - - .discussion-last-update, - .note-last-update { - &:before { - content: "\00b7"; - } - - a { - color: $gl-gray; - - &:hover { - text-decoration: underline; - } - } - } - .author { - color: #4c4e54; - margin-right: 3px; - - &:hover { - color: $gl-link-color; - } - } - .author-username { - } - - .note-role { - float: right; - margin-top: 1px; - border: 1px solid #bbb; - background-color: transparent; - color: $gl-gray; - } - } - .discussion-body { padding-top: 15px; } @@ -198,40 +151,88 @@ ul.notes { border-width: 1px 0; padding-top: 0; vertical-align: top; - &.parallel{ + &.parallel { border-width: 1px; } } } } +.discussion-header, +.note-header { + a { + color: inherit; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } + } + + .author_link { + font-weight: 600; + } +} + +.note-headline-light, +.discussion-headline-light { + color: $notes-light-color; +} + /** * Actions for Discussions/Notes */ -.discussion, -.note { - .discussion-actions, - .note-actions { - float: right; - margin-left: 10px; +.discussion-actions, +.note-actions { + float: right; + margin-left: 10px; + color: $notes-action-color; +} - a { - margin-left: 5px; - color: $gl-gray; +.note-action-button, +.discussion-action-button { + display: inline-block; + margin-left: 10px; + line-height: 24px; - i.fa { - font-size: 16px; - line-height: 16px; - } + .fa { + position: relative; + top: 1px; + font-size: 17px; + } - &:hover { - @extend .cgray; - &.danger { @extend .cred; } - } - } + .fa-trash-o { + top: 0; + font-size: 16px; } } + +.discussion-toggle-button { + line-height: 20px; + font-size: 13px; + + .fa { + margin-right: 3px; + font-size: 10px; + line-height: 18px; + vertical-align: top; + } +} + +.note-role { + position: relative; + top: -2px; + display: inline-block; + padding-left: 4px; + padding-right: 4px; + color: $notes-role-color; + font-size: 12px; + line-height: 20px; + border: 1px solid $notes-role-border-color; + border-radius: $border-radius-base; +} + .diff-file .note .note-actions { right: 0; top: 0; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e909e15f74c..e83fa9e3d52 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -6,7 +6,7 @@ .navbar-nav { li { .badge.todos-pending-count { - background-color: #7f8fa4; + background-color: $gl-icon-color; margin-top: -5px; font-weight: normal; } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ed9f6031389..f010436bd36 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :require_two_factor_authentication, :two_factor_grace_period, :gravatar_enabled, - :twitter_sharing_enabled, :sign_in_text, :help_page_text, :home_page_url, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 62451ac73a9..49064f5d505 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -224,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) + ci_commit = @merge_request.ci_commit + if ci_commit + status = ci_commit.status + coverage = ci_commit.try(:coverage) + else + ci_service = @merge_request.source_project.ci_service + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + if ci_service.respond_to?(:commit_coverage) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + end end response = { + title: merge_request.title, + sha: merge_request.last_commit_short_sha, status: status, coverage: coverage } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 62f53664db3..8c3a74c8236 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -71,7 +71,7 @@ class ProjectsController < Projects::ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.unlink_fork + if ::Projects::UnlinkForkService.new(@project, current_user).execute flash[:notice] = 'The fork relationship has been removed.' end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 23693629a4c..60a0ff32c9c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -3,10 +3,6 @@ module ApplicationSettingsHelper current_application_settings.gravatar_enabled? end - def twitter_sharing_enabled? - current_application_settings.twitter_sharing_enabled? - end - def signup_enabled? current_application_settings.signup_enabled? end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index a36b13a7db5..592bad8ba24 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -216,7 +216,7 @@ module EventsHelper end def event_row_class(event) - if event.body? || event.created_project? + if event.body? "event-block" else "event-inline" diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c4879598c4e..052cd874733 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -12,7 +12,6 @@ # 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 @@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/app/models/commit.rb b/app/models/commit.rb index d0dbe009d0d..d09876a07d9 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -74,14 +74,14 @@ class Commit # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?<commit>\h{7,40}) }x end def self.link_reference_pattern - super("commit", /(?<commit>\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) end def to_reference(from_project = nil) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 289dbc57287..51673897d98 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -43,14 +43,14 @@ class CommitRange # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?<commit_range>#{STRICT_PATTERN}) }x end def self.link_reference_pattern - super("compare", /(?<commit_range>#{PATTERN})/) + @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/) end # Initialize a CommitRange diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index cf5b2c71675..afa2ca039ae 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links + has_many :todos, as: :target, dependent: :destroy validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -41,7 +42,7 @@ module Issuable scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } - scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) } + scope :non_archived, -> { join_project.where(projects: { archived: false }) } delegate :name, :email, diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 2ca79df0a29..b8585d4e577 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} + @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end def to_reference(_from_project = nil) diff --git a/app/models/issue.rb b/app/models/issue.rb index ed960cb39f4..e064b0f8b95 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?<issue>\d+) }x end def self.link_reference_pattern - super("issues", /(?<issue>\d+)/) + @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/label.rb b/app/models/label.rb index 500d5a35521..55c01cae762 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -56,7 +56,7 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1245cc16d6a..bf185cb5dd8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?<merge_request>\d+) }x end def self.link_reference_pattern - super("merge_requests", /(?<merge_request>\d+)/) + @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/) end # Returns all the merge requests from an ActiveRecord:Relation. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index bbd59eab9ae..986184dd301 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base end def self.link_reference_pattern - super("milestones", /(?<milestone>\d+)/) + @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/) end def self.upcoming @@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) "[#{escaped_title}](#{url})" diff --git a/app/models/note.rb b/app/models/note.rb index b0c33f2eec5..87ced65c650 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -311,7 +311,7 @@ class Note < ActiveRecord::Base for_merge_request? && for_diff_line? end - def for_project_snippet? + def for_snippet? noteable_type == "Snippet" end diff --git a/app/models/project.rb b/app/models/project.rb index 2285063ab50..3e1f04b4158 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -206,6 +206,8 @@ class Project < ActiveRecord::Base mount_uploader :avatar, AvatarUploader # Scopes + default_scope { where(pending_delete: false) } + scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } @@ -469,7 +471,7 @@ class Project < ActiveRecord::Base end def web_url - Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -590,7 +592,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -929,16 +931,6 @@ class Project < ActiveRecord::Base self.builds_enabled = true end - 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 diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 05436cd0f79..eaa5654b9c6 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,7 +20,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index aba37921c09..1ed42c4f3e7 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -21,7 +21,7 @@ class JiraService < IssueTrackerService include HTTParty - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers DEFAULT_API_VERSION = 2 diff --git a/app/models/repository.rb b/app/models/repository.rb index ff24b75dcd1..a8e826c9cbf 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -72,7 +72,7 @@ class Repository return @has_visible_content unless @has_visible_content.nil? @has_visible_content = cache.fetch(:has_visible_content?) do - raw_repository.branch_count > 0 + branch_count > 0 end end @@ -173,7 +173,7 @@ class Repository end def branch_names - cache.fetch(:branch_names) { raw_repository.branch_names } + cache.fetch(:branch_names) { branches.map(&:name) } end def tag_names @@ -191,7 +191,7 @@ class Repository end def branch_count - @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + @branch_count ||= cache.fetch(:branch_count) { branches.size } end def tag_count @@ -239,7 +239,7 @@ class Repository def expire_branches_cache cache.expire(:branch_names) - @branches = nil + @local_branches = nil end def expire_cache(branch_name = nil, revision = nil) @@ -364,6 +364,11 @@ class Repository expire_tag_count_cache end + def before_import + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been forked/imported. def after_import expire_emptiness_caches @@ -614,10 +619,14 @@ class Repository refs_contains_sha('tag', sha) end - def branches - @branches ||= raw_repository.branches + def local_branches + @local_branches ||= rugged.branches.each(:local).map do |branch| + Gitlab::Git::Branch.new(branch.name, branch.target) + end end + alias_method :branches, :local_branches + def tags @tags ||= raw_repository.tags end @@ -820,7 +829,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b9e835a4486..b96e3937281 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?<snippet>\d+) }x end def self.link_reference_pattern - super("snippets", /(?<snippet>\d+)/) + @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/user.rb b/app/models/user.rb index 128ddc2a694..2b0bee2099f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -408,6 +408,8 @@ class User < ActiveRecord::Base end def owns_notification_email + return if self.temp_oauth_email? + self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index a5efb21fab6..82e7090f1ea 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -43,7 +43,7 @@ module Issues def create_new_issue new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, project: @new_project, author: @old_issue.author, - description: unfold_references(@old_issue.description) } + description: rewrite_content(@old_issue.description) } new_params = @old_issue.serializable_hash.merge(new_params) CreateService.new(@new_project, @current_user, new_params).execute @@ -53,7 +53,7 @@ module Issues @old_issue.notes.find_each do |note| new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, - note: unfold_references(new_note.note), + note: rewrite_content(new_note.note), created_at: note.created_at, updated_at: note.updated_at } @@ -61,6 +61,18 @@ module Issues end end + def rewrite_content(content) + return unless content + + rewriters = [Gitlab::Gfm::ReferenceRewriter, + Gitlab::Gfm::UploadsRewriter] + + rewriters.inject(content) do |text, klass| + rewriter = klass.new(text, @old_project, @current_user) + rewriter.rewrite(@new_project) + end + end + def close_issue close_service = CloseService.new(@old_project, @current_user) close_service.execute(@old_issue, notifications: false, system_note: false) @@ -78,20 +90,12 @@ module Issues direction: :to) end - def unfold_references(content) - return unless content - - rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project, - @current_user) - rewriter.rewrite(@new_project) + def mark_as_moved + @old_issue.update(moved_to: @new_issue) end def notify_participants notification_service.issue_moved(@old_issue, @new_issue, @current_user) end - - def mark_as_moved - @old_issue.update(moved_to: @new_issue) - end end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 2015897dd19..ef15ef6a473 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -46,6 +46,8 @@ module Projects def import_data return unless has_importer? + project.repository.before_import + unless importer.execute raise Error, 'The remote data could not be imported.' end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb new file mode 100644 index 00000000000..315c3e16292 --- /dev/null +++ b/app/services/projects/unlink_fork_service.rb @@ -0,0 +1,19 @@ +module Projects + class UnlinkForkService < BaseService + def execute + return unless @project.forked? + + @project.forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << @project + end + + merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) + + merge_requests.each do |mr| + MergeRequests::CloseService.new(@project, @current_user).execute(mr) + end + + @project.forked_project_link.destroy + end + end +end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index ea2b26ccb52..f0615ec7420 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -95,17 +95,19 @@ class SystemHooksService end def project_member_data(model) + project = model.project || Project.unscoped.find(model.source_id) + { - 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_username: model.user.username, - user_name: model.user.name, - user_email: model.user.email, - user_id: model.user.id, - access_level: model.human_access, - project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase + project_name: project.name, + project_path: project.path, + project_path_with_namespace: project.path_with_namespace, + project_id: project.id, + user_username: model.user.username, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + access_level: model.human_access, + project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e022a046c48..658b086496f 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -224,7 +224,7 @@ class SystemNoteService # # "Started branch `issue-branch-button-201`" def self.new_issue_branch(issue, project, author, branch) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "Started branch [`#{branch}`](#{link})" diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index f2662922e90..42c5bca90fd 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -123,7 +123,7 @@ class TodoService def handle_note(note, author) # Skip system notes, and notes on project snippet - return if note.system? || note.for_project_snippet? + return if note.system? || note.for_snippet? project = note.project target = note.noteable @@ -170,14 +170,30 @@ class TodoService end def filter_mentioned_users(project, target, author) - mentioned_users = target.mentioned_users.select do |user| - user.can?(:read_project, project) - end - + mentioned_users = target.mentioned_users + mentioned_users = reject_users_without_access(mentioned_users, project, target) mentioned_users.delete(author) mentioned_users.uniq end + def reject_users_without_access(users, project, target) + if target.is_a?(Note) && target.for_issue? + target = target.noteable + end + + if target.is_a?(Issue) + select_users(users, :read_issue, target) + else + select_users(users, :read_project, project) + end + end + + def select_users(users, ability, subject) + users.select do |user| + user.can?(ability.to_sym, subject) + end + end + def pending_todos(user, criteria = {}) valid_keys = [:project_id, :target_id, :target_type, :commit_id] user.todos.pending.where(criteria.slice(*valid_keys)) diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 86d24469e05..1af9e9b0edb 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,14 +1,15 @@ # encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base include UploaderHelper + MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} storage :file attr_accessor :project, :secret - def initialize(project, secret = self.class.generate_secret) + def initialize(project, secret = nil) @project = project - @secret = secret + @secret = secret || self.class.generate_secret end def base_dir @@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def self.generate_secret - SecureRandom.hex - end - def secure_url File.join("/uploads", @secret, file.filename) end + def to_markdown + to_h[:markdown] + end + def to_h filename = image? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") @@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base markdown: markdown } end + + def self.generate_secret + SecureRandom.hex + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0350995d03d..de86dacbb12 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -77,13 +77,6 @@ = f.check_box :gravatar_enabled Gravatar enabled .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :twitter_sharing_enabled do - = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - Twitter enabled - %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter - .form-group = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 = f.number_field :default_projects_limit, class: 'form-control' diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 588ad767426..3571eefd570 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -15,7 +15,7 @@ %td - if project - = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace" + = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project) %td = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 3274ba5377b..6dd2fef395d 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,4 +1,4 @@ -.admin-dashboard +.admin-dashboard.prepend-top-default .row .col-md-4 %h4 Statistics diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 41c43899978..149593e7f46 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,5 +1,5 @@ - page_title "Deploy Keys" -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading Public deploy keys (#{@deploy_keys.count}) .controls diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml new file mode 100644 index 00000000000..9025aaac097 --- /dev/null +++ b/app/views/admin/groups/_group.html.haml @@ -0,0 +1,28 @@ +- css_class = '' unless local_assigns[:css_class] +- css_class += ' no-description' if group.description.blank? + +%li.group-row{ class: css_class } + .controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' + + .stats + %span + = icon('bookmark') + = number_with_delimiter(group.projects.count) + + %span + = icon('users') + = number_with_delimiter(group.users.count) + + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + = visibility_level_icon(group.visibility_level, fw: false) + + = image_tag group_icon(group), class: 'avatar s40 hidden-xs' + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 6bdc885a312..775072a7441 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,20 +1,19 @@ - page_title "Groups" %h3.page-title Groups (#{number_with_delimiter(@groups.total_count)}) - = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" %p.light Group allows you to keep projects organized. Use groups for uniting related projects. -%hr -= form_tag admin_groups_path, method: :get, class: 'form-inline' do - = hidden_field_tag :sort, @sort - .form-group - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" +.top-area + .nav-search + = form_tag admin_groups_path, method: :get, class: 'form-inline' do + = hidden_field_tag :sort, @sort + = text_field_tag :name, params[:name], class: "form-control" + = button_tag "Search", class: "btn submit btn-primary" - .pull-right + .nav-controls .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} %span.light @@ -33,34 +32,10 @@ = sort_title_recently_updated = link_to admin_groups_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" -%hr - -%ul.bordered-list +%ul.content-list - @groups.each do |group| - %li - .clearfix - .pull-right.prepend-top-10 - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm" - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove" - - %h4 - = link_to [:admin, group] do - %span{ class: visibility_level_color(group.visibility_level) } - = visibility_level_icon(group.visibility_level) - - %i.fa.fa-folder - = group.name - - → - %span.monospace - %strong #{group.path}/ - .clearfix - %p - = truncate group.description, length: 150 - .clearfix - %p.light - #{pluralize(group.members.size, 'member')}, #{pluralize(group.projects.count, 'project')} - + = render 'group', group: group = paginate @groups, theme: "gitlab" diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index 3c57e3dc174..05d6b9ed238 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -1,8 +1,10 @@ - page_title "Labels" -= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do - New label -%h3.page-title - Labels + +%div + = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do + New label + %h3.page-title + Labels %hr .labels @@ -13,4 +15,4 @@ - else .light-well .nothing-here-block There are no labels yet - + diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index c407972cd08..2dad64b8d0f 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,4 +1,4 @@ -%p.lead +%p.lead.prepend-top-default %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. diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 25d2b64e381..aa0aff86d4d 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -10,7 +10,10 @@ (removed) %span.todo-label = todo_action_name(todo) - = todo_target_link(todo) + - if todo.target + = todo_target_link(todo) + - else + (removed) · #{time_ago_with_tooltip(todo.created_at)} diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 8cf36c711b4..5a2a469ba62 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -7,21 +7,3 @@ = link_to_project event.project - else = event.project_name - -- if !event.project.private? && twitter_sharing_enabled? - .event-body{"data-user-is" => event.author_id} - .event-note - .md - %p - Congratulations! Why not share your accomplishment with the world? - - %a.twitter-share-button{ | - href: "https://twitter.com/share", | - "data-url" => event.project.web_url, | - "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | - "data-size" => "medium", | - "data-related" => "gitlab", | - "data-hashtags" => "gitlab", | - "data-count" => "none"} - Tweet - %script{src: "//platform.twitter.com/widgets.js"} diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml deleted file mode 100644 index 2ed51d87ca1..00000000000 --- a/app/views/layouts/_collapse_button.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if nav_menu_collapsed? - = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close" -- else - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588d..9be36273c7d 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,7 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } = render "layouts/broadcast" + .expand-nav + = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -8,15 +10,19 @@ .gitlab-text-container %h3 GitLab - - if defined?(sidebar) && sidebar - = render "layouts/nav/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' + - primary_sidebar = current_user ? 'dashboard' : 'explore' + + - if defined?(sidebar) && sidebar && sidebar != primary_sidebar + .complex-sidebar + .nav-primary + = render "layouts/nav/#{primary_sidebar}" + .nav-secondary + = render "layouts/nav/#{sidebar}" - else - = render 'layouts/nav/explore' + = render "layouts/nav/#{primary_sidebar}" .collapse-nav - = render partial: 'layouts/collapse_button' + = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar" - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile" do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b93729..22d1d4d8597 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -95,7 +95,7 @@ Spam Logs %span.count= number_with_delimiter(SpamLog.count(:all)) - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = nav_link(controller: :application_settings) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4a0069f18f8..d1a180e4299 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -15,12 +15,12 @@ = icon('dashboard fw') %span Activity - = nav_link(controller: :groups) do + = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups - = nav_link(controller: :milestones) do + = nav_link(path: 'dashboard#milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span @@ -48,7 +48,6 @@ %span Help - %li.separate-item = nav_link(controller: :profile) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 55940741dc0..0b7de9633ec 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -42,7 +34,7 @@ %span Members - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "separate-item" }) do + = nav_link do = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc5..cc119fd64e6 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75e..d0f82b5f57f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,19 +1,4 @@ %ul.nav.nav-sidebar - - if @project.group - = nav_link do - = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to group - - else - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = icon('bookmark fw') @@ -113,7 +98,7 @@ Snippets - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = nav_link(html_options: {class: "#{project_tab_class}"}) do = link_to edit_project_path(@project), title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index 5d342ef58e5..69fc81cb45c 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -8,8 +8,6 @@ Increase your account's security by enabling two-factor authentication (2FA). .col-lg-9 %p - Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} - %p Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. .row.append-bottom-10 diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index e7c85edff96..1e4c46fca2f 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -3,25 +3,32 @@ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - if can?(current_user, :create_issue, @project) + - can_create_issue = can?(current_user, :create_issue, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - can_create_snippet = can?(current_user, :create_snippet, @project) + + - if can_create_issue %li = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - 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(merge_project.namespace, merge_project) do = icon('tasks fw') New merge request - - if can?(current_user, :create_snippet, @project) + + - if can_create_snippet %li = 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) + - if can_create_issue || merge_project || can_create_snippet %li.divider + + - if can?(current_user, :push_code, @project) %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') @@ -35,13 +42,11 @@ = 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, diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index d22d1da8402..2cf9115e4dd 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -39,7 +39,7 @@ %td = build.name - .pull-right + .label-container - if build.tags.any? - build.tags.each do |tag| %span.label.label-primary diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 8367112a9cb..2731219ccad 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,7 +1,10 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_commit_id = diff_refs.first.id -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +// diff_refs will be nil for orphaned commits (e.g. first commit in repo) +- if diff_refs + - old_commit_id = diff_refs.first.id + - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) + - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index b05ab869215..2ec0d20a879 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,15 +1,17 @@ - if @ci_commit .mr-widget-heading - .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 details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + - %w[success skipped canceled failed running pending].each do |status| + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = 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 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 @@ -43,5 +45,5 @@ :javascript $(function() { - merge_request_widget.getCiStatus(); + merge_request_widget.getCIStatus(false); }); diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a489d4f9b24..2be06aebe6c 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -9,12 +9,17 @@ :javascript var merge_request_widget; - - merge_request_widget = new MergeRequestWidget({ - url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + var opts = { + merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + gitlab_icon: "#{asset_path 'gitlab_logo.png'}", + ci_status: "", + ci_message: "Build {{status}} for \"{{title}}\"", ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.gitlab_merge_status}", - }); + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" + }; + if(typeof merge_request_widget === 'undefined') { + merge_request_widget = new MergeRequestWidget(opts); + } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 2cf32e6093d..34fe1743f4b 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,28 +5,21 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header + = link_to_member(note.project, note.author, avatar: false) + .inline.note-headline-light + = "#{note.author.to_reference} commented" + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - if note_editable?(note) .note-actions - = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil-square-o') - - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o') - - - unless note.system - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role.label - = access - - = link_to_member(note.project, note.author, avatar: false) - - %span.author-username - = '@' + note.author.username - - %span.note-last-update - %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'} - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 4f15a99d061..cd8a5f0bd02 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -1,22 +1,20 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + on the diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - %strong on the diff .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 8d0a1af787c..46f2ba4bbcf 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -3,21 +3,20 @@ - commit_description = commit ? 'commit' : 'a deleted commit' .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion on #{commit_description}" + - if commit + = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on #{commit_description} - - if commit - = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index 218b0da3977..f8e000b424f 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -1,19 +1,18 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + on the outdated diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-down Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on the - %strong outdated diff - %div + .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 5fcba2b7e93..9544e3d3e17 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,24 +1,20 @@ - project = note.project +- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) commented on + = link_to project.name_with_namespace, project + · - if note.for_commit? - = link_to project do - = project.name_with_namespace - · - = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do - Commit #{truncate_sha(note.commit_id)} + = link_to "Commit #{truncate_sha(note.commit_id)}", note_url - else - = link_to project do - = project.name_with_namespace - · - %span #{note.noteable_type.titleize} ##{note.noteable.iid} + %span #{note.noteable_type.titleize} ##{noteable_identifier} · - = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do - = note.noteable.title + = link_to note.noteable.title, note_url .note-search-result .term diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index c99da92be9f..921eaefd79a 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -7,13 +7,13 @@ class: "check_all_issues left" .issues-other-filters .filter-item.inline - - if params[:author_id] + - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - - if params[:assignee_id] + - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 006a34a11e3..fd5e58c1f1f 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,4 +1,4 @@ -- if params[:label_name] +- if params[:label_name].present? = hidden_field_tag(:label_name, params[:label_name]) .dropdown %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index e52d2e39e6b..2fcf40ece99 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,4 +1,4 @@ -- if params[:milestone_title] +- if params[:milestone_title].present? = hidden_field_tag(:milestone_title, params[:milestone_title]) = dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index bca816f22cb..0c4b6a5618b 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -87,7 +87,7 @@ %div{ class: container_class } .tab-content #activity.tab-pane - .gray-content-block.white.second-block + .gray-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } .user-calendar{data: {href: user_calendar_path}} %h4.center.light diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 02647229776..8ffcdc4a327 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -15,12 +15,14 @@ - if current_user :javascript + var get_emojis_url = "#{emojis_path}"; var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; var noteable_type = "#{votable.class.name.underscore}"; var noteable_id = "#{votable.id}"; var aliases = #{AwardEmoji.aliases.to_json}; window.awards_handler = new AwardsHandler( + get_emojis_url, post_emoji_url, noteable_type, noteable_id, diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index d06e4480292..b51c6a266c9 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -5,7 +5,7 @@ class ProjectDestroyWorker def perform(project_id, user_id, params) begin - project = Project.find(project_id) + project = Project.unscoped.find(project_id) rescue ActiveRecord::RecordNotFound return end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 626268d7648..2b989015279 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -174,7 +174,6 @@ end Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? -Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb new file mode 100644 index 00000000000..1fff9759d1e --- /dev/null +++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb @@ -0,0 +1,17 @@ +class RemoveTodosForDeletedIssues < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type = 'Issue' + AND NOT EXISTS ( + SELECT * + FROM issues + WHERE issues.id = todos.target_id + AND issues.deleted_at IS NULL + ) + SQL + end + + def down + end +end diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb new file mode 100644 index 00000000000..275554e736e --- /dev/null +++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb @@ -0,0 +1,6 @@ +class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration + def change + add_index :projects, :pending_delete + end +end + diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb new file mode 100644 index 00000000000..54cea964ff2 --- /dev/null +++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb @@ -0,0 +1,17 @@ +class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type = 'MergeRequest' + AND NOT EXISTS ( + SELECT * + FROM merge_requests + WHERE merge_requests.id = todos.target_id + AND merge_requests.deleted_at IS NULL + ) + SQL + end + + def down + end +end diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb new file mode 100644 index 00000000000..0d736e323b6 --- /dev/null +++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb @@ -0,0 +1,5 @@ +class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration + def change + remove_column :application_settings, :twitter_sharing_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index dce2bfe62ca..e63e22ce864 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160320204112) do +ActiveRecord::Schema.define(version: 20160331133914) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -418,7 +418,7 @@ ActiveRecord::Schema.define(version: 20160320204112) do t.integer "iid" t.integer "updated_by_id" t.integer "moved_to_id" - t.boolean "confidential", default: false + t.boolean "confidential", default: false t.datetime "deleted_at" end @@ -745,6 +745,7 @@ ActiveRecord::Schema.define(version: 20160320204112) do add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} + add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree diff --git a/doc/api/projects.md b/doc/api/projects.md index 3703f4b327a..3a909a2bc87 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -491,6 +491,172 @@ Parameters: - `id` (required) - The ID of the project to be forked +### Archive a project + +Archives the project if the user is either admin or the project owner of this project. This action is +idempotent, thus archiving an already archived project will not change the project. + +Status code 201 with the project as body is given when successful, in case the user doesn't +have the proper access rights, code 403 is returned. Status 404 is returned if the project +doesn't exist, or is hidden to the user. + +``` +POST /projects/:id/archive +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, + "archived": true, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" +} +``` + +### Unarchive a project + +Unarchives the project if the user is either admin or the project owner of this project. This action is +idempotent, thus unarchiving an non-archived project will not change the project. + +Status code 201 with the project as body is given when successful, in case the user doesn't +have the proper access rights, code 403 is returned. Status 404 is returned if the project +doesn't exist, or is hidden to the user. + +``` +POST /projects/:id/archive +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" +``` + +Example response: + +```json +{ + "id": 3, + "description": null, + "default_branch": "master", + "public": false, + "visibility_level": 0, + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site", + "tag_list": [ + "example", + "disapora project" + ], + "owner": { + "id": 3, + "name": "Diaspora", + "created_at": "2013-09-30T13: 46: 02Z" + }, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "issues_enabled": true, + "open_issues_count": 1, + "merge_requests_enabled": true, + "builds_enabled": true, + "wiki_enabled": true, + "snippets_enabled": false, + "created_at": "2013-09-30T13: 46: 02Z", + "last_activity_at": "2013-09-30T13: 46: 02Z", + "creator_id": 3, + "namespace": { + "created_at": "2013-09-30T13: 46: 02Z", + "description": "", + "id": 3, + "name": "Diaspora", + "owner_id": 1, + "path": "diaspora", + "updated_at": "2013-09-30T13: 46: 02Z" + }, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + }, + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "shared_runners_enabled": true, + "forks_count": 0, + "star_count": 0, + "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b" +} +``` + ### Remove project Removes a project including all associated resources (issues, merge requests etc.) diff --git a/doc/api/settings.md b/doc/api/settings.md index 001de76c7af..1e745115dc8 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -26,7 +26,6 @@ Example response: "default_branch_protection" : 2, "restricted_visibility_levels" : [], "signin_enabled" : true, - "twitter_sharing_enabled" : true, "after_sign_out_path" : null, "max_attachment_size" : 10, "user_oauth_applications" : true, @@ -57,7 +56,6 @@ PUT /application/settings | `sign_in_text` | string | no | Text on login page | | `home_page_url` | string | no | Redirect to this URL when not logged in | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. | -| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter | | `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. | | `max_attachment_size` | integer | no | Limit attachment size in MB | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | @@ -85,7 +83,6 @@ Example response: "updated_at": "2015-06-30T13:22:42.210Z", "home_page_url": "", "default_branch_protection": 2, - "twitter_sharing_enabled": true, "restricted_visibility_levels": [], "max_attachment_size": 10, "session_expire_delay": 10080, diff --git a/doc/install/installation.md b/doc/install/installation.md index bffbc776500..e0a16df09c1 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -227,9 +227,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab -**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md new file mode 100644 index 00000000000..76eee147c72 --- /dev/null +++ b/doc/update/8.6-to-8.7.md @@ -0,0 +1,146 @@ +# From 8.6 to 8.7 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-7-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-7-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all +sudo -u git -H git checkout v2.7.0 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab-ssl origin/8-7-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab origin/8-7-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-7-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.6) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.5 to 8.6](8.5-to-8.6.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index c3b3577c449..db73309804c 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -6,6 +6,7 @@ Feature: Dashboard And project "Shop" has push event And project "Shop" has CI enabled And project "Shop" has CI build + And project "Shop" has labels: "bug", "feature", "enhancement" And I visit dashboard page Scenario: I should see projects list @@ -51,6 +52,13 @@ Feature: Dashboard Then The list should be sorted by "Oldest updated" @javascript + Scenario: Filtering Issues by label + Given project "Shop" has issue "Bugfix1" with label "feature" + When I visit dashboard issues page + And I filter the list by label "feature" + Then I should see "Bugfix1" in issues list + + @javascript Scenario: Visiting Project's issues after sorting Given I visit dashboard issues page And I sort the list by "Oldest updated" diff --git a/features/groups.feature b/features/groups.feature index 419a5d3963d..49e939807b5 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -7,10 +7,6 @@ Feature: Groups When I visit group "NonExistentGroup" page Then page status code should be 404 - Scenario: I should have back to group button - When I visit group "Owned" page - Then I should see back to dashboard button - @javascript Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page diff --git a/features/project/project.feature b/features/project/project.feature index f1f3ed26065..aa22401c88e 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,15 +18,6 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button - Scenario: I should have back to group button - And project "Shop" belongs to group - And I visit project "Shop" page - Then I should see back to group button - - Scenario: I should have back to group button - And I visit project "Shop" page - Then I should see back to dashboard button - Scenario: I should have readme on page And I visit project "Shop" page Then I should see project "Shop" README diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 5062e348844..b5980b35102 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -87,4 +87,23 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I should see 1 project at group list' do expect(find('span.last_activity/span')).to have_content('1') end + + step 'I filter the list by label "feature"' do + page.within ".labels-filter" do + find('.dropdown').click + click_link "feature" + end + end + + step 'I should see "Bugfix1" in issues list' do + page.within "ul.content-list" do + expect(page).to have_content "Bugfix1" + end + end + + step 'project "Shop" has issue "Bugfix1" with label "feature"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user) + issue.labels << project.labels.find_by(title: 'feature') + end end diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index a167d259837..b6ce5bc9cec 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,7 +5,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - click_link 'Milestones' + page.within '.nav-secondary' do + click_link("Milestones") + end end step 'I should see group milestones index page has no milestones' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index e5b7db4c5e3..483370f41c6 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -4,10 +4,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedGroup include SharedUser - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - step 'I should see group "Owned"' do expect(page).to have_content '@owned' end diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 19d81453d8c..4584fc4d754 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -82,7 +82,9 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues step 'I click the "Milestones" tab' do - click_link('Milestones') + page.within '.nav-secondary' do + click_link('Milestones') + end end step 'I click the "Labels" tab' do diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 527f7853da9..d9b16afa9b8 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I goto the Merge Requests page' do - page.within '.page-sidebar-expanded' do + page.within '.nav-secondary' do click_link "Merge Requests" end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 91fe19dd477..a4f02b590ea 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -326,7 +326,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on diff' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -334,7 +334,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion by user "John Doe" has started on diff' do page.within(".notes .discussion") do - page.should have_content "#{user_exists("John Doe").name} started a discussion" + page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -350,7 +350,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on commit diff' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" end @@ -358,7 +358,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion has started on commit' do page.within(".notes .discussion") do - page.should have_content "#{current_user.name} started a discussion on commit" + page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content "One comment to rule them all" end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index ef185861e00..8f1d4a223a9 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should not see "Snippets" button' do - expect(page).not_to have_link 'Snippets' + page.within '.nav-secondary' do + expect(page).not_to have_link 'Snippets' + end end step 'project "Shop" belongs to group' do @@ -123,14 +125,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps @project.save! end - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - - step 'I should see back to group button' do - expect(page).to have_content 'Go to group' - end - step 'I click notifications drop down button' do click_link 'notifications-button' end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 223b7277b51..9f6aed1c5b9 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -85,7 +85,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I have an existing Wiki page with images linked on page' do - wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![image](image.jpg)", :markdown, "first commit") + wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![alt text](image.jpg)", :markdown, "first commit") @wiki_page = wiki.find_page("pictures") end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 4fc2ece79ff..fa7d24ce611 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -41,7 +41,7 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - page.within '.nav-sidebar' do + page.within '.nav-secondary' do expect(page).to have_content('Go to project') end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f686c568bee..340fc5452ab 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -8,7 +8,7 @@ module API expose :id, :state, :avatar_url expose :web_url do |user, options| - Gitlab::Application.routes.url_helpers.user_url(user) + Gitlab::Routing.url_helpers.user_url(user) end end @@ -89,7 +89,7 @@ module API expose :avatar_url expose :web_url do |group, options| - Gitlab::Application.routes.url_helpers.group_url(group) + Gitlab::Routing.url_helpers.group_url(group) end end @@ -334,7 +334,6 @@ module API expose :updated_at expose :home_page_url expose :default_branch_protection - expose :twitter_sharing_enabled expose :restricted_visibility_levels expose :max_attachment_size expose :session_expire_delay diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6fcb5261e40..24b31005475 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -244,6 +244,34 @@ module API end end + # Archive project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # PUT /projects/:id/archive + post ':id/archive' do + authorize!(:archive_project, user_project) + + user_project.archive! + + present user_project, with: Entities::Project + end + + # Unarchive project + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # PUT /projects/:id/unarchive + post ':id/unarchive' do + authorize!(:archive_project, user_project) + + user_project.unarchive! + + present user_project, with: Entities::Project + end + # Remove project # # Parameters: diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 34c38913474..f21dbef216c 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -11,15 +11,19 @@ module Banzai end def self.object_name - object_class.name.underscore + @object_name ||= object_class.name.underscore end def self.object_sym - object_name.to_sym + @object_sym ||= object_name.to_sym end def self.data_reference - "data-#{object_name.dasherize}" + @data_reference ||= "data-#{object_name.dasherize}" + end + + def self.object_class_title + @object_title ||= object_class.name.titleize end # Public: Find references in text (like `!123` for merge requests) @@ -53,6 +57,10 @@ module Banzai self.class.object_sym end + def object_class_title + self.class.object_class_title + end + def references_in(*args, &block) self.class.references_in(*args, &block) end @@ -62,36 +70,81 @@ module Banzai # Example: project.merge_requests.find end + def find_object_cached(project, id) + if RequestStore.active? + cache = find_objects_cache[object_class][project.id] + + get_or_set_cache(cache, id) { find_object(project, id) } + else + find_object(project, id) + end + end + + def project_from_ref_cache(ref) + if RequestStore.active? + cache = project_refs_cache + + get_or_set_cache(cache, ref) { project_from_ref(ref) } + else + project_from_ref(ref) + end + end + def url_for_object(object, project) # Implement in child class # Example: project_merge_request_url end - def call - if object_class.reference_pattern - # `#123` - replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content, object_class.reference_pattern) - end + def url_for_object_cached(object, project) + if RequestStore.active? + cache = url_for_object_cache[object_class][project.id] - # `[Issue](#123)`, which is turned into - # `<a href="#123">Issue</a>` - replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| - object_link_filter(link, object_class.reference_pattern, link_text: text) - end + get_or_set_cache(cache, object) { url_for_object(object, project) } + else + url_for_object(object, project) end + end - if object_class.link_reference_pattern - # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into - # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>` - replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| - object_link_filter(text, object_class.link_reference_pattern) - end + def call + return doc if project.nil? + + ref_pattern = object_class.reference_pattern + link_pattern = object_class.link_reference_pattern - # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into - # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>` - replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| - object_link_filter(link, object_class.link_reference_pattern, link_text: text) + each_node do |node| + if text_node?(node) && ref_pattern + replace_text_when_pattern_matches(node, ref_pattern) do |content| + object_link_filter(content, ref_pattern) + end + + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if ref_pattern && link =~ /\A#{ref_pattern}/ + replace_link_node_with_href(node, link) do + object_link_filter(link, ref_pattern, link_text: text) + end + + next + end + + next unless link_pattern + + if link == text && text =~ /\A#{link_pattern}/ + replace_link_node_with_text(node, link) do + object_link_filter(text, link_pattern) + end + + next + end + + if link =~ /\A#{link_pattern}\z/ + replace_link_node_with_href(node, link) do + object_link_filter(link, link_pattern, link_text: text) + end + + next + end + end end end @@ -109,9 +162,9 @@ module Banzai # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_text: nil) references_in(text, pattern) do |match, id, project_ref, matches| - project = project_from_ref(project_ref) + project = project_from_ref_cache(project_ref) - if project && object = find_object(project, id) + if project && object = find_object_cached(project, id) title = object_link_title(object) klass = reference_class(object_sym) @@ -121,8 +174,11 @@ module Banzai object_sym => object.id ) - url = matches[:url] if matches.names.include?("url") - url ||= url_for_object(object, project) + if matches.names.include?("url") && matches[:url] + url = matches[:url] + else + url = url_for_object_cached(object, project) + end text = link_text || object_link_text(object, matches) @@ -146,7 +202,7 @@ module Banzai end def object_link_title(object) - "#{object_class.name.titleize}: #{object.title}" + "#{object_class_title}: #{object.title}" end def object_link_text(object, matches) @@ -157,6 +213,32 @@ module Banzai text end + + private + + def project_refs_cache + RequestStore[:banzai_project_refs] ||= {} + end + + def find_objects_cache + RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end + + def url_for_object_cache + RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end + + def get_or_set_cache(cache, key) + if cache.key?(key) + cache[key] + else + cache[key] = yield + end + end end end end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index 470727ee312..b469ea0f626 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -43,7 +43,7 @@ module Banzai end def url_for_object(range, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 713a56ba949..bd88207326c 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -37,7 +37,7 @@ module Banzai end def url_for_object(commit, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index edc26386903..37344b90576 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -35,15 +35,29 @@ module Banzai def call # Early return if the project isn't using an external tracker - return doc if project.nil? || project.default_issues_tracker? + return doc if project.nil? || default_issues_tracker? - replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| - issue_link_filter(content) - end + ref_pattern = ExternalIssue.reference_pattern + ref_start_pattern = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + issue_link_filter(content) + end - replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text| - issue_link_filter(link, link_text: text) + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_start_pattern + replace_link_node_with_href(node, link) do + issue_link_filter(link, link_text: text) + end + end + end + end end + + doc end # Replace `JIRA-123` issue references in text with links to the referenced @@ -76,6 +90,21 @@ module Banzai def url_for_issue(*args) IssuesHelper.url_for_issue(*args) end + + def default_issues_tracker? + if RequestStore.active? + default_issues_tracker_cache[project.id] ||= + project.default_issues_tracker? + else + project.default_issues_tracker? + end + end + + private + + def default_issues_tracker_cache + RequestStore[:banzai_default_issues_tracker_cache] ||= {} + end end end end diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb new file mode 100644 index 00000000000..ccd106860bd --- /dev/null +++ b/lib/banzai/filter/image_link_filter.rb @@ -0,0 +1,27 @@ +module Banzai + module Filter + # HTML filter that wraps links around inline images. + class ImageLinkFilter < HTML::Pipeline::Filter + + # Find every image that isn't already wrapped in an `a` tag, create + # a new node (a link to the image source), copy the image as a child + # of the anchor, and then replace the img with the link-wrapped version. + def call + doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| + + link = doc.document.create_element( + 'a', + class: 'no-attachment-icon', + href: img['src'], + target: '_blank' + ) + + link.children = img.clone + img.replace(link) + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8147e5ed3c7..a2987850d03 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -31,7 +31,7 @@ module Banzai end def url_for_object(label, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index 57c71708992..cad38a51851 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(mr, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 8f710a92bdc..4cb82178024 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -11,7 +11,7 @@ module Banzai end def url_for_object(issue, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index a3326ae042c..31386cf851c 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -52,18 +52,13 @@ module Banzai html.html_safe? ? html : ERB::Util.html_escape_once(html) end - def ignore_parents - @ignore_parents ||= begin - # Don't look for references in text nodes that are children of these - # elements. + def ignore_ancestor_query + @ignore_ancestor_query ||= begin parents = %w(pre code a style) parents << 'blockquote' if context[:ignore_blockquotes] - parents.to_set - end - end - def ignored_ancestry?(node) - has_ancestor?(node, ignore_parents) + parents.map { |n| "ancestor::#{n}" }.join(' or ') + end end def project @@ -74,119 +69,66 @@ module Banzai "gfm gfm-#{type}" end - # Iterate through the document's text nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's content matches `pattern` AND - # * The node is not an ancestor of an ignored node type - # - # pattern - Regex pattern against which to match the node's content - # - # Yields the current node's String contents. The result of the block will - # replace the node's existing content and update the current document. + # Ensure that a :project key exists in context # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_text_nodes_matching(pattern) - return doc if project.nil? - - search_text_nodes(doc).each do |node| - next if ignored_ancestry?(node) - next unless node.text =~ pattern - - content = node.to_html - - html = yield content - - next if html == content - - node.replace(html) - end - - doc + # Note that while the key might exist, its value could be nil! + def validate + needs :project end - # Iterate through the document's link nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's content matches `pattern` - # - # pattern - Regex pattern against which to match the node's content - # - # Yields the current node's String contents. The result of the block will - # replace the node and update the current document. + # Iterates over all <a> and text() nodes in a document. # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_link_nodes_with_text(pattern) - return doc if project.nil? + # Nodes are skipped whenever their ancestor is one of the nodes returned + # by `ignore_ancestor_query`. Link tags are not processed if they have a + # "gfm" class or the "href" attribute is empty. + def each_node + query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] + | descendant-or-self::a[ + not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "") + ]} - doc.xpath('descendant-or-self::a').each do |node| - klass = node.attr('class') - next if klass && klass.include?('gfm') - - link = node.attr('href') - text = node.text - - next unless link && text - - link = CGI.unescape(link) - next unless link.force_encoding('UTF-8').valid_encoding? - # Ignore ending punctionation like periods or commas - next unless link == text && text =~ /\A#{pattern}/ - - html = yield text + doc.xpath(query).each do |node| + yield node + end + end - next if html == text + # Yields the link's URL and text whenever the node is a valid <a> tag. + def yield_valid_link(node) + link = CGI.unescape(node.attr('href').to_s) + text = node.text - node.replace(html) - end + return unless link.force_encoding('UTF-8').valid_encoding? - doc + yield link, text end - # Iterate through the document's link nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's HREF matches `pattern` - # - # pattern - Regex pattern against which to match the node's HREF - # - # Yields the current node's String HREF and String content. - # The result of the block will replace the node and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_link_nodes_with_href(pattern) - return doc if project.nil? + def replace_text_when_pattern_matches(node, pattern) + return unless node.text =~ pattern - doc.xpath('descendant-or-self::a').each do |node| - klass = node.attr('class') - next if klass && klass.include?('gfm') + content = node.to_html + html = yield content - link = node.attr('href') - text = node.text + node.replace(html) unless content == html + end - next unless link && text - link = CGI.unescape(link) - next unless link.force_encoding('UTF-8').valid_encoding? - next unless link && link =~ /\A#{pattern}\z/ + def replace_link_node_with_text(node, link) + html = yield - html = yield link, text + node.replace(html) unless html == node.text + end - next if html == link + def replace_link_node_with_href(node, link) + html = yield - node.replace(html) - end + node.replace(html) unless html == link + end - doc + def text_node?(node) + node.is_a?(Nokogiri::XML::Text) end - # Ensure that a :project key exists in context - # - # Note that while the key might exist, its value could be nil! - def validate - needs :project + def element_node?(node) + node.is_a?(Nokogiri::XML::Element) end end end diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index c870a42f741..d507eb5ebe1 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(snippet, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_snippet_url(project.namespace, project, snippet, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 24f16f8b547..eea3af842b6 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -59,13 +59,28 @@ module Banzai end def call - replace_text_nodes_matching(User.reference_pattern) do |content| - user_link_filter(content) + return doc if project.nil? + + ref_pattern = User.reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + user_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + user_link_filter(link, link_text: text) + end + end + end + end end - replace_link_nodes_with_href(User.reference_pattern) do |link, text| - user_link_filter(link, link_text: text) - end + doc end # Replace `@user` user references in text with links to the referenced @@ -90,7 +105,7 @@ module Banzai private def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def link_class diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8cd4b50e65a..ed3cfd6b023 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -7,6 +7,7 @@ module Banzai Filter::SanitizationFilter, Filter::UploadLinkFilter, + Filter::ImageLinkFilter, Filter::EmojiFilter, Filter::TableOfContentsFilter, Filter::AutolinkFilter, diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 761b63e98f6..1acc22fe5bf 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -21,7 +21,6 @@ module Gitlab default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 41f0edcaf7e..8f9be6cd9a3 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,7 +5,7 @@ module Gitlab attr_accessor :recipient attr_reader :author_id, :ref, :action - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb index 431d50882fd..2152182b37f 100644 --- a/lib/gitlab/fogbugz_import/client.rb +++ b/lib/gitlab/fogbugz_import/client.rb @@ -26,7 +26,7 @@ module Gitlab def user_map users = {} res = @api.command(:listPeople) - res['people']['person'].each do |user| + [res['people']['person']].flatten.each do |user| users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] } end users diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index a1c6ee7bd69..78d7a4f27cf 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -34,16 +34,21 @@ module Gitlab @source_project = source_project @current_user = current_user @original_html = markdown(text) + @pattern = Gitlab::ReferenceExtractor.references_pattern end def rewrite(target_project) - pattern = Gitlab::ReferenceExtractor.references_pattern + return @text unless needs_rewrite? - @text.gsub(pattern) do |reference| + @text.gsub(@pattern) do |reference| unfold_reference(reference, Regexp.last_match, target_project) end end + def needs_rewrite? + @text =~ @pattern + end + private def unfold_reference(reference, match, target_project) diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb new file mode 100644 index 00000000000..abc8c8c55e6 --- /dev/null +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -0,0 +1,51 @@ +module Gitlab + module Gfm + ## + # Class that rewrites markdown links for uploads + # + # Using a pattern defined in `FileUploader` it copies files to a new + # project and rewrites all links to uploads in in a given text. + # + # + class UploadsRewriter + def initialize(text, source_project, _current_user) + @text = text + @source_project = source_project + @pattern = FileUploader::MARKDOWN_PATTERN + end + + def rewrite(target_project) + return @text unless needs_rewrite? + + @text.gsub(@pattern) do |markdown| + file = find_file(@source_project, $~[:secret], $~[:file]) + return markdown unless file.try(:exists?) + + new_uploader = FileUploader.new(target_project) + new_uploader.store!(file) + new_uploader.to_markdown + end + end + + def needs_rewrite? + files.any? + end + + def files + referenced_files = @text.scan(@pattern).map do + find_file(@source_project, $~[:secret], $~[:file]) + end + + referenced_files.compact.select(&:exists?) + end + + private + + def find_file(project, secret, file) + uploader = FileUploader.new(project, secret) + uploader.retrieve_from_store!(file) + uploader.file + end + end + end +end diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb index 71cf6a0d886..18523e0aefe 100644 --- a/lib/gitlab/note_data_builder.rb +++ b/lib/gitlab/note_data_builder.rb @@ -41,7 +41,7 @@ module Gitlab data[:issue] = note.noteable.hook_attrs elsif note.for_merge_request? data[:merge_request] = note.noteable.hook_attrs - elsif note.for_project_snippet? + elsif note.for_snippet? data[:snippet] = note.noteable.hook_attrs end diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb new file mode 100644 index 00000000000..5132177de51 --- /dev/null +++ b/lib/gitlab/routing.rb @@ -0,0 +1,13 @@ +module Gitlab + module Routing + # Returns the URL helpers Module. + # + # This method caches the output as Rails' "url_helpers" method creates an + # anonymous module every time it's called. + # + # Returns a Module. + def self.url_helpers + @url_helpers ||= Gitlab::Application.routes.url_helpers + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 6f0d02cafd1..f301d42939d 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,7 +1,8 @@ module Gitlab class UrlBuilder - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers include GitlabRoutingHelper + include ActionView::RecordIdentifier def initialize(type) @type = type @@ -37,19 +38,16 @@ module Gitlab namespace_project_commit_url(namespace_id: note.project.namespace, id: note.commit_id, project_id: note.project, - anchor: "note_#{note.id}") + anchor: dom_id(note)) elsif note.for_issue? issue = Issue.find(note.noteable_id) - issue_url(issue, - anchor: "note_#{note.id}") + issue_url(issue, anchor: dom_id(note)) elsif note.for_merge_request? merge_request = MergeRequest.find(note.noteable_id) - merge_request_url(merge_request, - anchor: "note_#{note.id}") - elsif note.for_project_snippet? + merge_request_url(merge_request, anchor: dom_id(note)) + elsif note.for_snippet? snippet = Snippet.find(note.noteable_id) - project_snippet_url(snippet, - anchor: "note_#{note.id}") + project_snippet_url(snippet, anchor: dom_id(note)) end end end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 5b1f65d7aff..9ef8ba1b097 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -1,15 +1,14 @@ require 'spec_helper' describe Admin::UsersController do - let(:admin) { create(:admin) } + let(:user) { create(:user) } before do - sign_in(admin) + sign_in(create(:admin)) end describe 'DELETE #user with projects' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:empty_project, namespace: user.namespace) } before do project.team << [user, :developer] @@ -23,8 +22,6 @@ describe Admin::UsersController do end describe 'PUT block/:id' do - let(:user) { create(:user) } - it 'blocks user' do put :block, id: user.username user.reload @@ -50,8 +47,6 @@ describe Admin::UsersController do end context 'manually blocked users' do - let(:user) { create(:user) } - before do user.block end @@ -66,8 +61,6 @@ describe Admin::UsersController do end describe 'PUT unlock/:id' do - let(:user) { create(:user) } - before do request.env["HTTP_REFERER"] = "/" user.lock_access! @@ -95,8 +88,6 @@ describe Admin::UsersController do end describe 'PATCH disable_two_factor' do - let(:user) { create(:user) } - it 'disables 2FA for the user' do expect(user).to receive(:disable_two_factor!) allow(subject).to receive(:user).and_return(user) diff --git a/spec/factories/file_uploader.rb b/spec/factories/file_uploader.rb new file mode 100644 index 00000000000..1b36e21f2b0 --- /dev/null +++ b/spec/factories/file_uploader.rb @@ -0,0 +1,20 @@ +FactoryGirl.define do + factory :file_uploader do + project + secret nil + + transient do + fixture { 'rails_sample.jpg' } + path { File.join(Rails.root, 'spec/fixtures', fixture) } + file { Rack::Test::UploadedFile.new(path) } + end + + after(:build) do |uploader, evaluator| + uploader.store!(evaluator.file) + end + + initialize_with do + new(project, secret) + end + end +end diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 252bf2747e1..19a54946fe0 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -13,5 +13,10 @@ FactoryGirl.define do factory :forked_project_link do association :forked_to_project, factory: :project association :forked_from_project, factory: :project + + after(:create) do |link| + link.forked_from_project.reload + link.forked_to_project.reload + end end end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 457859dedaf..62de081661d 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,9 +1,17 @@ require 'spec_helper' -FactoryGirl.factories.map(&:name).each do |factory_name| - describe "#{factory_name} factory" do - it 'should be valid' do - expect(build(factory_name)).to be_valid +describe 'factories' do + FactoryGirl.factories.each do |factory| + describe "#{factory.name} factory" do + let(:entity) { build(factory.name) } + + it 'does not raise error when created 'do + expect { entity }.to_not raise_error + end + + it 'should be valid', if: factory.build_class < ActiveRecord::Base do + expect(entity).to be_valid + end end end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index dc41be8246f..de6aed74fb4 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'should have XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: <img[^>]*\/>/ + expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/ end end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb new file mode 100644 index 00000000000..fd02d584848 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +feature 'Create New Merge Request', feature: true, js: false do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + project.team << [user, :master] + + login_as user + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'generates a diff for an orphaned branch' do + click_link 'New Merge Request' + select "orphaned-branch", from: "merge_request_source_branch" + select "master", from: "merge_request_target_branch" + click_button "Compare branches" + + expect(page).to have_content "README.md" + expect(page).to have_content "wm.png" + + fill_in "merge_request_title", with: "Orphaned MR test" + click_button "Submit merge request" + + expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' + end +end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 84c036e59c0..3e6289a46b1 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -1,19 +1,46 @@ require 'spec_helper' describe "Search", feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + before do - login_as :user - @project = create(:project, namespace: @user.namespace) - @project.team << [@user, :reporter] + login_with(user) + project.team << [user, :reporter] visit search_path + end - page.within '.search-holder' do - fill_in "search", with: @project.name[0..3] - click_button "Search" + describe 'searching for Projects' do + it 'finds a project' do + page.within '.search-holder' do + fill_in "search", with: project.name[0..3] + click_button "Search" + end + + expect(page).to have_content project.name end end - it "should show project in search results" do - expect(page).to have_content @project.name + context 'search for comments' do + it 'finds a snippet' do + snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') + note = create(:note, + noteable: snippet, + author: user, + note: 'Supercalifragilisticexpialidocious', + project: project) + # Must visit project dashboard since global search won't search + # everything (e.g. comments, snippets, etc.) + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: note.note + click_button 'Go' + end + + click_link 'Comments' + + expect(page).to have_link(snippet.title) + end end end diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb new file mode 100644 index 00000000000..dd5594750c8 --- /dev/null +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Banzai::Filter::ImageLinkFilter, lib: true do + include FilterSpecHelper + + def image(path) + %(<img src="#{path}" />) + end + + it 'wraps the image with a link to the image src' do + doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + end + + it 'does not wrap a duplicate link' do + exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>) + expect(filter(act).to_html).to eq exp + end + + it 'works with external images' do + doc = filter(image('https://i.imgur.com/DfssX9C.jpg')) + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + end +end diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index f38fadda9ba..566035c60d0 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ExtractsPath, lib: true do include ExtractsPath include RepoHelpers - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers let(:project) { double('project') } diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 844fd79c991..a1f51429a79 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -236,6 +236,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb new file mode 100644 index 00000000000..2dc71be0254 --- /dev/null +++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::FogbugzImport::Client, lib: true do + + let(:client) { described_class.new(uri: '', token: '') } + let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } } + let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } } + + it 'retrieves user_map with one user' do + stub_api(one_user) + + expect(client.user_map.count).to eq(1) + end + + it 'retrieves user_map with two users' do + stub_api(two_users) + + expect(client.user_map.count).to eq(2) + end + + def stub_api(users) + allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listPeople).and_return(users) + end +end diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb new file mode 100644 index 00000000000..eda956e6f0a --- /dev/null +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Gitlab::Gfm::UploadsRewriter do + let(:user) { create(:user) } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } + let(:rewriter) { described_class.new(text, old_project, user) } + + context 'text contains links to uploads' do + let(:image_uploader) do + build(:file_uploader, project: old_project) + end + + let(:zip_uploader) do + build(:file_uploader, project: old_project, + fixture: 'ci_build_artifacts.zip') + end + + let(:text) do + "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}" + end + + describe '#rewrite' do + let!(:new_text) { rewriter.rewrite(new_project) } + + let(:old_files) { [image_uploader, zip_uploader].map(&:file) } + let(:new_files) do + described_class.new(new_text, new_project, user).files + end + + let(:old_paths) { old_files.map(&:path) } + let(:new_paths) { new_files.map(&:path) } + + it 'rewrites content' do + expect(new_text).to_not eq text + expect(new_text.length).to eq text.length + end + + it 'copies files' do + expect(new_files).to all(exist) + expect(old_paths).to_not match_array new_paths + expect(old_paths).to all(include(old_project.path_with_namespace)) + expect(new_paths).to all(include(new_project.path_with_namespace)) + end + + it 'does not remove old files' do + expect(old_files).to all(exist) + end + + it 'generates a new secret for each file' do + expect(new_paths).to_not include image_uploader.secret + expect(new_paths).to_not include zip_uploader.secret + end + end + + describe '#needs_rewrite?' do + subject { rewriter.needs_rewrite? } + it { is_expected.to eq true } + end + + describe '#files' do + subject { rewriter.files } + it { is_expected.to be_an(Array) } + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b1764d7ac09..520cf1b75de 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -12,7 +12,6 @@ # 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 diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index be29b6d66ff..b16ccc6e305 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -9,6 +9,7 @@ describe Issue, "Issuable" do it { is_expected.to belong_to(:author) } it { is_expected.to belong_to(:assignee) } it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe "Validation" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 55f1c665b86..f29c389e094 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -104,6 +104,15 @@ describe Project, models: true do end end + describe 'default_scope' do + it 'excludes projects pending deletion from the results' do + project = create(:empty_project) + create(:empty_project, pending_delete: true) + + expect(Project.all).to eq [project] + end + end + describe 'project token' do it 'should set an random token if none provided' do project = FactoryGirl.create :empty_project, runners_token: '' diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9242a6f1739..f517f325c03 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -303,7 +303,7 @@ describe Repository, models: true do describe 'when there are no branches' do before do - allow(repository.raw_repository).to receive(:branch_count).and_return(0) + allow(repository).to receive(:branch_count).and_return(0) end it { is_expected.to eq(false) } @@ -311,13 +311,13 @@ describe Repository, models: true do describe 'when there are branches' do it 'returns true' do - expect(repository.raw_repository).to receive(:branch_count).and_return(3) + expect(repository).to receive(:branch_count).and_return(3) expect(subject).to eq(true) end it 'caches the output' do - expect(repository.raw_repository).to receive(:branch_count). + expect(repository).to receive(:branch_count). once. and_return(3) @@ -436,7 +436,7 @@ describe Repository, models: true do it 'expires the visible content cache' do repository.has_visible_content? - expect(repository.raw_repository).to receive(:branch_count). + expect(repository).to receive(:branch_count). once. and_return(0) @@ -612,6 +612,20 @@ describe Repository, models: true do end end + describe '#before_import' do + it 'flushes the emptiness cachess' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.before_import + end + + it 'flushes the exists cache' do + expect(repository).to receive(:expire_exists_cache) + + repository.before_import + end + end + describe '#after_import' do it 'flushes the emptiness cachess' do expect(repository).to receive(:expire_emptiness_caches) @@ -865,4 +879,21 @@ describe Repository, models: true do repository.build_cache end end + + describe '#local_branches' do + it 'returns the local branches' do + masterrev = repository.find_branch('master').target + create_remote_branch('joe', 'remote_branch', masterrev) + repository.add_branch(user, 'local_branch', masterrev) + + expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) + expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) + end + end + + def create_remote_branch(remote_name, branch_name, target) + rugged = repository.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) + end + end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0ab7fd88ce6..8b2fb77e28e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -173,6 +173,13 @@ describe User, models: true do expect(user).to be_invalid end end + + context 'owns_notification_email' do + it 'accepts temp_oauth_email emails' do + user = build(:user, email: "temp-email-for-oauth@example.com") + expect(user).to be_valid + end + end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a5d4985dc78..be2034e0f39 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -948,6 +948,78 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/archive' do + context 'on an unarchived project' do + it 'archives the project' do + post api("/projects/#{project.id}/archive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'remains archived' do + post api("/projects/#{project.id}/archive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post api("/projects/#{project.id}/archive", user3) + + expect(response.status).to eq(403) + end + end + end + + describe 'POST /projects/:id/unarchive' do + context 'on an unarchived project' do + it 'remains unarchived' do + post api("/projects/#{project.id}/unarchive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'unarchives the project' do + post api("/projects/#{project.id}/unarchive", user) + + expect(response.status).to eq(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post api("/projects/#{project.id}/unarchive", user3) + + expect(response.status).to eq(403) + end + end + end + describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'should remove project' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 9b0c73aaf37..2a5e4ac3ec4 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -160,6 +160,20 @@ describe Issues::MoveService, services: true do .to eq "Note with reference to merge request #{old_project.to_reference}!1" end end + + context 'issue description with uploads' do + let(:uploader) { build(:file_uploader, project: old_project) } + let(:description) { "Text and #{uploader.to_markdown}" } + + include_context 'issue move executed' + + it 'rewrites uploads in description' do + expect(new_issue.description).to_not eq description + expect(new_issue.description) + .to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/) + expect(new_issue.description).to_not include uploader.secret + end + end end describe 'rewritting references' do diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 04f474c736c..32bf3acf483 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -72,6 +72,23 @@ describe Projects::ImportService, services: true do expect(result[:status]).to eq :success end + it 'flushes various caches' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository). + with(project.path_with_namespace, project.import_url). + and_return(true) + + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute). + and_return(true) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). + and_call_original + + expect_any_instance_of(Repository).to receive(:expire_exists_cache). + and_call_original + + subject.execute + end + it 'fails if importer fails' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb new file mode 100644 index 00000000000..23f5555d3e0 --- /dev/null +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::UnlinkForkService, services: true do + subject { Projects::UnlinkForkService.new(fork_project, user) } + + let(:fork_link) { create(:forked_project_link) } + let(:fork_project) { fork_link.forked_to_project } + let(:user) { create(:user) } + + context 'with opened merge request on the source project' do + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } + + before do + allow(MergeRequests::CloseService).to receive(:new). + with(fork_project, user). + and_return(mr_close_service) + end + + it 'close all pending merge requests' do + expect(mr_close_service).to receive(:execute).with(merge_request) + + subject.execute + end + end + + it 'remove fork relation' do + expect(fork_project.forked_project_link).to receive(:destroy) + + subject.execute + end +end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index b4728807b8b..82b7fbfa816 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -2,22 +2,25 @@ require 'spec_helper' describe TodoService, services: true do let(:author) { create(:user) } - let(:john_doe) { create(:user, username: 'john_doe') } - let(:michael) { create(:user, username: 'michael') } - let(:stranger) { create(:user, username: 'stranger') } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let(:john_doe) { create(:user) } let(:project) { create(:project) } - let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') } + let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') } let(:service) { described_class.new } before do project.team << [author, :developer] + project.team << [member, :developer] project.team << [john_doe, :developer] - project.team << [michael, :developer] end describe 'Issues' do let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } describe '#new_issue' do it 'creates a todo if assigned' do @@ -37,10 +40,20 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.new_issue(issue, author) - should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + end + + it 'does not create todo for non project members when issue is confidential' do + service.new_issue(confidential_issue, john_doe) + + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED) + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end end @@ -48,16 +61,26 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.update_issue(issue, author) - should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_create_todo(user: member, target: issue, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) end it 'does not create a todo if user was already mentioned' do - create(:todo, :mentioned, user: michael, project: project, target: issue, author: author) + create(:todo, :mentioned, user: member, project: project, target: issue, author: author) - expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count) + expect { service.update_issue(issue, author) }.not_to change(member.todos, :count) + end + + it 'does not create todo for non project members when issue is confidential' do + service.update_issue(confidential_issue, john_doe) + + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end end @@ -109,8 +132,10 @@ describe TodoService, services: true do describe '#new_note' do let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } + let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } let(:system_note) { create(:system_note, project: project, noteable: issue) } @@ -142,19 +167,29 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.new_note(note, john_doe) - should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) - should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + end + + it 'does not create todo for non project members when leaving a note on a confidential issue' do + service.new_note(note_on_confidential_issue, john_doe) + + should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) + should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue) end it 'creates a todo for each valid mentioned user when leaving a note on commit' do service.new_note(note_on_commit, john_doe) - should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end it 'does not create todo when leaving a note on snippet' do @@ -185,10 +220,10 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.new_merge_request(mr_assigned, author) - should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end end @@ -196,16 +231,16 @@ describe TodoService, services: true do it 'creates a todo for each valid mentioned user' do service.update_merge_request(mr_assigned, author) - should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) - should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) end it 'does not create a todo if user was already mentioned' do - create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author) + create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author) - expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count) + expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index ef5ea7d626e..e849a9633b9 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -78,6 +78,6 @@ module FilterSpecHelper # Shortcut to Rails' auto-generated routes helpers, to avoid including the # module def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 73c6792b65f..b87cd6bbca2 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -106,7 +106,7 @@ class MarkdownFeature end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def raw_markdown diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 0d1bd030f3c..71664bb192e 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -15,6 +15,7 @@ module TestEnv 'lfs' => 'be93687', 'master' => '5937ac0', "'test'" => 'e56497b', + 'orphaned-branch' => '45127a9', } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index b11c5de94e3..1abd87d7d33 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -22,6 +22,8 @@ describe MergeWorker do merge_request.reload expect(merge_request).to be_merged + + source_project.repository.expire_branches_cache expect(source_project.repository.branch_names).not_to include('markdown') end end |