summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRémy Coutable <remy@rymai.me>2016-01-14 14:23:42 +0100
committerRémy Coutable <remy@rymai.me>2016-01-14 14:23:42 +0100
commit1b08cd811abeed18c3601e1b997b0566a243662c (patch)
tree1b8f624b29bd2ae77d9344e31a3afe99f7b14536
parentc31d777c8f24029d7e11dd1e78eddf0c0b6e6f01 (diff)
parente47f0e563d127f593f16c0a521c836e21bceb77f (diff)
downloadgitlab-ce-1b08cd811abeed18c3601e1b997b0566a243662c.tar.gz
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into add-pagination-headers-to-api
-rw-r--r--CHANGELOG6
-rw-r--r--app/assets/javascripts/activities.js.coffee4
-rw-r--r--app/assets/javascripts/merge_request.js.coffee13
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee2
-rw-r--r--app/assets/javascripts/notes.js.coffee4
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss29
-rw-r--r--app/assets/stylesheets/framework/buttons.scss33
-rw-r--r--app/assets/stylesheets/framework/common.scss69
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss7
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss24
-rw-r--r--app/assets/stylesheets/framework/mixins.scss35
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss39
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss3
-rw-r--r--app/assets/stylesheets/framework/tables.scss7
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss41
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/pages/branches.scss3
-rw-r--r--app/assets/stylesheets/pages/commit.scss6
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss5
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/issues.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss47
-rw-r--r--app/assets/stylesheets/pages/tags.scss3
-rw-r--r--app/assets/stylesheets/pages/tree.scss18
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/projects/artifacts_controller.rb56
-rw-r--r--app/controllers/projects/builds_controller.rb27
-rw-r--r--app/helpers/events_helper.rb8
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/ci/build.rb41
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/identity.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/user.rb14
-rw-r--r--app/services/repair_ldap_blocked_user_service.rb17
-rw-r--r--app/views/admin/builds/_build.html.haml4
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml5
-rw-r--r--app/views/admin/users/_head.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml190
-rw-r--r--app/views/dashboard/_activities.html.haml4
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/milestones/show.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml48
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/show.html.haml31
-rw-r--r--app/views/help/ui.html.haml21
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml2
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml7
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml11
-rw-r--r--app/views/projects/artifacts/browse.html.haml24
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml14
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml4
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_head.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/graphs/_head.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/runners/index.html.haml3
-rw-r--r--app/views/projects/show.html.haml26
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml4
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml4
-rw-r--r--app/views/projects/wikis/_nav.html.haml2
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_milestones_filter.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml2
-rw-r--r--app/views/users/show.html.haml127
-rw-r--r--config/routes.rb7
-rw-r--r--db/fixtures/development/14_builds.rb79
-rw-r--r--db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb5
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/build_variables.md128
-rw-r--r--doc/api/users.md6
-rw-r--r--features/project/builds.feature58
-rw-r--r--features/steps/project/builds.rb89
-rw-r--r--features/steps/shared/active_tab.rb4
-rw-r--r--features/steps/shared/builds.rb28
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/users.rb14
-rw-r--r--lib/api/variables.rb95
-rw-r--r--lib/ci/api/builds.rb22
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb109
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb119
-rw-r--r--lib/gitlab/ldap/access.rb6
-rw-r--r--spec/controllers/admin/identities_controller_spec.rb26
-rw-r--r--spec/controllers/admin/users_controller_spec.rb35
-rw-r--r--spec/factories/ci/variables.rb22
-rw-r--r--spec/features/builds_spec.rb8
-rw-r--r--spec/fixtures/ci_build_artifacts.zipbin0 -> 106365 bytes
-rw-r--r--spec/fixtures/ci_build_artifacts_metadata.gzbin0 -> 415 bytes
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb84
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb35
-rw-r--r--spec/models/build_spec.rb85
-rw-r--r--spec/models/identity_spec.rb38
-rw-r--r--spec/models/user_spec.rb44
-rw-r--r--spec/requests/api/users_spec.rb23
-rw-r--r--spec/requests/api/variables_spec.rb182
-rw-r--r--spec/requests/ci/api/builds_spec.rb46
-rw-r--r--spec/services/repair_ldap_blocked_user_service_spec.rb23
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'
+ &nbsp;
+ .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'
- &nbsp;
- = 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
&nbsp;
- 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
&nbsp;
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
+ &nbsp;
+ = 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
- &nbsp;
- = 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
new file mode 100644
index 00000000000..dae976d918e
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts.zip
Binary files differ
diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz
new file mode 100644
index 00000000000..fe9d4c8c661
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts_metadata.gz
Binary files differ
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