diff options
author | Rémy Coutable <remy@rymai.me> | 2016-01-14 14:23:42 +0100 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2016-01-14 14:23:42 +0100 |
commit | 1b08cd811abeed18c3601e1b997b0566a243662c (patch) | |
tree | 1b8f624b29bd2ae77d9344e31a3afe99f7b14536 | |
parent | c31d777c8f24029d7e11dd1e78eddf0c0b6e6f01 (diff) | |
parent | e47f0e563d127f593f16c0a521c836e21bceb77f (diff) | |
download | gitlab-ce-1b08cd811abeed18c3601e1b997b0566a243662c.tar.gz |
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into add-pagination-headers-to-api
148 files changed, 2131 insertions, 729 deletions
diff --git a/CHANGELOG b/CHANGELOG index 160fc171778..152b1a1a46f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ Please view this file on the master branch, on stable branches it's out of date. +v 8.5.0 (unreleased) + - Remove gray background from layout in UI + v 8.4.0 (unreleased) - Add pagination headers to already paginated API resources + - Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages - Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse) - Improved performance of finding issues for an entire group (Yorick Peterse) - Added custom application performance measuring system powered by InfluxDB (Yorick Peterse) @@ -45,10 +49,12 @@ v 8.4.0 (unreleased) - Allow subsequent validations in CI Linter - Show referenced MRs & Issues only when the current viewer can access them - Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee) + - Add API support for managing build variables of project - Allow broadcast messages to be edited v 8.3.4 - Use gitlab-workhorse 0.5.4 (fixes API routing bug) + - Add build artifacts browser v 8.3.3 - Preserve CE behavior with JIRA integration by only calling API if URL is set diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 63803747413..3b6b453ac51 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,7 +1,7 @@ class @Activities constructor: -> Pager.init 20, true - $(".event-filter .btn").bind "click", (event) => + $(".event-filter a").bind "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() @@ -12,7 +12,7 @@ class @Activities toggleFilter: (sender) -> - sender.toggleClass "active" + sender.closest('li').toggleClass "active" event_filters = $.cookie("event_filter") filter = sender.attr("id").split("_")[0] if event_filters diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index ed0bf2b3f48..1f46e331427 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -48,14 +48,15 @@ class @MergeRequest _this = @ $('a.btn-close, a.btn-reopen').on 'click', (e) -> $this = $(this) - if $this.data('submitted') - return - e.preventDefault() - e.stopImmediatePropagation() shouldSubmit = $this.hasClass('btn-comment') - console.log("shouldSubmit") + if shouldSubmit && $this.data('submitted') + return if shouldSubmit - _this.submitNoteForm($this.closest('form'),$this) + if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen') + e.preventDefault() + e.stopImmediatePropagation() + _this.submitNoteForm($this.closest('form'),$this) + submitNoteForm: (form, $button) => noteText = form.find("textarea.js-note-text").val() diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 9e2dc1250c9..b10e1db7f3f 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -5,7 +5,7 @@ # # ### Example Markup # -# <ul class="nav nav-tabs merge-request-tabs"> +# <ul class="nav-links merge-request-tabs"> # <li class="notes-tab active"> # <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> # Discussion diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 8ba00ecbbab..fcf50dd1b51 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -521,9 +521,13 @@ class @Notes if textarea.val().trim().length > 0 form.find('.js-note-target-reopen').text('Comment & reopen') form.find('.js-note-target-close').text('Comment & close') + form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen') + form.find('.js-note-target-close').addClass('btn-comment-and-close') else form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-close').text('Close') + form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen') + form.find('.js-note-target-close').removeClass('btn-comment-and-close') initTaskList: -> @enableTaskList() diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 48a4971c8fc..fa7641b1676 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -24,6 +24,7 @@ @import "framework/lists.scss"; @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; +@import "framework/nav.scss"; @import "framework/pagination.scss"; @import "framework/panels.scss"; @import "framework/selects.scss"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index fa0e70847f3..8c1bbff8201 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -18,9 +18,9 @@ line-height: 36px; } -.content-block, .gray-content-block { - margin: -$gl-padding; + margin-top: 0; + margin-bottom: -$gl-padding; background-color: $background-color; padding: $gl-padding; margin-bottom: 0px; @@ -86,10 +86,7 @@ .cover-block { text-align: center; background: $background-color; - margin: -$gl-padding; - margin-bottom: 0; - padding: 44px $gl-padding; - border-bottom: 1px solid $border-color; + padding-top: 44px; position: relative; .avatar-holder { @@ -136,3 +133,23 @@ .block-connector { margin-top: -1px; } + +.nav-block { + .controls { + float: right; + + .btn { + padding: 7px 10px; + margin-top: 11px; + } + } +} + +.content-block { + padding: $gl-padding 0; + border-bottom: 1px solid $border-color; + + &.oneline-block { + line-height: 42px; + } +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 97a94638847..d1d692f8384 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -131,6 +131,12 @@ &:last-child { margin-right: 0px; } + &.btn-xs { + margin-right: 3px; + } + } + &.disabled { + pointer-events: auto !important; } } @@ -153,33 +159,6 @@ } } -.btn-group-next { - .btn { - padding: 9px 0px; - font-size: 15px; - color: #7f8fa4; - border-color: #e7e9ed; - width: 140px; - - .badge { - font-weight: normal; - background-color: #eee; - color: #78a; - } - - &.active { - border-color: $gl-info; - background: $gl-info; - color: #fff; - - .badge { - color: $gl-info; - background-color: white; - } - } - } -} - .btn-clipboard { border: none; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 11730000f85..05645116268 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -374,75 +374,6 @@ table { } } -.center-top-menu, .left-top-menu { - @include nav-menu; - text-align: center; - margin-top: 5px; - margin-bottom: $gl-padding; - height: auto; - margin-top: -$gl-padding; - - &.no-bottom { - margin-bottom: 0; - } - - &.no-top { - margin-top: 0; - } - - li a { - display: inline-block; - padding-top: $gl-padding; - padding-bottom: 11px; - margin-bottom: -1px; - } - - &.bottom-border { - border-bottom: 1px solid $border-color; - height: 57px; - } - - &.wide { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - } -} - -.left-top-menu { - text-align: left; - border-bottom: 1px solid #EEE; -} - -.center-middle-menu { - @include nav-menu; - padding: 0; - text-align: center; - margin: -$gl-padding; - margin-top: 0; - margin-bottom: 0; - height: 58px; - border-bottom: 1px solid $border-color; - - li { - &:after { - content: "|"; - color: $border-gray-light; - } - - &:last-child { - &:after { - content: none; - } - } - - > a { - display: inline-block; - text-transform: uppercase; - font-size: 13px; - } - } -} - .dropzone .dz-preview .dz-progress { border-color: $border-color !important; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index cbfd4bc29b6..6ee104ee31a 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -3,11 +3,8 @@ * */ .file-holder { - margin-left: -$gl-padding; - margin-right: -$gl-padding; border: none; - border-top: 1px solid #E7E9EE; - border-bottom: 1px solid #E7E9EE; + border: 1px solid $border-color; &.readme-holder { border-bottom: 0; diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 82eb50ad4be..1bfd0213995 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -8,10 +8,12 @@ .flash-notice { @extend .alert; @extend .alert-info; + margin: 0; } .flash-alert { @extend .alert; @extend .alert-danger; + margin: 0; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4dbbb56104b..ba5e72c8c5a 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -28,6 +28,7 @@ header { min-height: $header-height; background-color: #fff; border: none; + border-bottom: 1px solid #EEE; .container-fluid { width: 100% !important; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index a1a9990241d..e901c78d02f 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -5,8 +5,6 @@ html { } body { - background-color: #F3F3F3 !important; - &.navless { background-color: white !important; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index bbdb1c038c5..c6bc6fb324d 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -109,10 +109,8 @@ ul.content-list { padding: 0; > li { - padding: $gl-padding; + padding: $gl-padding 0; border-color: $table-border-color; - margin-left: -$gl-padding; - margin-right: -$gl-padding; color: $gl-gray; .avatar { @@ -133,6 +131,7 @@ ul.content-list { .panel > .content-list { li { margin: 0; + padding: $gl-padding; } } @@ -148,7 +147,7 @@ ul.controls { > li { float: left; margin-right: 10px; - + &:last-child { margin-right: 0; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 4a00a197d9a..c540e527bed 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -65,13 +65,6 @@ position: relative; } -.md-header { - ul { - float: left; - margin-bottom: 1px; - } -} - .referenced-users { color: #4c4e54; padding-top: 10px; @@ -85,23 +78,6 @@ box-shadow: none; } -.new_note, -.edit_note, -.detail-page-description, -.milestone-description, -.wiki-content, -.merge-request-form { - .nav-tabs { - margin-bottom: 0; - border: none; - - li a, - li.active a { - border: 1px solid #DDD; - } - } -} - .markdown-area { @include border-radius(0); background: #FFF; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 41fd890f14f..1d5000fe388 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -118,38 +118,3 @@ font-size: 16px; line-height: 24px; } - -@mixin nav-menu { - padding: 0; - margin: 0; - list-style: none; - height: 56px; - - li { - display: inline-block; - - a { - padding: 14px; - font-size: 15px; - line-height: 28px; - color: #959494; - border-bottom: 2px solid transparent; - - &:hover, &:active, &:focus { - text-decoration: none; - outline: none; - } - } - - &.active a { - color: #616060; - border-bottom: 2px solid #4688f1; - } - - .badge { - font-weight: normal; - background-color: #eee; - color: #78a; - } - } -} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index c00709fb6bb..28c294714ad 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -9,7 +9,7 @@ padding-right: 5px; } - .nav.nav-tabs > li > a { + .nav-links > li > a { padding: 10px; font-size: 12px; margin-right: 3px; @@ -81,7 +81,7 @@ display: none; } - .center-top-menu, .left-top-menu { + .nav-links, .nav-links { li a { font-size: 14px; padding: 19px 10px; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss new file mode 100644 index 00000000000..c537d97fb24 --- /dev/null +++ b/app/assets/stylesheets/framework/nav.scss @@ -0,0 +1,39 @@ +.nav-links { + padding: 0; + margin: 0; + list-style: none; + height: auto; + border-bottom: 1px solid $border-color; + + li { + display: inline-block; + + a { + display: inline-block; + padding: 14px; + padding-top: $gl-padding; + padding-bottom: 11px; + margin-bottom: -1px; + font-size: 15px; + line-height: 28px; + color: #959494; + border-bottom: 2px solid transparent; + + &:hover, &:active, &:focus { + text-decoration: none; + outline: none; + } + } + + &.active a { + color: #000000; + border-bottom: 2px solid #4688f1; + } + + .badge { + font-weight: normal; + background-color: #eee; + color: #78a; + } + } +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 83243dd2457..540d0b03163 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -21,11 +21,10 @@ .content-wrapper { width: 100%; - padding: 20px; .container-fluid { background: #FFF; - padding: $gl-padding; + padding: 0 $gl-padding; &.container-blank { background: none; diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 793ab3d9bb9..c4e9f467ce4 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,13 +1,11 @@ .table-holder { - margin: -$gl-padding; - margin-top: 0; - margin-bottom: 0; + margin: 0; } table { &.table { margin-bottom: $gl-padding; - + .dropdown-menu a { text-decoration: none; } @@ -32,6 +30,7 @@ table { } th { + background-color: $background-color; font-weight: normal; font-size: 15px; border-bottom: 1px solid $border-color !important; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index ff41e26ed8a..47b843e5e3d 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -5,10 +5,8 @@ padding: 0; .timeline-entry { - padding: $gl-padding; + padding: $gl-padding 0; border-color: $table-border-color; - margin-left: -$gl-padding; - margin-right: -$gl-padding; color: $gl-gray; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 94f0ed761df..88072606bf5 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -99,47 +99,6 @@ } } -// Nav tabs -.nav.nav-tabs { - margin-bottom: 15px; - - li { - > a { - margin-right: 5px; - line-height: 20px; - border-color: #EEE; - color: #888; - border-bottom: 1px solid #ddd; - .badge { - background-color: #eee; - color: #888; - text-shadow: 0 1px 1px #fff; - } - i.fa { - line-height: 14px; - } - } - &.active { - > a { - border-color: #CCC; - border-bottom: 1px solid #fff; - color: #333; - font-weight: bold; - } - } - } -} - -.nav-tabs > li > a, -.nav-pills > li > a { - color: #666; -} - -.nav-pills > .active > a > span > .badge { - background-color: #fff; - color: $gl-primary; -} - /** * fix to keep tooltips position in top navigation bar diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 714369d9f15..ab4f71af039 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -177,7 +177,7 @@ body { } .page-title { - margin-top: 0px; + margin-top: $gl-padding; line-height: 1.3; font-size: 1.25em; font-weight: 600; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 002bd7e8ca5..c3f27333fad 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -4,7 +4,7 @@ position: absolute; top: 0px; right: 4px; - line-height: 40px; + line-height: 56px; } a.js-zen-leave { diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss new file mode 100644 index 00000000000..abae5c3d0a5 --- /dev/null +++ b/app/assets/stylesheets/pages/branches.scss @@ -0,0 +1,3 @@ +.branch-name{ + font-weight: 600; +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 17245d3be7b..6ec88bdd804 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -2,6 +2,10 @@ display: block; } +.commit-row-title .commit-title { + font-weight: 600; +} + .commit-author, .commit-committer{ display: block; color: #999; @@ -35,6 +39,8 @@ } .commit-box { + border-top: 1px solid $border-color; + .commit-title { margin: 0; font-size: 23px; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index deab805dbc2..529a43548c8 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,7 +1,5 @@ .detail-page-header { - margin: -$gl-padding; - padding: 7px $gl-padding; - margin-bottom: 0px; + padding: 11px 0; border-bottom: 1px solid $border-color; color: #5c5d5e; font-size: 16px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index afd6fb73675..685e7697237 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,9 +1,7 @@ // Common .diff-file { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - border: none; - border-bottom: 1px solid #E7E9EE; + border: 1px solid $border-color; + border-top: none; .diff-header { position: relative; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 984b4b91216..8fa15b35748 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,9 +4,7 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px); - margin-left: -$gl-padding; - margin-right: -$gl-padding; + padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px); border-bottom: 1px solid $table-border-color; color: #7f8fa4; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 263993f59a5..3404c2631e1 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -11,3 +11,8 @@ height: 42px; } } + +.content-list .group-name { + font-weight: 600; + color: #4c4e54; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d4b44004f4f..7ab77550496 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -30,7 +30,7 @@ margin-top: 7px; } - .center-top-menu { + .nav-links { text-align: left; } } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 1e1af662850..ad92cc22815 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -6,7 +6,7 @@ .issue-title { margin-bottom: 5px; font-size: $list-font-size; - font-weight: bold; + font-weight: 600; } .issue-info { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 82effde0bf3..75f2ae80a92 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,9 +3,9 @@ * */ .mr-state-widget { - background: #F7F8FA; + background: $background-color; color: $gl-gray; - border: 1px solid #dce0e6; + border: 1px solid $border-color; @include border-radius(2px); form { @@ -150,7 +150,7 @@ .merge-request-title { margin-bottom: 5px; font-size: $list-font-size; - font-weight: bold; + font-weight: 600; } .merge-request-info { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f24b71963a8..bd5f96a87ef 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -26,6 +26,8 @@ } .project-home-panel { + padding-bottom: 40px; + border-bottom: 1px solid $border-color; .cover-controls { .project-settings-dropdown { @@ -226,7 +228,6 @@ } .projects-search-form { - .input-group .form-control { height: 42px; } @@ -351,28 +352,6 @@ color: #555; } -ul.nav.nav-projects-tabs { - @extend .nav-tabs; - - padding-left: 8px; - - li { - a { - padding: 6px 25px; - margin-top: 2px; - border-color: #DDD; - background-color: #EEE; - text-shadow: 0 1px 1px white; - color: #555; - } - &.active { - a { - font-weight: bold; - } - } - } -} - .project_member_row form { margin: 0px; } @@ -416,11 +395,8 @@ ul.nav.nav-projects-tabs { .top-area { border-bottom: 1px solid #EEE; - margin: 0 -16px; - padding: 0 $gl-padding; - height: 42px; - ul.left-top-menu { + ul.nav-links { display: inline-block; width: 50%; margin-bottom: 0px; @@ -482,11 +458,11 @@ table.table.protected-branches-list tr.no-border { padding-top: 10px; padding-bottom: 4px; - ul.nav-pills { + ul.nav { display:inline-block; } - .nav-pills li { + .nav li { display:inline; } @@ -523,8 +499,7 @@ pre.light-well { } .projects-search-form { - margin: -$gl-padding; - padding: $gl-padding; + padding: $gl-padding 0; padding-bottom: 0; margin-bottom: 0px; @@ -574,10 +549,8 @@ pre.light-well { @include basic-list; .project-row { - padding: $gl-padding; + padding: $gl-padding 0; border-color: $table-border-color; - margin-left: -$gl-padding; - margin-right: -$gl-padding; &.no-description { .project { @@ -631,8 +604,6 @@ pre.light-well { } .project-last-commit { - margin: 0 7px; - .ci-status { margin-right: 16px; } @@ -662,9 +633,7 @@ pre.light-well { } .project-show-readme .readme-holder { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - padding: ($gl-padding + 7px); + padding: $gl-padding 0; border-top: 0; .edit-project-readme { diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss new file mode 100644 index 00000000000..e9cd6dc6c5e --- /dev/null +++ b/app/assets/stylesheets/pages/tags.scss @@ -0,0 +1,3 @@ +.tag-name{ + font-weight: 600; +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 97505edeabf..6a6dd7dfc85 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,4 +1,7 @@ .tree-holder { + > .nav-block { + margin: 11px 0; + } .file-finder { width: 50%; @@ -13,7 +16,7 @@ tr { > td, > th { - line-height: 28px; + line-height: 26px; } &:hover { @@ -86,12 +89,14 @@ .blob-commit-info { list-style: none; + padding: $gl-padding; + background: $background-color; + border: 1px solid $border-color; + border-bottom: none; margin: 0; - padding: 0; - margin-bottom: 5px; .commit { - padding: $gl-padding 0; + padding: 0; .commit-row-title { .commit-row-message { @@ -115,3 +120,8 @@ font-weight: normal; color: $md-link-color; } + +.tree-controls { + float: right; + margin-top: 11px; +} diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index e383fe38ea6..79a53556f0a 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController def update if @identity.update_attributes(identity_params) + RepairLdapBlockedUserService.new(@user).execute redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.' else render :edit @@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController def destroy if @identity.destroy + RepairLdapBlockedUserService.new(@user).execute redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.' else redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.' diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d7c927d444c..87f4fb455b8 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController end def unblock - if user.activate + if user.ldap_blocked? + redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab") + elsif user.activate redirect_back_or_admin_user(notice: "Successfully unblocked") else redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked") diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb new file mode 100644 index 00000000000..dff0732bdfe --- /dev/null +++ b/app/controllers/projects/artifacts_controller.rb @@ -0,0 +1,56 @@ +class Projects::ArtifactsController < Projects::ApplicationController + layout 'project' + before_action :authorize_read_build_artifacts! + + def download + unless artifacts_file.file_storage? + return redirect_to artifacts_file.url + end + + unless artifacts_file.exists? + return not_found! + end + + send_file artifacts_file.path, disposition: 'attachment' + end + + def browse + return render_404 unless build.artifacts? + + directory = params[:path] ? "#{params[:path]}/" : '' + @entry = build.artifacts_metadata_entry(directory) + + return render_404 unless @entry.exists? + end + + def file + entry = build.artifacts_metadata_entry(params[:path]) + + if entry.exists? + render json: { archive: build.artifacts_file.path, + entry: Base64.encode64(entry.path) } + else + render json: {}, status: 404 + end + end + + private + + def build + @build ||= project.builds.unscoped.find_by!(id: params[:build_id]) + end + + def artifacts_file + @artifacts_file ||= build.artifacts_file + end + + def authorize_read_build_artifacts! + unless can?(current_user, :read_build_artifacts, @project) + if current_user.nil? + return authenticate_user! + else + return render_404 + end + end + end +end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 39d3ba26ba2..0e965966ffa 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_manage_builds!, except: [:index, :show, :status] - before_action :authorize_download_build_artifacts!, only: [:download] layout "project" @@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController redirect_to build_path(build) end - def download - unless artifacts_file.file_storage? - return redirect_to artifacts_file.url - end - - unless artifacts_file.exists? - return not_found! - end - - send_file artifacts_file.path, disposition: 'attachment' - end - def status render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) end @@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController @build ||= project.builds.unscoped.find_by!(id: params[:id]) end - def artifacts_file - build.artifacts_file - end - def build_path(build) namespace_project_build_path(build.project.namespace, build.project, build) end @@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController return page_404 end end - - def authorize_download_build_artifacts! - unless can?(current_user, :download_build_artifacts, @project) - if current_user.nil? - return authenticate_user! - else - return render_404 - end - end - end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index dde83ff36b5..31bf45baeb7 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -27,13 +27,15 @@ module EventsHelper key = key.to_s active = 'active' if @event_filter.active?(key) link_opts = { - class: "event-filter-link btn btn-default #{active}", + class: "event-filter-link", id: "#{key}_event_filter", title: "Filter by #{tooltip.downcase}", } - link_to request.path, link_opts do - content_tag(:span, ' ' + tooltip) + content_tag :li, class: active do + link_to request.path, link_opts do + content_tag(:span, ' ' + tooltip) + end end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 5a1a67db8e1..5375148a654 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -175,7 +175,7 @@ class Ability :create_merge_request, :create_wiki, :manage_builds, - :download_build_artifacts, + :read_build_artifacts, :push_code ] end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a4779d06de8..6cc26abce66 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -30,10 +30,12 @@ # description :string(255) # artifacts_file :text # gl_project_id :integer +# artifacts_metadata :text # module Ci class Build < CommitStatus + include Gitlab::Application.routes.url_helpers LAZY_ATTRIBUTES = ['trace'] belongs_to :runner, class_name: 'Ci::Runner' @@ -49,6 +51,7 @@ module Ci scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } mount_uploader :artifacts_file, ArtifactUploader + mount_uploader :artifacts_metadata, ArtifactUploader acts_as_taggable @@ -291,21 +294,18 @@ module Ci end def target_url - Gitlab::Application.routes.url_helpers. - namespace_project_build_url(project.namespace, project, self) + namespace_project_build_url(project.namespace, project, self) end def cancel_url if active? - Gitlab::Application.routes.url_helpers. - cancel_namespace_project_build_path(project.namespace, project, self) + cancel_namespace_project_build_path(project.namespace, project, self) end end def retry_url if retryable? - Gitlab::Application.routes.url_helpers. - retry_namespace_project_build_path(project.namespace, project, self) + retry_namespace_project_build_path(project.namespace, project, self) end end @@ -321,20 +321,35 @@ module Ci pending? && !any_runners_online? end - def download_url - if artifacts_file.exists? - Gitlab::Application.routes.url_helpers. - download_namespace_project_build_path(project.namespace, project, self) - end - end - def execute_hooks build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) end + def artifacts? + artifacts_file.exists? + end + + def artifacts_download_url + if artifacts? + download_namespace_project_build_artifacts_path(project.namespace, project, self) + end + end + def artifacts_browse_url + if artifacts_browser_supported? + browse_namespace_project_build_artifacts_path(project.namespace, project, self) + end + end + + def artifacts_browser_supported? + artifacts? && artifacts_metadata.exists? + end + + def artifacts_metadata_entry(path) + Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry + end private diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 7f6f497f325..e786bd7dd93 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -18,8 +18,12 @@ module Ci belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - validates_presence_of :key validates_uniqueness_of :key, scope: :gl_project_id + validates :key, + presence: true, + length: { within: 0..255 }, + format: { with: /\A[a-zA-Z0-9_]+\z/, + message: "can contain only letters, digits and '_'." } attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ff479493474..4ce9b998dfc 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -131,7 +131,11 @@ class CommitStatus < ActiveRecord::Base false end - def download_url + def artifacts_download_url + nil + end + + def artifacts_browse_url nil end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 8bcdc194953..e1915b079d4 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base validates :provider, presence: true validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider } + + def ldap? + provider.starts_with?('ldap') + end end diff --git a/app/models/project.rb b/app/models/project.rb index 31990485f7d..7e131151513 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -397,7 +397,7 @@ class Project < ActiveRecord::Base result.password = '*****' unless result.password.nil? result.to_s rescue - original_url + self.import_url end def check_limit diff --git a/app/models/user.rb b/app/models/user.rb index 46b36c605b0..592468933ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -196,10 +196,22 @@ class User < ActiveRecord::Base state_machine :state, initial: :active do event :block do transition active: :blocked + transition ldap_blocked: :blocked + end + + event :ldap_block do + transition active: :ldap_blocked end event :activate do transition blocked: :active + transition ldap_blocked: :active + end + + state :blocked, :ldap_blocked do + def blocked? + true + end end end @@ -207,7 +219,7 @@ class User < ActiveRecord::Base # Scopes scope :admins, -> { where(admin: true) } - scope :blocked, -> { with_state(:blocked) } + scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb new file mode 100644 index 00000000000..863cef7ff61 --- /dev/null +++ b/app/services/repair_ldap_blocked_user_service.rb @@ -0,0 +1,17 @@ +class RepairLdapBlockedUserService + attr_accessor :user + + def initialize(user) + @user = user + end + + def execute + user.block if ldap_hard_blocked? + end + + private + + def ldap_hard_blocked? + user.ldap_blocked? && !user.ldap_user? + end +end diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 6936e614346..c395bd908c3 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -60,8 +60,8 @@ %td .pull-right - - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url - = link_to build.download_url, title: 'Download artifacts' do + - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts? + = link_to build.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, build.project) - if build.active? diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index ddd4e1481eb..ebf2b7b60e7 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -4,7 +4,7 @@ - if @all_builds.running_or_pending.any? = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - %ul.center-top-menu + %ul.nav-links %li{class: ('active' if @scope.nil?)} = link_to admin_builds_path do All diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 1484baa78e0..af9fdeb0734 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,12 +1,13 @@ - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger] -%ul.nav.nav-tabs.log-tabs +%ul.nav-links.log-tabs - loggers.each do |klass| %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' -%p.light To prevent performance issues admin logs output the last 2000 lines +.gray-content-block + To prevent performance issues admin logs output the last 2000 lines .tab-content - loggers.each do |klass| .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index bc44b1b1d8e..ce5e21e54cc 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -12,7 +12,7 @@ %i.fa.fa-pencil-square-o Edit %hr -%ul.nav.nav-tabs +%ul.nav-links = nav_link(path: 'users#show') do = link_to "Account", admin_user_path(@user) = nav_link(path: 'users#groups') do @@ -23,3 +23,4 @@ = link_to "SSH keys", keys_admin_user_path(@user) = nav_link(controller: :identities) do = link_to "Identities", admin_user_identities_path(@user) +.append-bottom-default diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index a92c9c152b9..b050a4d01c3 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,101 +1,101 @@ - page_title "Users" = render 'shared/show_aside' -.row - %aside.col-md-3 - .admin-filter - %ul.nav.nav-pills.nav-stacked - %li{class: "#{'active' unless params[:filter]}"} - = link_to admin_users_path do - Active - %small.pull-right= number_with_delimiter(User.active.count) - %li{class: "#{'active' if params[:filter] == "admins"}"} - = link_to admin_users_path(filter: "admins") do - Admins - %small.pull-right= number_with_delimiter(User.admins.count) - %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.pull-right= number_with_delimiter(User.with_two_factor.count) - %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.pull-right= number_with_delimiter(User.without_two_factor.count) - %li{class: "#{'active' if params[:filter] == "blocked"}"} - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.pull-right= number_with_delimiter(User.blocked.count) - %li{class: "#{'active' if params[:filter] == "wop"}"} - = link_to admin_users_path(filter: "wop") do - Without projects - %small.pull-right= number_with_delimiter(User.without_projects.count) - %hr - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false - = hidden_field_tag "filter", params[:filter] - = button_tag class: 'btn btn-primary' do - %i.fa.fa-search - %hr - = link_to 'Reset', admin_users_path, class: "btn btn-cancel" +.admin-filter + %ul.nav-links + %li{class: "#{'active' unless params[:filter]}"} + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + %li{class: "#{'active' if params[:filter] == "admins"}"} + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + %li{class: "#{'active' if params[:filter] == "blocked"}"} + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + %li{class: "#{'active' if params[:filter] == "wop"}"} + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) - %section.col-md-9 - .panel.panel-default - .panel-heading - Users (#{number_with_delimiter(@users.total_count)}) - .panel-head-actions - .dropdown.inline - %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_name - %b.caret - %ul.dropdown-menu - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated - - = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" - %ul.well-list - - @users.each do |user| + .gray-content-block.second-block + .pull-right + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_name + %b.caret + %ul.dropdown-menu %li - .list-item-name - - if user.blocked? - %i.fa.fa-lock.cred + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated + + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = form_tag admin_users_path, method: :get, class: 'form-inline' do + .form-group + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false + = hidden_field_tag "filter", params[:filter] + = button_tag class: 'btn btn-primary' do + %i.fa.fa-search + + +.panel.panel-default + %ul.well-list + - @users.each do |user| + %li + .list-item-name + - if user.blocked? + %i.fa.fa-lock.cred + - else + %i.fa.fa-user.cgreen + = link_to user.name, [:admin, user] + - if user.admin? + %strong.cred (Admin) + - if user == current_user + %span.cred It's you! + .pull-right + %span.light + %i.fa.fa-envelope + = mail_to user.email, user.email, class: 'light' + + .pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' + - unless user == current_user + - if user.ldap_blocked? + = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do + %i.fa.fa-lock + Unblock + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' - else - %i.fa.fa-user.cgreen - = link_to user.name, [:admin, user] - - if user.admin? - %strong.cred (Admin) - - if user == current_user - %span.cred It's you! - .pull-right - %span.light - %i.fa.fa-envelope - = mail_to user.email, user.email, class: 'light' - - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" - - unless user == current_user - - if user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" - - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning" - - if user.access_locked? - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" - = paginate @users, theme: "gitlab" + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' + - if user.access_locked? + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' += paginate @users, theme: "gitlab" diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index f98fd9f06ba..dc76599b776 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,9 +1,9 @@ .hidden-xs = render "events/event_last_push", event: @last_push -.gray-content-block +.nav-block - if current_user - .pull-right + .controls = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index 9f4be025bf2..b78e70ebc1e 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links %li{ class: ("active" unless params[:filter]) } = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 64bd356f546..6ca97a692b4 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(page: dashboard_groups_path) do = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do Your Groups diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index f4a3e3162bf..5c4b58cd688 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,7 +1,7 @@ = content_for :flash_message do = render 'shared/project_limit' .top-area - %ul.left-top-menu + %ul.nav-links = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do Your Projects diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index 0ae62d6f1b6..b25e8ea1f0c 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do Your Snippets diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 49a558e8ac9..3810267577c 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -48,7 +48,7 @@ #{@milestone.open_items_count} open = milestone_progress_bar(@milestone) -%ul.center-top-menu.no-top.no-bottom +%ul.nav-links.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 07b6d57932e..d4e7862981c 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -3,32 +3,36 @@ = render 'dashboard/snippets_head' -.gray-content-block - .pull-right +.nav-block + .controls = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do = icon('plus') New Snippet - .btn-group.btn-group-next.snippet-scope-menu - = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do - All - %span.badge - = current_user.snippets.count - - = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do - Private - %span.badge - = current_user.snippets.are_private.count - - = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do - Internal - %span.badge - = current_user.snippets.are_internal.count - - = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do - Public - %span.badge - = current_user.snippets.are_public.count + .nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to dashboard_snippets_path do + All + %span.badge + = current_user.snippets.count + + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to dashboard_snippets_path(scope: 'are_private') do + Private + %span.badge + = current_user.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to dashboard_snippets_path(scope: 'are_internal') do + Internal + %span.badge + = current_user.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to dashboard_snippets_path(scope: 'are_public') do + Public + %span.badge + = current_user.snippets.are_public.count = render 'snippets/snippets' diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 41ad2c231d4..2c15e2c4891 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -7,7 +7,7 @@ %h3 Sign in .login-body - if form_based_providers.any? - %ul.nav.nav-tabs + %ul.nav-links - if crowd_enabled? %li.active = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 7e3e2e28bc9..e2f97fd9337 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,7 +1,7 @@ - header_title group_title(@group, "Settings", edit_group_path(@group)) - @blank_container = true -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading Group settings .panel-body diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 335bf036074..6a8acc42af9 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -2,7 +2,7 @@ - header_title group_title(@group, "Members", group_group_members_path(@group)) - @blank_container = true -.group-members-page +.group-members-page.prepend-top-default - if current_user && current_user.can?(:admin_group_member, @group) .panel.panel-default .panel-heading diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index d063b257b5e..1233da85524 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -54,7 +54,7 @@ #{@milestone.open_items_count} open = milestone_progress_bar(@milestone) -%ul.center-top-menu.no-top.no-bottom +%ul.nav-links.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index f1d507a50c7..9ca11ed1177 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,7 +1,7 @@ - page_title "Projects" - header_title group_title(@group, "Projects", projects_group_path(@group)) -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading %strong= @group.name projects: diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 48a544fc834..5e39d3ab601 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,6 @@ +- @no_container = true +- @blank_container = true + - unless can?(current_user, :read_group, @group) - @disable_search_panel = true @@ -25,8 +28,8 @@ .cover-desc.description = markdown(@group.description, pipeline: :description) -- if can?(current_user, :read_group, @group) - %ul.center-top-menu.no-top + + %ul.nav-links %li.active = link_to "#activity", 'data-toggle' => 'tab' do Activity @@ -35,20 +38,22 @@ = link_to "#projects", 'data-toggle' => 'tab' do Projects - .tab-content - .tab-pane.active#activity - .gray-content-block.activity-filter-block - - if current_user - = render "events/event_last_push", event: @last_push +- if can?(current_user, :read_group, @group) + %div{ class: container_class } + .tab-content + .tab-pane.active#activity + .activity-filter-block + - if current_user + = render "events/event_last_push", event: @last_push - = render 'shared/event_filter' + = render 'shared/event_filter' - .content_list - = spinner + .content_list + = spinner - .tab-pane#projects - = render "projects", projects: @projects + .tab-pane#projects + = render "projects", projects: @projects - else - %p.center-top-menu.no-top + %p.nav-links.no-top No projects to show diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index d9ffda884c8..7b45bd09050 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -139,26 +139,9 @@ %h2#navs Navigation %h4 - %code .center-top-menu + %code .nav-links .example - %ul.center-top-menu - %li.active - %a Open - %li - %a Closed - - %h4 - %code .btn-group.btn-group-next - .example - %div.btn-group.btn-group-next - %a.btn.active Open - %a.btn Closed - - - %h4 - %code .nav.nav-tabs - .example - %ul.nav.nav-tabs + %ul.nav-links %li.active %a Open %li diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index ec7cd79bc54..26159989777 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -24,7 +24,7 @@ .content-wrapper = render "layouts/flash" = yield :flash_message - %div{ class: container_class } + %div{ class: (container_class unless @no_container) } .content .clearfix = yield diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 17e47c622ce..a42fd38de3a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -6,7 +6,7 @@ .alert.alert-info Some options are unavailable for LDAP accounts -.account-page +.account-page.prepend-top-default .panel.panel-default.update-token .panel-heading Reset Private token diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 101880bd105..961b61d2e76 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,6 +1,6 @@ -.gray-content-block.activity-filter-block +.nav-block.activity-filter-block - if current_user - .pull-right + .controls = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do %i.fa.fa-rss diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index fa978325ddd..96c2fa87f45 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -1,5 +1,5 @@ #tree-holder.tree-holder.clearfix - .gray-content-block.second-block + .nav-block = render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_content', tree: @tree diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 54c818baaf4..1fb37ef6621 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,6 +1,6 @@ .md-area .md-header.clearfix - %ul.center-top-menu + %ul.nav-links %li.active %a.js-md-write-button(href="#md-write-holder" tabindex="-1") Write diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml new file mode 100644 index 00000000000..5b87d55efd5 --- /dev/null +++ b/app/views/projects/artifacts/_tree_directory.html.haml @@ -0,0 +1,7 @@ +%tr{ class: 'tree-item' } + %td.tree-item-file-name + = tree_icon('folder', '755', directory.name) + %span.str-truncated + = link_to directory.name, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path) + %td + %td diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml new file mode 100644 index 00000000000..92c1648f726 --- /dev/null +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -0,0 +1,11 @@ +%tr{ class: 'tree-item' } + %td.tree-item-file-name + = tree_icon('file', '664', file.name) + %span.str-truncated + = file.name + %td + = number_to_human_size(file.metadata[:size], precision: 2) + %td + = link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path), + class: 'btn btn-xs btn-default artifact-download' do + = icon('download') diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml new file mode 100644 index 00000000000..1add7ef6bfb --- /dev/null +++ b/app/views/projects/artifacts/browse.html.haml @@ -0,0 +1,24 @@ +- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' += render 'projects/builds/header_title' + +#tree-holder.tree-holder + .gray-content-block.top-block.clearfix + .pull-right + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), + class: 'btn btn-default' do + = icon('download') + Download artifacts archive + +%div.tree-content-holder + .table-holder + %table.table.tree-table.table-striped + %thead + %tr + %th Name + %th Size + %th Download + = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory + = render partial: 'tree_file', collection: @entry.files, as: :file + +- if @entry.empty? + .center Empty diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 2a3315da3db..3d8d88834e2 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.top-block +.nav-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'blob', path: @path diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 09fa148b129..a279e6eda55 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,7 +2,7 @@ = render "header_title" .file-editor - %ul.center-top-menu.no-bottom.js-edit-mode + %ul.nav-links.no-bottom.js-edit-mode %li.active = link_to '#editor' do = icon('edit') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index a234536723e..d276e5932d1 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -6,7 +6,7 @@ %li(class="js-branch-#{branch.name}") %div = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do - %strong.str-truncated= branch.name + .branch-name.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-primary default diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 3bbfdb1e3b0..5d18c0d803a 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -8,7 +8,7 @@ - if @all_builds.running_or_pending.any? = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - %ul.center-top-menu + %ul.nav-links %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do All diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 5b7ecce86ab..2be572d3b10 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -14,7 +14,7 @@ #up-build-trace - if @commit.matrix_for_ref?(@build.ref) - %ul.center-top-menu.no-top.no-bottom + %ul.nav-links.no-top.no-bottom - @commit.latest_builds_for_ref(@build.ref).each do |build| %li{class: ('active' if build == @build) } = link_to namespace_project_build_path(@project.namespace, @project, build) do @@ -89,9 +89,15 @@ Test coverage %h1 #{@build.coverage}% - - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url - .build-widget.center - = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary' + - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts? + + .build-widget.artifacts + %h4.title Build artifacts + .center + .btn-group{ role: :group } + = link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary' + - if @build.artifacts_browser_supported? + = link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary' .build-widget %h4.title diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index f74f8b427ec..ea33aa472a6 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu.no-top.no-bottom.commit-ci-menu +%ul.nav-links.no-top.no-bottom.commit-ci-menu = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index ddb77fd796b..bbe820b8842 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -50,7 +50,7 @@ .commit-info-row.branches %i.fa.fa-spinner.fa-spin -.commit-box.gray-content-block.middle-block +.commit-box.content-block %h3.commit-title = markdown escape_once(@commit.title), pipeline: :single_line - if @commit.description.present? diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 58aa45e8d2c..02297158dec 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -2,7 +2,9 @@ - page_description @commit.description = render "projects/commits/header_title" -= render "commit_box" + +.prepend-top-default + = render "commit_box" - if @ci_commit = render "ci_menu" - else diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 74a05df24d3..1736dccaf3c 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -66,8 +66,8 @@ %td .pull-right - - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url - = link_to commit_status.download_url, title: 'Download artifacts' do + - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts? + = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do %i.fa.fa-download - if current_user && can?(current_user, :manage_builds, commit_status.project) - if commit_status.active? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 012825f0fdb..4d4b410ee29 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -11,7 +11,7 @@ = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } .commit-row-title - %strong.str-truncated + .commit-title.str-truncated = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index fcccb002d7e..498c5e05b32 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 034057da42e..ede64d47ab3 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -6,7 +6,7 @@ = render "head" -.gray-content-block +.gray-content-block.second-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f9d661d59d2..f67058ae0ba 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - diff_files = safe_diff_files(diffs) -.gray-content-block.middle-block.oneline-block +.content-block.oneline-block .inline-parallel-buttons .btn-group = inline_diff_btn diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 31e752c6649..8a99aceef7f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,6 +1,6 @@ - @blank_container = true -.project-edit-container +.project-edit-container.prepend-top-default .project-edit-errors .project-edit-content .panel.panel-default diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index a47643bd09c..79a56647c53 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,4 +1,4 @@ -%ul.center-top-menu +%ul.nav-links = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index a14943b15d3..dd2c59e112a 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -18,7 +18,7 @@ = f.hidden_field :target_branch .mr-compare.merge-request - %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %ul.merge-request-tabs.nav-links.no-top.no-bottom %li.commits-tab = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 095876450a0..200bfa5ac4f 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -45,7 +45,7 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits.present? - %ul.merge-request-tabs.center-top-menu.no-top.no-bottom + %ul.merge-request-tabs.nav-links.no-top.no-bottom %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index d1db0f64f88..1142c584592 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -57,7 +57,7 @@ %span.pull-right= @milestone.expires_at = milestone_progress_bar(@milestone) -%ul.center-top-menu.no-top.no-bottom +%ul.nav-links.no-top.no-bottom %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 29225a36364..6239a148905 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -2,7 +2,7 @@ = render "header_title" - @blank_container = true -.project-members-page +.project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 315afe4a764..2d5b9f43c24 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -1,5 +1,6 @@ - page_title "Runners" -.light + +.light.prepend-top-default %p A 'runner' is a process which runs a build. You can setup as many runners as you need. diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8436be433b1..797bee708bd 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,3 +1,6 @@ +- @no_container = true +- @blank_container = true + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity") @@ -8,11 +11,10 @@ = render 'shared/no_password' = render 'projects/last_push' - = render "home_panel" .project-stats.gray-content-block.second-block - %ul.nav.nav-pills + %ul.nav %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = pluralize(number_with_delimiter(@project.commit_count), 'commit') @@ -57,15 +59,17 @@ = link_to add_contribution_guide_path(@project) do Add Contribution guide -- if @project.archived? - .text-warning.center.prepend-top-20 - %p - = icon("exclamation-triangle fw") - Archived project! Repository is read-only - - if @repository.commit .content-block.second-block.white - = render 'projects/last_commit', commit: @repository.commit, project: @project + %div{ class: container_class } + = render 'projects/last_commit', commit: @repository.commit, project: @project + +%div{ class: container_class } + - if @project.archived? + .text-warning.center.prepend-top-20 + %p + = icon("exclamation-triangle fw") + Archived project! Repository is read-only -%div{class: "project-show-#{default_project_view}"} - = render default_project_view + %div{class: "project-show-#{default_project_view}"} + = render default_project_view diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 28b706c5c7e..56a7ced1236 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -3,7 +3,7 @@ %li %div = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do - %strong + .tag-name = icon('tag') = tag.name - if tag.message.present? diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index b594d4f1f27..dbb20347860 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -17,8 +17,8 @@ .pull-right = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do %i.fa.fa-trash-o - .title - %strong= @tag.name + .tag-name.title + = @tag.name - if @tag.message.present? %span.light diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 1927883513a..558e6146ae9 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,6 +1,6 @@ %div.tree-content-holder .table-holder - %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" } + %table.table#tree-slider{class: "table_#{@hex_path} tree-table" } %thead %tr %th Name diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index c57570afa09..91fb2a44594 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -5,13 +5,13 @@ = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' -.pull-right +.tree-controls = render 'projects/find_file_link' - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true #tree-holder.tree-holder.clearfix - .gray-content-block.top-block + .nav-block = render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_content', tree: @tree diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index e6e6ad5bc4b..69ba301e231 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -7,7 +7,7 @@ = render 'projects/wikis/new' - %ul.center-top-menu + %ul.nav-links = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 481451edb23..2c3fca439f3 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-tabs.search-filter +%ul.nav-links.search-filter - if @project %li{class: ("active" if @scope == 'blobs')} = link_to search_filter_path(scope: 'blobs') do diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 2a38c98dcfc..d0e64537621 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,7 +1,7 @@ - if @search_results.empty? = render partial: "search/results/empty" - else - %p.light + .gray-content-block Search results for %code = @search_term diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index f4f3dcfc29f..215dbb3909e 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,5 +1,7 @@ - page_title @search_term -= render 'search/form' + +.prepend-top-default + = render 'search/form' - if @search_term = render 'search/category' = render 'search/results' diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 8495774accc..c38d9313dba 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,4 +1,4 @@ -.btn-group.btn-group-next.event-filter +%ul.nav-links.event-filter = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index cbdecda4fff..f77feeb79cd 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,5 +1,5 @@ .milestones-filters - %ul.center-top-menu + %ul.nav-links %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} = link_to milestones_filter_path(state: 'opened') do Open diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index a54c5fa8c33..f4cfa29ae56 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -10,8 +10,7 @@ %i.fa.fa-sign-out = image_tag group_icon(group), class: "avatar s46 hidden-xs" - = link_to group, class: 'group-name' do - %strong= group.name + = link_to group.name, group, class: 'group-name' - if group_member as diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0e3e9275fc1..8d6f47b38ef 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,6 +1,6 @@ .issues-filters .issues-state-filters - %ul.center-top-menu + %ul.nav-links - if defined?(type) && type == :merge_requests - page_context_word = 'merge requests' - else diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml index 4a84348ac82..83f61ce4b07 100644 --- a/app/views/sherlock/queries/show.html.haml +++ b/app/views/sherlock/queries/show.html.haml @@ -1,7 +1,7 @@ - page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query') - header_title t('sherlock.title'), sherlock_transactions_path -%ul.center-top-menu +%ul.nav-links %li.active %a(href="#tab-general" data-toggle="tab") = t('sherlock.general') diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index 3c8ffb06648..9d4b0b2724c 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -1,7 +1,7 @@ - page_title t('sherlock.title'), t('sherlock.transaction') - header_title t('sherlock.title'), sherlock_transactions_path -%ul.center-top-menu +%ul.nav-links %li.active %a(href="#tab-general" data-toggle="tab") = t('sherlock.general') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index ce17fc7bca1..284a62e6f47 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,6 +1,8 @@ - page_title @user.name - page_description @user.bio - header_title @user.name, user_path(@user) +- @no_container = true +- @blank_container = true = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") @@ -8,6 +10,25 @@ = render 'shared/show_aside' .cover-block + .cover-controls + - if @user == current_user + = link_to profile_path, class: 'btn btn-gray' do + = icon('pencil') + - elsif current_user + %span.report-abuse + - if @user.abuse_report + %button.btn.btn-danger{ title: 'Already reported for abuse', + data: { toggle: 'tooltip', placement: 'left', container: 'body' }} + = icon('exclamation-circle') + - else + = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray', + title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do + = icon('exclamation-circle') + - if current_user + + = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do + = icon('rss') + .avatar-holder = link_to avatar_icon(@user, 400), target: '_blank' do = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' @@ -47,74 +68,56 @@ = icon('map-marker') = @user.location + %ul.nav-links.center + %li.active + = link_to "#activity", 'data-toggle' => 'tab' do + Activity + - if @groups.any? + %li + = link_to "#groups", 'data-toggle' => 'tab' do + Groups + - if @contributed_projects.present? + %li + = link_to "#contributed", 'data-toggle' => 'tab' do + Contributed projects + - if @projects.present? + %li + = link_to "#personal", 'data-toggle' => 'tab' do + Personal projects - .cover-controls - - if @user == current_user - = link_to profile_path, class: 'btn btn-gray' do - = icon('pencil') - - elsif current_user - %span.report-abuse - - if @user.abuse_report - %button.btn.btn-danger{ title: 'Already reported for abuse', - data: { toggle: 'tooltip', placement: 'left', container: 'body' }} - = icon('exclamation-circle') - - else - = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray', - title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do - = icon('exclamation-circle') - - if current_user - - = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do - = icon('rss') - -.gray-content-block.second-block - .user-calendar - %h4.center.light - %i.fa.fa-spinner.fa-spin - .user-calendar-activities - +%div{ class: container_class } + .tab-content + .tab-pane.active#activity + .gray-content-block.white.second-block + %div{ class: container_class } + .user-calendar + %h4.center.light + %i.fa.fa-spinner.fa-spin + .user-calendar-activities -%ul.center-top-menu.no-top.no-bottom.bottom-border.wide - %li.active - = link_to "#activity", 'data-toggle' => 'tab' do - Activity - - if @groups.any? - %li - = link_to "#groups", 'data-toggle' => 'tab' do - Groups - - if @contributed_projects.present? - %li - = link_to "#contributed", 'data-toggle' => 'tab' do - Contributed projects - - if @projects.present? - %li - = link_to "#personal", 'data-toggle' => 'tab' do - Personal projects -.tab-content - .tab-pane.active#activity - .content_list - = spinner + .content_list + = spinner - - if @groups.any? - .tab-pane#groups - %ul.content-list - - @groups.each do |group| - = render 'shared/groups/group', group: group + - if @groups.any? + .tab-pane#groups + %ul.content-list + - @groups.each do |group| + = render 'shared/groups/group', group: group - - if @contributed_projects.present? - .tab-pane#contributed - .contributed-projects - = render 'shared/projects/list', - projects: @contributed_projects.sort_by(&:star_count).reverse, - projects_limit: 5, stars: true, avatar: true + - if @contributed_projects.present? + .tab-pane#contributed + .contributed-projects + = render 'shared/projects/list', + projects: @contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: true - - if @projects.present? - .tab-pane#personal - .personal-projects - = render 'shared/projects/list', - projects: @projects.sort_by(&:star_count).reverse, - projects_limit: 10, stars: true, avatar: true + - if @projects.present? + .tab-pane#personal + .personal-projects + = render 'shared/projects/list', + projects: @projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: true :javascript $(".user-calendar").load("#{user_calendar_path}"); diff --git a/config/routes.rb b/config/routes.rb index 05d6ff1e884..0a29782f55b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -604,9 +604,14 @@ Rails.application.routes.draw do member do get :status post :cancel - get :download post :retry end + + resource :artifacts, only: [] do + get :download + get :browse, path: 'browse(/*path)', format: false + get :file, path: 'file/*path', format: false + end end resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb new file mode 100644 index 00000000000..03a12323845 --- /dev/null +++ b/db/fixtures/development/14_builds.rb @@ -0,0 +1,79 @@ +class Gitlab::Seeder::Builds + BUILD_STATUSES = %w(running pending success failed canceled) + + def initialize(project) + @project = project + end + + def seed! + ci_commits.each do |ci_commit| + build = Ci::Build.new(build_attributes_for(ci_commit)) + + artifacts_cache_file(artifacts_archive_path) do |file| + build.artifacts_file = file + end + + artifacts_cache_file(artifacts_metadata_path) do |file| + build.artifacts_metadata = file + end + + begin + build.save! + print '.' + rescue ActiveRecord::RecordInvalid + print 'F' + end + end + end + + def ci_commits + commits = @project.repository.commits('master', nil, 5) + commits_sha = commits.map { |commit| commit.raw.id } + commits_sha.map do |sha| + @project.ensure_ci_commit(sha) + end + rescue + [] + end + + def build_attributes_for(ci_commit) + { name: 'test build', commands: "$ build command", + stage: 'test', stage_idx: 1, ref: 'master', + user_id: build_user, gl_project_id: @project.id, + status: build_status, commit_id: ci_commit.id, + created_at: Time.now, updated_at: Time.now } + end + + def build_user + @project.team.users.sample + end + + def build_status + BUILD_STATUSES.sample + end + + def artifacts_archive_path + Rails.root + 'spec/fixtures/ci_build_artifacts.zip' + end + + def artifacts_metadata_path + Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + + end + + def artifacts_cache_file(file_path) + cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_") + + FileUtils.copy(file_path, cache_path) + File.open(cache_path) do |file| + yield file + end + end +end + +Gitlab::Seeder.quiet do + Project.all.sample(10).each do |project| + project_builds = Gitlab::Seeder::Builds.new(project) + project_builds.seed! + end +end diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb new file mode 100644 index 00000000000..6c282fc5039 --- /dev/null +++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb @@ -0,0 +1,5 @@ +class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration + def change + add_column :ci_builds, :artifacts_metadata, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 42c3e79f9d7..2fc8c4d5ed4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -123,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160113111034) do t.string "description" t.text "artifacts_file" t.integer "gl_project_id" + t.text "artifacts_metadata" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree diff --git a/doc/api/README.md b/doc/api/README.md index 25a31b235cc..c3401bcbc9d 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -23,6 +23,7 @@ - [Namespaces](namespaces.md) - [Settings](settings.md) - [Keys](keys.md) +- [Build Variables](build_variables.md) ## Clients diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md new file mode 100644 index 00000000000..b96f1bdac8a --- /dev/null +++ b/doc/api/build_variables.md @@ -0,0 +1,128 @@ +# Build Variables + +## List project variables + +Get list of a project's build variables. + +``` +GET /projects/:id/variables +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer | yes | The ID of a project | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" +``` + +```json +[ + { + "key": "TEST_VARIABLE_1", + "value": "TEST_1" + }, + { + "key": "TEST_VARIABLE_2", + "value": "TEST_2" + } +] +``` + +## Show variable details + +Get the details of a project's specific build variable. + +``` +GET /projects/:id/variables/:key +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-----------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable | + +``` +curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1" +``` + +```json +{ + "key": "TEST_VARIABLE_1", + "value": "TEST_1" +} +``` + +## Create variable + +Create a new build variable. + +``` +POST /projects/:id/variables +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-----------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed | +| `value` | string | yes | The `value` of a variable | + +``` +curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value" +``` + +```json +{ + "key": "NEW_VARIABLE", + "value": "new value" +} +``` + +## Update variable + +Update a project's build variable. + +``` +PUT /projects/:id/variables/:key +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-------------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable | +| `value` | string | yes | The `value` of a variable | + +``` +curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value" +``` + +```json +{ + "key": "NEW_VARIABLE", + "value": "updated value" +} +``` + +## Remove variable + +Remove a project's build variable. + +``` +DELETE /projects/:id/variables/:key +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|-------------------------| +| `id` | integer | yes | The ID of a project | +| `key` | string | yes | The `key` of a variable | + +``` +curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" +``` + +```json +{ + "key": "VARIABLE_1", + "value": "VALUE_1" +} +``` diff --git a/doc/api/users.md b/doc/api/users.md index 773fe36d277..b7fc903825e 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -558,7 +558,8 @@ Parameters: - `uid` (required) - id of specified user -Will return `200 OK` on success, or `404 User Not Found` is user cannot be found. +Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +`403 Forbidden` when trying to block an already blocked user by LDAP synchronization. ## Unblock user @@ -572,4 +573,5 @@ Parameters: - `uid` (required) - id of specified user -Will return `200 OK` on success, or `404 User Not Found` is user cannot be found. +Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. diff --git a/features/project/builds.feature b/features/project/builds.feature new file mode 100644 index 00000000000..c00b0a7ae07 --- /dev/null +++ b/features/project/builds.feature @@ -0,0 +1,58 @@ +Feature: Project Builds + Background: + Given I sign in as a user + And I own a project + And CI is enabled + And I have recent build for my project + + Scenario: I browse build summary page + When I visit recent build summary page + Then I see summary for build + And I see build trace + + Scenario: I download build artifacts + Given recent build has artifacts available + When I visit recent build summary page + And I click artifacts download button + Then download of build artifacts archive starts + + Scenario: I browse build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build summary page + And I click artifacts browse button + Then I should see content of artifacts archive + + Scenario: I browse subdirectory of build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build summary page + And I click artifacts browse button + And I click link to subdirectory within build artifacts + Then I should see content of subdirectory within artifacts archive + + Scenario: I browse directory with UTF-8 characters in name + Given recent build has artifacts available + And recent build has artifacts metadata available + And recent build artifacts contain directory with UTF-8 characters + When I visit recent build summary page + And I click artifacts browse button + And I navigate to directory with UTF-8 characters in name + Then I should see content of directory with UTF-8 characters in name + + Scenario: I try to browse directory with invalid UTF-8 characters in name + Given recent build has artifacts available + And recent build has artifacts metadata available + And recent build artifacts contain directory with invalid UTF-8 characters + When I visit recent build summary page + And I click artifacts browse button + And I navigate to parent directory of directory with invalid name + Then I should not see directory with invalid name on the list + + Scenario: I download a single file from build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build summary page + And I click artifacts browse button + And I click download button for a file within build artifacts + Then download of a file extracted from build artifacts should start diff --git a/features/steps/project/builds.rb b/features/steps/project/builds.rb new file mode 100644 index 00000000000..28395281077 --- /dev/null +++ b/features/steps/project/builds.rb @@ -0,0 +1,89 @@ +class Spinach::Features::ProjectBuilds < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include RepoHelpers + + step 'I see summary for build' do + expect(page).to have_content "Build ##{@build.id}" + end + + step 'I see build trace' do + expect(page).to have_css '#build-trace' + end + + step 'I click artifacts download button' do + page.within('.artifacts') { click_link 'Download' } + end + + step 'download of build artifacts archive starts' do + expect(page.response_headers['Content-Type']).to eq 'application/zip' + expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary' + end + + step 'I click artifacts browse button' do + page.within('.artifacts') { click_link 'Browse' } + end + + step 'I should see content of artifacts archive' do + page.within('.tree-table') do + expect(page).to have_no_content '..' + expect(page).to have_content 'other_artifacts_0.1.2' + expect(page).to have_content 'ci_artifacts.txt' + expect(page).to have_content 'rails_sample.jpg' + end + end + + step 'I click link to subdirectory within build artifacts' do + page.within('.tree-table') { click_link 'other_artifacts_0.1.2' } + end + + step 'I should see content of subdirectory within artifacts archive' do + page.within('.tree-table') do + expect(page).to have_content '..' + expect(page).to have_content 'another-subdirectory' + expect(page).to have_content 'doc_sample.txt' + end + end + + step 'recent build artifacts contain directory with UTF-8 characters' do + # metadata fixture contains relevant directory + end + + step 'I navigate to directory with UTF-8 characters in name' do + page.within('.tree-table') { click_link 'tests_encoding' } + page.within('.tree-table') { click_link 'utf8 test dir ✓' } + end + + step 'I should see content of directory with UTF-8 characters in name' do + page.within('.tree-table') do + expect(page).to have_content '..' + expect(page).to have_content 'regular_file_2' + end + end + + step 'recent build artifacts contain directory with invalid UTF-8 characters' do + # metadata fixture contains relevant directory + end + + step 'I navigate to parent directory of directory with invalid name' do + page.within('.tree-table') { click_link 'tests_encoding' } + end + + step 'I should not see directory with invalid name on the list' do + page.within('.tree-table') do + expect(page).to have_no_content('non-utf8-dir') + end + end + + step 'I click download button for a file within build artifacts' do + page.within('.tree-table') { first('.artifact-download').click } + end + + step 'download of a file extracted from build artifacts should start' do + # this will be accelerated by Workhorse + response_json = JSON.parse(page.body, symbolize_names: true) + expect(response_json[:archive]).to end_with('build_artifacts.zip') + expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt') + end +end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index eb2ccd9d01e..0bee91d758d 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -6,7 +6,7 @@ module SharedActiveTab end def ensure_active_sub_tab(content) - expect(find('div.content ul.center-top-menu li.active')).to have_content(content) + expect(find('div.content ul.nav-links li.active')).to have_content(content) end def ensure_active_sub_nav(content) @@ -18,7 +18,7 @@ module SharedActiveTab end step 'no other sub tabs should be active' do - expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1) + expect(page).to have_selector('div.content ul.nav-links li.active', count: 1) end step 'no other sub navs should be active' do diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb new file mode 100644 index 00000000000..a83d74e5946 --- /dev/null +++ b/features/steps/shared/builds.rb @@ -0,0 +1,28 @@ +module SharedBuilds + include Spinach::DSL + + step 'CI is enabled' do + @project.enable_ci + end + + step 'I have recent build for my project' do + ci_commit = create :ci_commit, project: @project, sha: sample_commit.id + @build = create :ci_build, commit: ci_commit + end + + step 'I visit recent build summary page' do + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + step 'recent build has artifacts available' do + artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip' + archive = fixture_file_upload(artifacts, 'application/zip') + @build.update_attributes(artifacts_file: archive) + end + + step 'recent build has artifacts metadata available' do + metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + gzip = fixture_file_upload(metadata, 'application/x-gzip') + @build.update_attributes(artifacts_metadata: gzip) + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index 7834262d612..098dd975840 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -54,5 +54,6 @@ module API mount Keys mount Tags mount Triggers + mount Variables end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e3bc3316ce5..e2feb7bb6d9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -366,5 +366,9 @@ module API class TriggerRequest < Grape::Entity expose :id, :variables end + + class Variable < Grape::Entity + expose :key, :value + end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 312ef90915f..6d2380cf47d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -287,12 +287,14 @@ module API # file helpers - def uploaded_file!(field, uploads_path) + def uploaded_file(field, uploads_path) if params[field] bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename) return params[field] end + return nil unless params["#{field}.path"] && params["#{field}.name"] + # sanitize file paths # this requires all paths to exist required_attributes! %W(#{field}.path) diff --git a/lib/api/users.rb b/lib/api/users.rb index 0d7813428e2..fd2128bd179 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -284,10 +284,12 @@ module API authenticated_as_admin! user = User.find_by(id: params[:id]) - if user + if !user + not_found!('User') + elsif !user.ldap_blocked? user.block else - not_found!('User') + forbidden!('LDAP blocked users cannot be modified by the API') end end @@ -299,10 +301,12 @@ module API authenticated_as_admin! user = User.find_by(id: params[:id]) - if user - user.activate - else + if !user not_found!('User') + elsif user.ldap_blocked? + forbidden!('LDAP blocked users cannot be unblocked by the API') + else + user.activate end end end diff --git a/lib/api/variables.rb b/lib/api/variables.rb new file mode 100644 index 00000000000..d9a055f6c92 --- /dev/null +++ b/lib/api/variables.rb @@ -0,0 +1,95 @@ +module API + # Projects variables API + class Variables < Grape::API + before { authenticate! } + before { authorize_admin_project } + + resource :projects do + # Get project variables + # + # Parameters: + # id (required) - The ID of a project + # page (optional) - The page number for pagination + # per_page (optional) - The value of items per page to show + # Example Request: + # GET /projects/:id/variables + get ':id/variables' do + variables = user_project.variables + present paginate(variables), with: Entities::Variable + end + + # Get specific variable of a project + # + # Parameters: + # id (required) - The ID of a project + # key (required) - The `key` of variable + # Example Request: + # GET /projects/:id/variables/:key + get ':id/variables/:key' do + key = params[:key] + variable = user_project.variables.find_by(key: key.to_s) + + return not_found!('Variable') unless variable + + present variable, with: Entities::Variable + end + + # Create a new variable in project + # + # Parameters: + # id (required) - The ID of a project + # key (required) - The key of variable + # value (required) - The value of variable + # Example Request: + # POST /projects/:id/variables + post ':id/variables' do + required_attributes! [:key, :value] + + variable = user_project.variables.create(key: params[:key], value: params[:value]) + + if variable.valid? + present variable, with: Entities::Variable + else + render_validation_error!(variable) + end + end + + # Update existing variable of a project + # + # Parameters: + # id (required) - The ID of a project + # key (optional) - The `key` of variable + # value (optional) - New value for `value` field of variable + # Example Request: + # PUT /projects/:id/variables/:key + put ':id/variables/:key' do + variable = user_project.variables.find_by(key: params[:key].to_s) + + return not_found!('Variable') unless variable + + attrs = attributes_for_keys [:value] + if variable.update(attrs) + present variable, with: Entities::Variable + else + render_validation_error!(variable) + end + end + + # Delete existing variable of a project + # + # Parameters: + # id (required) - The ID of a project + # key (required) - The ID of a variable + # Example Request: + # DELETE /projects/:id/variables/:key + delete ':id/variables/:key' do + variable = user_project.variables.find_by(key: params[:key].to_s) + + return not_found!('Variable') unless variable + variable.destroy + + present variable, with: Entities::Variable + end + end + end +end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 15faa6edd84..fb87637b94f 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -78,11 +78,13 @@ module Ci # Parameters: # id (required) - The ID of a build # token (required) - The build authorization token - # file (required) - The uploaded file + # file (required) - Artifacts file # Parameters (accelerated by GitLab Workhorse): # file.path - path to locally stored body (generated by Workhorse) # file.name - real filename as send in Content-Disposition # file.type - real content type as send in Content-Type + # metadata.path - path to locally stored body (generated by Workhorse) + # metadata.name - filename (generated by Workhorse) # Headers: # BUILD-TOKEN (required) - The build authorization token, the same as token # Body: @@ -96,13 +98,20 @@ module Ci build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) - forbidden!('build is not running') unless build.running? + forbidden!('Build is not running!') unless build.running? - file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path) - file_to_large! unless file.size < max_artifacts_size + artifacts_upload_path = ArtifactUploader.artifacts_upload_path + artifacts = uploaded_file(:file, artifacts_upload_path) + metadata = uploaded_file(:metadata, artifacts_upload_path) - if build.update_attributes(artifacts_file: file) - present build, with: Entities::Build + bad_request!('Missing artifacts file!') unless artifacts + file_to_large! unless artifacts.size < max_artifacts_size + + build.artifacts_file = artifacts + build.artifacts_metadata = metadata + + if build.save + present(build, with: Entities::Build) else render_validation_error!(build) end @@ -148,6 +157,7 @@ module Ci not_found! unless build authenticate_build_token!(build) build.remove_artifacts_file! + build.remove_artifacts_metadata! end end end diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb new file mode 100644 index 00000000000..1344f5d120b --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -0,0 +1,109 @@ +require 'zlib' +require 'json' + +module Gitlab + module Ci + module Build + module Artifacts + class Metadata + class ParserError < StandardError; end + + VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/ + INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)} + + attr_reader :file, :path, :full_version + + def initialize(file, path) + @file, @path = file, path + @full_version = read_version + end + + def version + @full_version.match(VERSION_PATTERN)[1] + end + + def errors + gzip do |gz| + read_string(gz) # version + errors = read_string(gz) + raise ParserError, 'Errors field not found!' unless errors + + begin + JSON.parse(errors) + rescue JSON::ParserError + raise ParserError, 'Invalid errors field!' + end + end + end + + def find_entries! + gzip do |gz| + 2.times { read_string(gz) } # version and errors fields + match_entries(gz) + end + end + + def to_entry + entries = find_entries! + Entry.new(@path, entries) + end + + private + + def match_entries(gz) + entries = {} + match_pattern = %r{^#{Regexp.escape(@path)}[^/]*/?$} + + until gz.eof? do + begin + path = read_string(gz).force_encoding('UTF-8') + meta = read_string(gz).force_encoding('UTF-8') + + next unless path.valid_encoding? && meta.valid_encoding? + next unless path =~ match_pattern + next if path =~ INVALID_PATH_PATTERN + + entries[path] = JSON.parse(meta, symbolize_names: true) + rescue JSON::ParserError, Encoding::CompatibilityError + next + end + end + + entries + end + + def read_version + gzip do |gz| + version_string = read_string(gz) + + unless version_string + raise ParserError, 'Artifacts metadata file empty!' + end + + unless version_string =~ VERSION_PATTERN + raise ParserError, 'Invalid version!' + end + + version_string.chomp + end + end + + def read_uint32(gz) + binary = gz.read(4) + binary.unpack('L>')[0] if binary + end + + def read_string(gz) + string_size = read_uint32(gz) + return nil unless string_size + gz.read(string_size) + end + + def gzip(&block) + Zlib::GzipReader.open(@file, &block) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb new file mode 100644 index 00000000000..25b71fc3275 --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -0,0 +1,119 @@ +module Gitlab + module Ci::Build::Artifacts + class Metadata + ## + # Class that represents an entry (path and metadata) to a file or + # directory in GitLab CI Build Artifacts binary file / archive + # + # This is IO-operations safe class, that does similar job to + # Ruby's Pathname but without the risk of accessing filesystem. + # + # This class is working only with UTF-8 encoded paths. + # + class Entry + attr_reader :path, :entries + attr_accessor :name + + def initialize(path, entries) + @path = path.dup.force_encoding('UTF-8') + @entries = entries + + if path.include?("\0") + raise ArgumentError, 'Path contains zero byte character!' + end + + unless path.valid_encoding? + raise ArgumentError, 'Path contains non-UTF-8 byte sequence!' + end + end + + def directory? + blank_node? || @path.end_with?('/') + end + + def file? + !directory? + end + + def has_parent? + nodes > 0 + end + + def parent + return nil unless has_parent? + self.class.new(@path.chomp(basename), @entries) + end + + def basename + (directory? && !blank_node?) ? name + '/' : name + end + + def name + @name || @path.split('/').last.to_s + end + + def children + return [] unless directory? + return @children if @children + + child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$} + @children = select_entries { |path| path =~ child_pattern } + end + + def directories(opts = {}) + return [] unless directory? + dirs = children.select(&:directory?) + return dirs unless has_parent? && opts[:parent] + + dotted_parent = parent + dotted_parent.name = '..' + dirs.prepend(dotted_parent) + end + + def files + return [] unless directory? + children.select(&:file?) + end + + def metadata + @entries[@path] || {} + end + + def nodes + @path.count('/') + (file? ? 1 : 0) + end + + def blank_node? + @path.empty? # "" is considered to be './' + end + + def exists? + blank_node? || @entries.include?(@path) + end + + def empty? + children.empty? + end + + def to_s + @path + end + + def ==(other) + @path == other.path && @entries == other.entries + end + + def inspect + "#{self.class.name}: #{@path}" + end + + private + + def select_entries + selected = @entries.select { |path, _metadata| yield path } + selected.map { |path, _metadata| self.class.new(path, @entries) } + end + end + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index b2bdbc10d7f..da4435c7308 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -37,15 +37,15 @@ module Gitlab # Block user in GitLab if he/she was blocked in AD if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) - user.block + user.ldap_block false else - user.activate if user.blocked? && !ldap_config.block_auto_created_users + user.activate if user.ldap_blocked? true end else # Block the user if they no longer exist in LDAP/AD - user.block + user.ldap_block false end rescue diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb new file mode 100644 index 00000000000..c131d22a30a --- /dev/null +++ b/spec/controllers/admin/identities_controller_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Admin::IdentitiesController do + let(:admin) { create(:admin) } + before { sign_in(admin) } + + describe 'UPDATE identity' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } + + it 'repairs ldap blocks' do + expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute) + + put :update, user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' } + end + end + + describe 'DELETE identity' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } + + it 'repairs ldap blocks' do + expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute) + + delete :destroy, user_id: user.username, id: user.ldap_identity.id + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 8b7af4d3a0a..5b1f65d7aff 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -34,17 +34,34 @@ describe Admin::UsersController do end describe 'PUT unblock/:id' do - let(:user) { create(:user) } - - before do - user.block + context 'ldap blocked users' do + let(:user) { create(:omniauth_user, provider: 'ldapmain') } + + before do + user.ldap_block + end + + it 'will not unblock user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_truthy + expect(flash[:alert]).to eq 'This user cannot be unlocked manually from GitLab' + end end - it 'unblocks user' do - put :unblock, id: user.username - user.reload - expect(user.blocked?).to be_falsey - expect(flash[:notice]).to eq 'Successfully unblocked' + context 'manually blocked users' do + let(:user) { create(:user) } + + before do + user.block + end + + it 'unblocks user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_falsey + expect(flash[:notice]).to eq 'Successfully unblocked' + end end end diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb new file mode 100644 index 00000000000..8f62d64411b --- /dev/null +++ b/spec/factories/ci/variables.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: ci_variables +# +# id :integer not null, primary key +# project_id :integer not null +# key :string(255) +# value :text +# encrypted_value :text +# encrypted_value_salt :string(255) +# encrypted_value_iv :string(255) +# gl_project_id :integer +# + +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :ci_variable, class: Ci::Variable do + sequence(:key) { |n| "VARIABLE_#{n}" } + value 'VARIABLE_VALUE' + end +end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 240e56839df..d37bd103714 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -80,7 +80,11 @@ describe "Builds" do visit namespace_project_build_path(@project.namespace, @project, @build) end - it { expect(page).to have_content 'Download artifacts' } + it 'has button to download artifacts' do + page.within('.artifacts') do + expect(page).to have_content 'Download' + end + end end end @@ -111,7 +115,7 @@ describe "Builds" do before do @build.update_attributes(artifacts_file: artifacts_file) visit namespace_project_build_path(@project.namespace, @project, @build) - click_link 'Download artifacts' + page.within('.artifacts') { click_link 'Download' } end it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } diff --git a/spec/fixtures/ci_build_artifacts.zip b/spec/fixtures/ci_build_artifacts.zip Binary files differnew file mode 100644 index 00000000000..dae976d918e --- /dev/null +++ b/spec/fixtures/ci_build_artifacts.zip diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz Binary files differnew file mode 100644 index 00000000000..fe9d4c8c661 --- /dev/null +++ b/spec/fixtures/ci_build_artifacts_metadata.gz diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb new file mode 100644 index 00000000000..41257103ead --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do + let(:entries) do + { 'path/' => {}, + 'path/dir_1/' => {}, + 'path/dir_1/file_1' => {}, + 'path/dir_1/file_b' => {}, + 'path/dir_1/subdir/' => {}, + 'path/dir_1/subdir/subfile' => {}, + 'path/second_dir' => {}, + 'path/second_dir/dir_3/file_2' => {}, + 'path/second_dir/dir_3/file_3'=> {}, + 'another_directory/'=> {}, + 'another_file' => {}, + '/file/with/absolute_path' => {} } + end + + def path(example) + entry(example.metadata[:path]) + end + + def entry(path) + described_class.new(path, entries) + end + + describe '/file/with/absolute_path', path: '/file/with/absolute_path' do + subject { |example| path(example) } + + it { is_expected.to be_file } + it { is_expected.to have_parent } + + describe '#basename' do + subject { |example| path(example).basename } + it { is_expected.to eq 'absolute_path' } + end + end + + describe 'path/dir_1/', path: 'path/dir_1/' do + subject { |example| path(example) } + it { is_expected.to have_parent } + it { is_expected.to be_directory } + + describe '#basename' do + subject { |example| path(example).basename } + it { is_expected.to eq 'dir_1/' } + end + + describe '#name' do + subject { |example| path(example).name } + it { is_expected.to eq 'dir_1' } + end + + describe '#parent' do + subject { |example| path(example).parent } + it { is_expected.to eq entry('path/') } + end + + describe '#children' do + subject { |example| path(example).children } + + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/file_1'), + entry('path/dir_1/file_b'), + entry('path/dir_1/subdir/') + end + end + + describe '#files' do + subject { |example| path(example).files } + + it { is_expected.to all(be_file) } + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/file_1'), + entry('path/dir_1/file_b') + end + end + + describe '#directories' do + context 'without options' do + subject { |example| path(example).directories } + + it { is_expected.to all(be_directory) } + it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to contain_exactly entry('path/dir_1/subdir/') } + end + + context 'with option parent: true' do + subject { |example| path(example).directories(parent: true) } + + it { is_expected.to all(be_directory) } + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/subdir/'), + entry('path/') + end + end + + describe '#nodes' do + subject { |example| path(example).nodes } + it { is_expected.to eq 2 } + end + + describe '#exists?' do + subject { |example| path(example).exists? } + it { is_expected.to be true } + end + + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be false } + end + end + end + + describe 'empty path', path: '' do + subject { |example| path(example) } + it { is_expected.to_not have_parent } + + describe '#children' do + subject { |example| path(example).children } + it { expect(subject.count).to eq 3 } + end + + end + + describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do + describe '#nodes' do + subject { |example| path(example).nodes } + it { is_expected.to eq 4 } + end + end + + describe 'non-existent/', path: 'non-existent/' do + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be true } + end + + describe '#exists?' do + subject { |example| path(example).exists? } + it { is_expected.to be false } + end + end + + describe 'another_directory/', path: 'another_directory/' do + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be true } + end + end + + describe '#metadata' do + let(:entries) do + { 'path/' => { name: '/path/' }, + 'path/file1' => { name: '/path/file1' }, + 'path/file2' => { name: '/path/file2' } } + end + + subject do + described_class.new('path/file1', entries).metadata[:name] + end + + it { is_expected.to eq '/path/file1' } + end +end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb new file mode 100644 index 00000000000..828eedfa7b0 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Metadata do + def metadata(path = '') + described_class.new(metadata_file_path, path) + end + + let(:metadata_file_path) do + Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + end + + context 'metadata file exists' do + describe '#find_entries! empty string' do + subject { metadata('').find_entries! } + + it 'matches correct paths' do + expect(subject.keys).to contain_exactly 'ci_artifacts.txt', + 'other_artifacts_0.1.2/', + 'rails_sample.jpg', + 'tests_encoding/' + end + + it 'matches metadata for every path' do + expect(subject.keys.count).to eq 4 + end + + it 'return Hashes for each metadata' do + expect(subject.values).to all(be_kind_of(Hash)) + end + end + + describe '#find_entries! other_artifacts_0.1.2/' do + subject { metadata('other_artifacts_0.1.2/').find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/' + end + end + + describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do + subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + end + + describe '#to_entry' do + subject { metadata('').to_entry } + it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) } + end + + describe '#full_version' do + subject { metadata('').full_version } + it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' } + end + + describe '#version' do + subject { metadata('').version } + it { is_expected.to eq '0.0.1' } + end + + describe '#errors' do + subject { metadata('').errors } + it { is_expected.to eq({}) } + end + end + + context 'metadata file does not exist' do + let(:metadata_file_path) { '' } + + describe '#find_entries!' do + it 'raises error' do + expect { metadata.find_entries! }.to raise_error(Errno::ENOENT) + end + end + end +end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index a628d0c0157..32a19bf344b 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -13,64 +13,58 @@ describe Gitlab::LDAP::Access, lib: true do end it { is_expected.to be_falsey } - + it 'should block user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).to be_ldap_blocked end end context 'when the user is found' do before do - allow(Gitlab::LDAP::Person). - to receive(:find_by_dn).and_return(:ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) end context 'and the user is disabled via active directory' do before do - allow(Gitlab::LDAP::Person). - to receive(:disabled_via_active_directory?).and_return(true) + allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true) end it { is_expected.to be_falsey } - it "should block user in GitLab" do + it 'should block user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).to be_ldap_blocked end end context 'and has no disabled flag in active diretory' do before do - user.block - - allow(Gitlab::LDAP::Person). - to receive(:disabled_via_active_directory?).and_return(false) + allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) end it { is_expected.to be_truthy } context 'when auto-created users are blocked' do - before do - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(true) + user.block end - it "does not unblock user in GitLab" do + it 'does not unblock user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic end end - context "when auto-created users are not blocked" do - + context 'when auto-created users are not blocked' do before do - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(false) + user.ldap_block end - it "should unblock user in GitLab" do + it 'should unblock user in GitLab' do access.allowed? expect(user).not_to be_blocked end @@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do context 'without ActiveDirectory enabled' do before do allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:active_directory).and_return(false) + allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false) end it { is_expected.to be_truthy } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 1c22e3cb7c4..0e13456723d 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1,28 +1,3 @@ -# == Schema Information -# -# Table name: builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# commit_id :integer -# coverage :float -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# - require 'spec_helper' describe Ci::Build, models: true do @@ -368,21 +343,75 @@ describe Ci::Build, models: true do end end - describe :download_url do - subject { build.download_url } + describe :artifacts_download_url do + subject { build.artifacts_download_url } it "should be nil if artifact doesn't exist" do build.update_attributes(artifacts_file: nil) is_expected.to be_nil end - it 'should be nil if artifact exist' do + it 'should not be nil if artifact exist' do gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') build.update_attributes(artifacts_file: gif) is_expected.to_not be_nil end end + describe :artifacts_browse_url do + subject { build.artifacts_browse_url } + + it "should be nil if artifacts browser is unsupported" do + allow(build).to receive(:artifacts_browser_supported?).and_return(false) + is_expected.to be_nil + end + + it 'should not be nil if artifacts browser is supported' do + allow(build).to receive(:artifacts_browser_supported?).and_return(true) + is_expected.to_not be_nil + end + end + + describe :artifacts? do + subject { build.artifacts? } + + context 'artifacts archive does not exist' do + before { build.update_attributes(artifacts_file: nil) } + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + before do + gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + build.update_attributes(artifacts_file: gif) + end + + it { is_expected.to be_truthy } + end + end + + + describe :artifacts_browser_supported? do + subject { build.artifacts_browser_supported? } + context 'artifacts metadata does not exist' do + it { is_expected.to be_falsy } + end + + context 'artifacts archive is a zip file and metadata exists' do + before do + fixture_dir = Rails.root + 'spec/fixtures/' + archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip', + 'application/zip') + metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz', + 'application/x-gzip') + build.update_attributes(artifacts_file: archive) + build.update_attributes(artifacts_metadata: metadata) + end + + it { is_expected.to be_truthy } + end + end + describe :repo_url do let(:build) { FactoryGirl.create :ci_build } let(:project) { build.project } diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb new file mode 100644 index 00000000000..5afe042e154 --- /dev/null +++ b/spec/models/identity_spec.rb @@ -0,0 +1,38 @@ +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# extern_uid :string(255) +# provider :string(255) +# user_id :integer +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +RSpec.describe Identity, models: true do + + describe 'relations' do + it { is_expected.to belong_to(:user) } + end + + describe 'fields' do + it { is_expected.to respond_to(:provider) } + it { is_expected.to respond_to(:extern_uid) } + end + + describe '#is_ldap?' do + let(:ldap_identity) { create(:identity, provider: 'ldapmain') } + let(:other_identity) { create(:identity, provider: 'twitter') } + + it 'returns true if it is a ldap identity' do + expect(ldap_identity.ldap?).to be_truthy + end + + it 'returns false if it is not a ldap identity' do + expect(other_identity.ldap?).to be_falsey + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3cd63b2b0e8..0bef68e2885 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -569,27 +569,39 @@ describe User, models: true do end end - describe :ldap_user? do - it "is true if provider name starts with ldap" do - user = create(:omniauth_user, provider: 'ldapmain') - expect( user.ldap_user? ).to be_truthy - end + context 'ldap synchronized user' do + describe :ldap_user? do + it 'is true if provider name starts with ldap' do + user = create(:omniauth_user, provider: 'ldapmain') + expect(user.ldap_user?).to be_truthy + end - it "is false for other providers" do - user = create(:omniauth_user, provider: 'other-provider') - expect( user.ldap_user? ).to be_falsey + it 'is false for other providers' do + user = create(:omniauth_user, provider: 'other-provider') + expect(user.ldap_user?).to be_falsey + end + + it 'is false if no extern_uid is provided' do + user = create(:omniauth_user, extern_uid: nil) + expect(user.ldap_user?).to be_falsey + end end - it "is false if no extern_uid is provided" do - user = create(:omniauth_user, extern_uid: nil) - expect( user.ldap_user? ).to be_falsey + describe :ldap_identity do + it 'returns ldap identity' do + user = create :omniauth_user + expect(user.ldap_identity.provider).not_to be_empty + end end - end - describe :ldap_identity do - it "returns ldap identity" do - user = create :omniauth_user - expect(user.ldap_identity.provider).not_to be_empty + describe '#ldap_block' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') } + + it 'blocks user flaging the action caming from ldap' do + user.ldap_block + expect(user.blocked?).to be_truthy + expect(user.ldap_blocked?).to be_truthy + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4f278551d07..b82c5c7685f 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -8,6 +8,8 @@ describe API::API, api: true do let(:key) { create(:key, user: user) } let(:email) { create(:email, user: user) } let(:omniauth_user) { create(:omniauth_user) } + let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } describe "GET /users" do context "when unauthenticated" do @@ -783,6 +785,12 @@ describe API::API, api: true do expect(user.reload.state).to eq('blocked') end + it 'should not re-block ldap blocked users' do + put api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response.status).to eq(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + it 'should not be available for non admin users' do put api("/users/#{user.id}/block", user) expect(response.status).to eq(403) @@ -797,7 +805,9 @@ describe API::API, api: true do end describe 'PUT /user/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } before { admin } + it 'should unblock existing user' do put api("/users/#{user.id}/unblock", admin) expect(response.status).to eq(200) @@ -805,12 +815,15 @@ describe API::API, api: true do end it 'should unblock a blocked user' do - put api("/users/#{user.id}/block", admin) - expect(response.status).to eq(200) - expect(user.reload.state).to eq('blocked') - put api("/users/#{user.id}/unblock", admin) + put api("/users/#{blocked_user.id}/unblock", admin) expect(response.status).to eq(200) - expect(user.reload.state).to eq('active') + expect(blocked_user.reload.state).to eq('active') + end + + it 'should not unblock ldap blocked users' do + put api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response.status).to eq(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'should not be available for non admin users' do diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb new file mode 100644 index 00000000000..9744729ba0c --- /dev/null +++ b/spec/requests/api/variables_spec.rb @@ -0,0 +1,182 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } + let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) } + let!(:variable) { create(:ci_variable, project: project) } + + describe 'GET /projects/:id/variables' do + context 'authorized user with proper permissions' do + it 'should return project variables' do + get api("/projects/#{project.id}/variables", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Array) + end + end + + context 'authorized user with invalid permissions' do + it 'should not return project variables' do + get api("/projects/#{project.id}/variables", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not return project variables' do + get api("/projects/#{project.id}/variables") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}", user) + + expect(response.status).to eq(200) + expect(json_response['value']).to eq(variable.value) + end + + it 'should respond with 404 Not Found if requesting non-existing variable' do + get api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/variables' do + context 'authorized user with proper permissions' do + it 'should create variable' do + expect do + post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' + end.to change{project.variables.count}.by(1) + + expect(response.status).to eq(201) + expect(json_response['key']).to eq('TEST_VARIABLE_2') + expect(json_response['value']).to eq('VALUE_2') + end + + it 'should not allow to duplicate variable key' do + expect do + post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2' + end.to change{project.variables.count}.by(0) + + expect(response.status).to eq(400) + end + end + + context 'authorized user with invalid permissions' do + it 'should not create variable' do + post api("/projects/#{project.id}/variables", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not create variable' do + post api("/projects/#{project.id}/variables") + + expect(response.status).to eq(401) + end + end + end + + describe 'PUT /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should update variable data' do + initial_variable = project.variables.first + value_before = initial_variable.value + + put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP' + + updated_variable = project.variables.first + + expect(response.status).to eq(200) + expect(value_before).to eq(variable.value) + expect(updated_variable.value).to eq('VALUE_1_UP') + end + + it 'should responde with 404 Not Found if requesting non-existing variable' do + put api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not update variable' do + put api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not update variable' do + put api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should delete variable' do + expect do + delete api("/projects/#{project.id}/variables/#{variable.key}", user) + end.to change{project.variables.count}.by(-1) + expect(response.status).to eq(200) + end + + it 'should responde with 404 Not Found if requesting non-existing variable' do + delete api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not delete variable' do + delete api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not delete variable' do + delete api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c27e87c4acc..648ea0d5f50 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -210,6 +210,52 @@ describe Ci::API::API do end end + context 'should post artifacts file and metadata file' do + let!(:artifacts) { file_upload } + let!(:metadata) { file_upload2 } + + let(:stored_artifacts_file) { build.reload.artifacts_file.file } + let(:stored_metadata_file) { build.reload.artifacts_metadata.file } + + before do + build.run! + post(post_url, post_data, headers_with_token) + end + + context 'post data accelerated by workhorse is correct' do + let(:post_data) do + { 'file.path' => artifacts.path, + 'file.name' => artifacts.original_filename, + 'metadata.path' => metadata.path, + 'metadata.name' => metadata.original_filename } + end + + it 'responds with valid status' do + expect(response.status).to eq(201) + end + + it 'stores artifacts and artifacts metadata' do + expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) + expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) + end + end + + context 'no artifacts file in post data' do + let(:post_data) do + { 'metadata' => metadata } + end + + it 'is expected to respond with bad request' do + expect(response.status).to eq(400) + end + + it 'does not store metadata' do + expect(stored_metadata_file).to be_nil + end + end + end + + context "should fail to post too large artifact" do before do build.run! diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb new file mode 100644 index 00000000000..ce7d1455975 --- /dev/null +++ b/spec/services/repair_ldap_blocked_user_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe RepairLdapBlockedUserService, services: true do + let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + let(:identity) { user.ldap_identity } + subject(:service) { RepairLdapBlockedUserService.new(user) } + + describe '#execute' do + it 'change to normal block after destroying last ldap identity' do + identity.destroy + service.execute + + expect(user.reload).not_to be_ldap_blocked + end + + it 'change to normal block after changing last ldap identity to another provider' do + identity.update_attribute(:provider, 'twitter') + service.execute + + expect(user.reload).not_to be_ldap_blocked + end + end +end |