diff options
102 files changed, 1255 insertions, 526 deletions
diff --git a/.scss-lint.yml b/.scss-lint.yml index 937d3407b60..3ce0c4901bd 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -132,8 +132,9 @@ linters: SpaceAroundOperator: enabled: false + # Opening braces should be preceded by a single space. SpaceBeforeBrace: - enabled: false + enabled: true StringQuotes: enabled: false diff --git a/CHANGELOG b/CHANGELOG index 744e6b70ba1..f72bb670ece 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,34 +1,39 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - 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) -v 8.6.3 (unreleased) - - Destroy related todos when an Issue/MR is deleted. !3376 - - Fix error 500 when target is nil on todo list. !3376 +v 8.6.4 + - Don't attempt to fetch any tags from a forked repo (Stan Hu) v 8.6.3 - - Fix copying uploads when moving issue to another project - -v 8.6.3 (unreleased) - - Mentions on confidential issues doesn't create todos for non-members - -v 8.6.3 (unreleased) + - 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 @@ -123,6 +128,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/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/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/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/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 b3397d16016..724980b2208 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -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..fc3b0a422a7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -100,6 +100,7 @@ > form { display: inline-block; + margin-top: -1px; } .icon-label { @@ -110,7 +111,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/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2ea6aabe156..98fe794d362 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -209,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/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/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/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/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 47ac22995ab..afa2ca039ae 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -42,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/project.rb b/app/models/project.rb index f208965086d..3e1f04b4158 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -471,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 @@ -592,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 @@ -931,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..e80c2238402 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) @@ -614,10 +614,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 +824,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/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_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/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/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/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/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/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 b7de575cdcd..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 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/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/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/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 e157bb96f2a..f301d42939d 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,6 +1,6 @@ module Gitlab class UrlBuilder - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers include GitlabRoutingHelper include ActionView::RecordIdentifier 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/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/models/repository_spec.rb b/spec/models/repository_spec.rb index 9242a6f1739..c5d5a1c2492 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) @@ -865,4 +865,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/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/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 |