Commit 1b08cd81 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into add-pagination-headers-to-api

parents c31d777c e47f0e56
Please view this file on the master branch, on stable branches it's out of date. 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) v 8.4.0 (unreleased)
- Add pagination headers to already paginated API resources - 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) - 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) - Improved performance of finding issues for an entire group (Yorick Peterse)
- Added custom application performance measuring system powered by InfluxDB (Yorick Peterse) - Added custom application performance measuring system powered by InfluxDB (Yorick Peterse)
...@@ -45,10 +49,12 @@ v 8.4.0 (unreleased) ...@@ -45,10 +49,12 @@ v 8.4.0 (unreleased)
- Allow subsequent validations in CI Linter - Allow subsequent validations in CI Linter
- Show referenced MRs & Issues only when the current viewer can access them - 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) - 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 - Allow broadcast messages to be edited
v 8.3.4 v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug) - Use gitlab-workhorse 0.5.4 (fixes API routing bug)
- Add build artifacts browser
v 8.3.3 v 8.3.3
- Preserve CE behavior with JIRA integration by only calling API if URL is set - Preserve CE behavior with JIRA integration by only calling API if URL is set
......
class @Activities class @Activities
constructor: -> constructor: ->
Pager.init 20, true Pager.init 20, true
$(".event-filter .btn").bind "click", (event) => $(".event-filter a").bind "click", (event) =>
event.preventDefault() event.preventDefault()
@toggleFilter($(event.currentTarget)) @toggleFilter($(event.currentTarget))
@reloadActivities() @reloadActivities()
...@@ -12,7 +12,7 @@ class @Activities ...@@ -12,7 +12,7 @@ class @Activities
toggleFilter: (sender) -> toggleFilter: (sender) ->
sender.toggleClass "active" sender.closest('li').toggleClass "active"
event_filters = $.cookie("event_filter") event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0] filter = sender.attr("id").split("_")[0]
if event_filters if event_filters
......
...@@ -48,15 +48,16 @@ class @MergeRequest ...@@ -48,15 +48,16 @@ class @MergeRequest
_this = @ _this = @
$('a.btn-close, a.btn-reopen').on 'click', (e) -> $('a.btn-close, a.btn-reopen').on 'click', (e) ->
$this = $(this) $this = $(this)
if $this.data('submitted') shouldSubmit = $this.hasClass('btn-comment')
if shouldSubmit && $this.data('submitted')
return return
if shouldSubmit
if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
e.preventDefault() e.preventDefault()
e.stopImmediatePropagation() e.stopImmediatePropagation()
shouldSubmit = $this.hasClass('btn-comment')
console.log("shouldSubmit")
if shouldSubmit
_this.submitNoteForm($this.closest('form'),$this) _this.submitNoteForm($this.closest('form'),$this)
submitNoteForm: (form, $button) => submitNoteForm: (form, $button) =>
noteText = form.find("textarea.js-note-text").val() noteText = form.find("textarea.js-note-text").val()
if noteText.trim().length > 0 if noteText.trim().length > 0
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
# #
# ### Example Markup # ### Example Markup
# #
# <ul class="nav nav-tabs merge-request-tabs"> # <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active"> # <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> # <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion # Discussion
......
...@@ -521,9 +521,13 @@ class @Notes ...@@ -521,9 +521,13 @@ class @Notes
if textarea.val().trim().length > 0 if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen') form.find('.js-note-target-reopen').text('Comment & reopen')
form.find('.js-note-target-close').text('Comment & close') 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 else
form.find('.js-note-target-reopen').text('Reopen') form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close') 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: -> initTaskList: ->
@enableTaskList() @enableTaskList()
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
@import "framework/lists.scss"; @import "framework/lists.scss";
@import "framework/markdown_area.scss"; @import "framework/markdown_area.scss";
@import "framework/mobile.scss"; @import "framework/mobile.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss"; @import "framework/pagination.scss";
@import "framework/panels.scss"; @import "framework/panels.scss";
@import "framework/selects.scss"; @import "framework/selects.scss";
......
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
line-height: 36px; line-height: 36px;
} }
.content-block,
.gray-content-block { .gray-content-block {
margin: -$gl-padding; margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $background-color; background-color: $background-color;
padding: $gl-padding; padding: $gl-padding;
margin-bottom: 0px; margin-bottom: 0px;
...@@ -86,10 +86,7 @@ ...@@ -86,10 +86,7 @@
.cover-block { .cover-block {
text-align: center; text-align: center;
background: $background-color; background: $background-color;
margin: -$gl-padding; padding-top: 44px;
margin-bottom: 0;
padding: 44px $gl-padding;
border-bottom: 1px solid $border-color;
position: relative; position: relative;
.avatar-holder { .avatar-holder {
...@@ -136,3 +133,23 @@ ...@@ -136,3 +133,23 @@
.block-connector { .block-connector {
margin-top: -1px; 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;
}
}
...@@ -131,6 +131,12 @@ ...@@ -131,6 +131,12 @@
&:last-child { &:last-child {
margin-right: 0px; margin-right: 0px;
} }
&.btn-xs {
margin-right: 3px;
}
}
&.disabled {
pointer-events: auto !important;
} }
} }
...@@ -153,33 +159,6 @@ ...@@ -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 { .btn-clipboard {
border: none; border: none;
} }
...@@ -374,75 +374,6 @@ table { ...@@ -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 { .dropzone .dz-preview .dz-progress {
border-color: $border-color !important; border-color: $border-color !important;
} }
......
...@@ -3,11 +3,8 @@ ...@@ -3,11 +3,8 @@
* *
*/ */
.file-holder { .file-holder {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border: none; border: none;
border-top: 1px solid #E7E9EE; border: 1px solid $border-color;
border-bottom: 1px solid #E7E9EE;
&.readme-holder { &.readme-holder {
border-bottom: 0; border-bottom: 0;
......
...@@ -8,10 +8,12 @@ ...@@ -8,10 +8,12 @@
.flash-notice { .flash-notice {
@extend .alert; @extend .alert;
@extend .alert-info; @extend .alert-info;
margin: 0;
} }
.flash-alert { .flash-alert {
@extend .alert; @extend .alert;
@extend .alert-danger; @extend .alert-danger;
margin: 0;
} }
} }
...@@ -28,6 +28,7 @@ header { ...@@ -28,6 +28,7 @@ header {
min-height: $header-height; min-height: $header-height;
background-color: #fff; background-color: #fff;
border: none; border: none;
border-bottom: 1px solid #EEE;
.container-fluid { .container-fluid {
width: 100% !important; width: 100% !important;
......
...@@ -5,8 +5,6 @@ html { ...@@ -5,8 +5,6 @@ html {
} }
body { body {
background-color: #F3F3F3 !important;
&.navless { &.navless {
background-color: white !important; background-color: white !important;
} }
......
...@@ -109,10 +109,8 @@ ul.content-list { ...@@ -109,10 +109,8 @@ ul.content-list {
padding: 0; padding: 0;
> li { > li {
padding: $gl-padding; padding: $gl-padding 0;
border-color: $table-border-color; border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray; color: $gl-gray;
.avatar { .avatar {
...@@ -133,6 +131,7 @@ ul.content-list { ...@@ -133,6 +131,7 @@ ul.content-list {
.panel > .content-list { .panel > .content-list {
li { li {
margin: 0; margin: 0;
padding: $gl-padding;
} }
} }
......
...@@ -65,13 +65,6 @@ ...@@ -65,13 +65,6 @@
position: relative; position: relative;
} }
.md-header {
ul {
float: left;
margin-bottom: 1px;
}
}
.referenced-users { .referenced-users {
color: #4c4e54; color: #4c4e54;
padding-top: 10px; padding-top: 10px;
...@@ -85,23 +78,6 @@ ...@@ -85,23 +78,6 @@
box-shadow: none; 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 { .markdown-area {
@include border-radius(0); @include border-radius(0);
background: #FFF; background: #FFF;
......
...@@ -118,38 +118,3 @@ ...@@ -118,38 +118,3 @@
font-size: 16px; font-size: 16px;
line-height: 24px; 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;
}
}
}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
padding-right: 5px; padding-right: 5px;
} }
.nav.nav-tabs > li > a { .nav-links > li > a {
padding: 10px; padding: 10px;
font-size: 12px; font-size: 12px;
margin-right: 3px; margin-right: 3px;
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
display: none; display: none;
} }
.center-top-menu, .left-top-menu { .nav-links, .nav-links {
li a { li a {
font-size: 14px; font-size: 14px;
padding: 19px 10px; padding: 19px 10px;
......
.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;
}
}
}
...@@ -21,11 +21,10 @@ ...@@ -21,11 +21,10 @@
.content-wrapper { .content-wrapper {
width: 100%; width: 100%;
padding: 20px;
.container-fluid { .container-fluid {
background: #FFF; background: #FFF;
padding: $gl-padding; padding: 0 $gl-padding;
&.container-blank { &.container-blank {
background: none; background: none;
......
.table-holder { .table-holder {
margin: -$gl-padding; margin: 0;
margin-top: 0;
margin-bottom: 0;
} }
table { table {
...@@ -32,6 +30,7 @@ table { ...@@ -32,6 +30,7 @@ table {
} }
th { th {
background-color: $background-color;
font-weight: normal; font-weight: normal;
font-size: 15px; font-size: 15px;
border-bottom: 1px solid $border-color !important; border-bottom: 1px solid $border-color !important;
......
...@@ -5,10 +5,8 @@ ...@@ -5,10 +5,8 @@
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding; padding: $gl-padding 0;
border-color: $table-border-color; border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray; color: $gl-gray;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
......
...@@ -99,47 +99,6 @@ ...@@ -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 * fix to keep tooltips position in top navigation bar
......
...@@ -177,7 +177,7 @@ body { ...@@ -177,7 +177,7 @@ body {
} }
.page-title { .page-title {
margin-top: 0px; margin-top: $gl-padding;
line-height: 1.3; line-height: 1.3;
font-size: 1.25em; font-size: 1.25em;
font-weight: 600; font-weight: 600;
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
position: absolute; position: absolute;
top: 0px; top: 0px;
right: 4px; right: 4px;
line-height: 40px; line-height: 56px;
} }
a.js-zen-leave { a.js-zen-leave {
......
.branch-name{
font-weight: 600;
}
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
display: block; display: block;
} }
.commit-row-title .commit-title {
font-weight: 600;
}
.commit-author, .commit-committer{ .commit-author, .commit-committer{
display: block; display: block;
color: #999; color: #999;
...@@ -35,6 +39,8 @@ ...@@ -35,6 +39,8 @@
} }
.commit-box { .commit-box {
border-top: 1px solid $border-color;
.commit-title { .commit-title {
margin: 0; margin: 0;
font-size: 23px; font-size: 23px;
......
.detail-page-header { .detail-page-header {
margin: -$gl-padding; padding: 11px 0;
padding: 7px $gl-padding;
margin-bottom: 0px;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
color: #5c5d5e; color: #5c5d5e;
font-size: 16px; font-size: 16px;
......
// Common // Common
.diff-file { .diff-file {
margin-left: -$gl-padding; border: 1px solid $border-color;
margin-right: -$gl-padding; border-top: none;
border: none;
border-bottom: 1px solid #E7E9EE;
.diff-header { .diff-header {
position: relative; position: relative;
......
...@@ -4,9 +4,7 @@ ...@@ -4,9 +4,7 @@
*/ */
.event-item { .event-item {
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px); padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px);
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border-bottom: 1px solid $table-border-color; border-bottom: 1px solid $table-border-color;
color: #7f8fa4; color: #7f8fa4;
......
...@@ -11,3 +11,8 @@ ...@@ -11,3 +11,8 @@
height: 42px; height: 42px;
} }
} }
.content-list .group-name {
font-weight: 600;
color: #4c4e54;
}
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
margin-top: 7px; margin-top: 7px;
} }
.center-top-menu { .nav-links {
text-align: left; text-align: left;
} }
} }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.issue-title { .issue-title {
margin-bottom: 5px; margin-bottom: 5px;
font-size: $list-font-size; font-size: $list-font-size;
font-weight: bold; font-weight: 600;
} }
.issue-info { .issue-info {
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
* *
*/ */
.mr-state-widget { .mr-state-widget {
background: #F7F8FA; background: $background-color;
color: $gl-gray; color: $gl-gray;
border: 1px solid #dce0e6; border: 1px solid $border-color;
@include border-radius(2px); @include border-radius(2px);
form { form {
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
.merge-request-title { .merge-request-title {
margin-bottom: 5px; margin-bottom: 5px;
font-size: $list-font-size; font-size: $list-font-size;
font-weight: bold; font-weight: 600;
} }
.merge-request-info { .merge-request-info {
......
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
} }
.project-home-panel { .project-home-panel {
padding-bottom: 40px;
border-bottom: 1px solid $border-color;
.cover-controls { .cover-controls {
.project-settings-dropdown { .project-settings-dropdown {
...@@ -226,7 +228,6 @@ ...@@ -226,7 +228,6 @@
} }
.projects-search-form { .projects-search-form {
.input-group .form-control { .input-group .form-control {
height: 42px; height: 42px;
} }
...@@ -351,28 +352,6 @@ ...@@ -351,28 +352,6 @@
color: #555; 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 { .project_member_row form {
margin: 0px; margin: 0px;
} }
...@@ -416,11 +395,8 @@ ul.nav.nav-projects-tabs { ...@@ -416,11 +395,8 @@ ul.nav.nav-projects-tabs {
.top-area { .top-area {
border-bottom: 1px solid #EEE; border-bottom: 1px solid #EEE;
margin: 0 -16px;
padding: 0 $gl-padding;
height: 42px;
ul.left-top-menu { ul.nav-links {
display: inline-block; display: inline-block;
width: 50%; width: 50%;
margin-bottom: 0px; margin-bottom: 0px;
...@@ -482,11 +458,11 @@ table.table.protected-branches-list tr.no-border { ...@@ -482,11 +458,11 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px; padding-top: 10px;
padding-bottom: 4px; padding-bottom: 4px;
ul.nav-pills { ul.nav {
display:inline-block; display:inline-block;
} }
.nav-pills li { .nav li {
display:inline; display:inline;
} }
...@@ -523,8 +499,7 @@ pre.light-well { ...@@ -523,8 +499,7 @@ pre.light-well {
} }
.projects-search-form { .projects-search-form {
margin: -$gl-padding; padding: $gl-padding 0;
padding: $gl-padding;
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 0px; margin-bottom: 0px;
...@@ -574,10 +549,8 @@ pre.light-well { ...@@ -574,10 +549,8 @@ pre.light-well {
@include basic-list; @include basic-list;
.project-row { .project-row {
padding: $gl-padding; padding: $gl-padding 0;
border-color: $table-border-color; border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
&.no-description { &.no-description {
.project { .project {
...@@ -631,8 +604,6 @@ pre.light-well { ...@@ -631,8 +604,6 @@ pre.light-well {
} }
.project-last-commit { .project-last-commit {
margin: 0 7px;
.ci-status { .ci-status {
margin-right: 16px; margin-right: 16px;
} }
...@@ -662,9 +633,7 @@ pre.light-well { ...@@ -662,9 +633,7 @@ pre.light-well {
} }
.project-show-readme .readme-holder { .project-show-readme .readme-holder {
margin-left: -$gl-padding; padding: $gl-padding 0;
margin-right: -$gl-padding;
padding: ($gl-padding + 7px);
border-top: 0; border-top: 0;
.edit-project-readme { .edit-project-readme {
......
.tag-name{
font-weight: 600;
}
.tree-holder { .tree-holder {
> .nav-block {
margin: 11px 0;
}
.file-finder { .file-finder {
width: 50%; width: 50%;
...@@ -13,7 +16,7 @@ ...@@ -13,7 +16,7 @@
tr { tr {
> td, > th { > td, > th {
line-height: 28px; line-height: 26px;
} }
&:hover { &:hover {
...@@ -86,12 +89,14 @@ ...@@ -86,12 +89,14 @@
.blob-commit-info { .blob-commit-info {
list-style: none; list-style: none;
padding: $gl-padding;
background: $background-color;
border: 1px solid $border-color;
border-bottom: none;
margin: 0; margin: 0;
padding: 0;
margin-bottom: 5px;
.commit { .commit {
padding: $gl-padding 0; padding: 0;
.commit-row-title { .commit-row-title {
.commit-row-message { .commit-row-message {
...@@ -115,3 +120,8 @@ ...@@ -115,3 +120,8 @@
font-weight: normal; font-weight: normal;
color: $md-link-color; color: $md-link-color;
} }
.tree-controls {
float: right;
margin-top: 11px;
}
...@@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController ...@@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def update def update
if @identity.update_attributes(identity_params) if @identity.update_attributes(identity_params)
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.' redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else else
render :edit render :edit
...@@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController ...@@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy def destroy
if @identity.destroy if @identity.destroy
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.' redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
else else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.' redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
......
...@@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController
end end
def unblock 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") redirect_back_or_admin_user(notice: "Successfully unblocked")
else else
redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked") redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
......
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
...@@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_manage_builds!, except: [:index, :show, :status] before_action :authorize_manage_builds!, except: [:index, :show, :status]
before_action :authorize_download_build_artifacts!, only: [:download]
layout "project" layout "project"
...@@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController
redirect_to build_path(build) redirect_to build_path(build)
end 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 def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end end
...@@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id]) @build ||= project.builds.unscoped.find_by!(id: params[:id])
end end
def artifacts_file
build.artifacts_file
end
def build_path(build) def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build) namespace_project_build_path(build.project.namespace, build.project, build)
end end
...@@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController
return page_404 return page_404
end end
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 end
...@@ -27,15 +27,17 @@ module EventsHelper ...@@ -27,15 +27,17 @@ module EventsHelper
key = key.to_s key = key.to_s
active = 'active' if @event_filter.active?(key) active = 'active' if @event_filter.active?(key)
link_opts = { link_opts = {
class: "event-filter-link btn btn-default #{active}", class: "event-filter-link",
id: "#{key}_event_filter", id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}", title: "Filter by #{tooltip.downcase}",
} }
content_tag :li, class: active do
link_to request.path, link_opts do link_to request.path, link_opts do
content_tag(:span, ' ' + tooltip) content_tag(:span, ' ' + tooltip)
end end
end end
end
def icon_for_event def icon_for_event
{ {
......
...@@ -175,7 +175,7 @@ class Ability ...@@ -175,7 +175,7 @@ class Ability
:create_merge_request, :create_merge_request,
:create_wiki, :create_wiki,
:manage_builds, :manage_builds,
:download_build_artifacts, :read_build_artifacts,
:push_code :push_code
] ]
end end
......
...@@ -30,10 +30,12 @@ ...@@ -30,10 +30,12 @@
# description :string(255) # description :string(255)
# artifacts_file :text # artifacts_file :text
# gl_project_id :integer # gl_project_id :integer
# artifacts_metadata :text
# #
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace'] LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner' belongs_to :runner, class_name: 'Ci::Runner'
...@@ -49,6 +51,7 @@ module Ci ...@@ -49,6 +51,7 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) } 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_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable acts_as_taggable
...@@ -291,20 +294,17 @@ module Ci ...@@ -291,20 +294,17 @@ module Ci
end end
def target_url 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 end
def cancel_url def cancel_url
if active? 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
end end
def retry_url def retry_url
if retryable? 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
end end
...@@ -321,20 +321,35 @@ module Ci ...@@ -321,20 +321,35 @@ module Ci
pending? && !any_runners_online? pending? && !any_runners_online?
end 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 def execute_hooks
build_data = Gitlab::BuildDataBuilder.build(self) build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks) project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks)
end 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 private
......
...@@ -18,8 +18,12 @@ module Ci ...@@ -18,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id 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 attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end end
......
...@@ -131,7 +131,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -131,7 +131,11 @@ class CommitStatus < ActiveRecord::Base
false false
end end
def download_url def artifacts_download_url
nil
end
def artifacts_browse_url
nil nil
end end
end end
...@@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base ...@@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider }
def ldap?
provider.starts_with?('ldap')
end
end end
...@@ -397,7 +397,7 @@ class Project < ActiveRecord::Base ...@@ -397,7 +397,7 @@ class Project < ActiveRecord::Base
result.password = '*****' unless result.password.nil? result.password = '*****' unless result.password.nil?
result.to_s result.to_s
rescue rescue
original_url self.import_url
end end
def check_limit def check_limit
......
...@@ -196,10 +196,22 @@ class User < ActiveRecord::Base ...@@ -196,10 +196,22 @@ class User < ActiveRecord::Base
state_machine :state, initial: :active do state_machine :state, initial: :active do
event :block do event :block do
transition active: :blocked transition active: :blocked
transition ldap_blocked: :blocked
end
event :ldap_block do
transition active: :ldap_blocked
end end
event :activate do event :activate do
transition blocked: :active transition blocked: :active
transition ldap_blocked: :active
end
state :blocked, :ldap_blocked do
def blocked?
true
end
end end
end end
...@@ -207,7 +219,7 @@ class User < ActiveRecord::Base ...@@ -207,7 +219,7 @@ class User < ActiveRecord::Base
# Scopes # Scopes
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_state(:blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :active, -> { with_state(:active) } 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 :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)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
......
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
...@@ -60,8 +60,8 @@ ...@@ -60,8 +60,8 @@
%td %td
.pull-right .pull-right
- if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts?
= link_to build.download_url, title: 'Download artifacts' do = link_to build.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download %i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, build.project) - if current_user && can?(current_user, :manage_builds, build.project)
- if build.active? - if build.active?
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if @all_builds.running_or_pending.any? - 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 = 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?)} %li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do = link_to admin_builds_path do
All All
......
- page_title "Logs" - page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger] Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
%ul.nav.nav-tabs.log-tabs %ul.nav-links.log-tabs
- loggers.each do |klass| - loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}", = link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab' '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 .tab-content
- loggers.each do |klass| - loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
Edit Edit
%hr %hr
%ul.nav.nav-tabs %ul.nav-links
= nav_link(path: 'users#show') do = nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user) = link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do = nav_link(path: 'users#groups') do
...@@ -23,3 +23,4 @@ ...@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user) = link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do = nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user) = link_to "Identities", admin_user_identities_path(@user)
.append-bottom-default
- page_title "Users" - page_title "Users"
= render 'shared/show_aside' = render 'shared/show_aside'
.row .admin-filter
%aside.col-md-3 %ul.nav-links
.admin-filter
%ul.nav.nav-pills.nav-stacked
%li{class: "#{'active' unless params[:filter]}"} %li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do = link_to admin_users_path do
Active Active
%small.pull-right= number_with_delimiter(User.active.count) %small.badge= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"} %li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do = link_to admin_users_path(filter: "admins") do
Admins Admins
%small.pull-right= number_with_delimiter(User.admins.count) %small.badge= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do = link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled 2FA Enabled
%small.pull-right= number_with_delimiter(User.with_two_factor.count) %small.badge= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do = link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled 2FA Disabled
%small.pull-right= number_with_delimiter(User.without_two_factor.count) %small.badge= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"} %li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do = link_to admin_users_path(filter: "blocked") do
Blocked Blocked
%small.pull-right= number_with_delimiter(User.blocked.count) %small.badge= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"} %li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do = link_to admin_users_path(filter: "wop") do
Without projects Without projects
%small.pull-right= number_with_delimiter(User.without_projects.count) %small.badge= 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"
%section.col-md-9 .gray-content-block.second-block
.panel.panel-default .pull-right
.panel-heading
Users (#{number_with_delimiter(@users.total_count)})
.panel-head-actions
.dropdown.inline .dropdown.inline
%a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light sort: %span.light sort:
- if @sort.present? - if @sort.present?
= sort_options_hash[@sort] = sort_options_hash[@sort]
...@@ -69,7 +55,16 @@ ...@@ -69,7 +55,16 @@
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated = sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" = 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 %ul.well-list
- @users.each do |user| - @users.each do |user|
%li %li
...@@ -88,14 +83,19 @@ ...@@ -88,14 +83,19 @@
%i.fa.fa-envelope %i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light' = mail_to user.email, user.email, class: 'light'
&nbsp; &nbsp;
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs" .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 - unless user == current_user
- if user.blocked? - if user.ldap_blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success" = 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 - 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" = 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? - 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?' } = 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? - 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" = 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" = paginate @users, theme: "gitlab"
.hidden-xs .hidden-xs
= render "events/event_last_push", event: @last_push = render "events/event_last_push", event: @last_push
.gray-content-block .nav-block
- if current_user - if current_user
.pull-right .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
%ul.center-top-menu %ul.nav-links
%li{ class: ("active" unless params[:filter]) } %li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects Your Projects
......
%ul.center-top-menu %ul.nav-links
= nav_link(page: dashboard_groups_path) do = nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
Your Groups Your Groups
......
= content_for :flash_message do = content_for :flash_message do
= render 'shared/project_limit' = render 'shared/project_limit'
.top-area .top-area
%ul.left-top-menu %ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects Your Projects
......
%ul.center-top-menu %ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets Your Snippets
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
#{@milestone.open_items_count} open #{@milestone.open_items_count} open
= milestone_progress_bar(@milestone) = milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom %ul.nav-links.no-top.no-bottom
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do = link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues Issues
......
...@@ -3,29 +3,33 @@ ...@@ -3,29 +3,33 @@
= render 'dashboard/snippets_head' = render 'dashboard/snippets_head'
.gray-content-block .nav-block
.pull-right .controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus') = icon('plus')
New Snippet New Snippet
.btn-group.btn-group-next.snippet-scope-menu .nav-links.snippet-scope-menu
= link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do %li{ class: ("active" unless params[:scope]) }
= link_to dashboard_snippets_path do
All All
%span.badge %span.badge
= current_user.snippets.count = current_user.snippets.count
= link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do %li{ class: ("active" if params[:scope] == "are_private") }
= link_to dashboard_snippets_path(scope: 'are_private') do
Private Private
%span.badge %span.badge
= current_user.snippets.are_private.count = 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 %li{ class: ("active" if params[:scope] == "are_internal") }
= link_to dashboard_snippets_path(scope: 'are_internal') do
Internal Internal
%span.badge %span.badge
= current_user.snippets.are_internal.count = 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 %li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public Public
%span.badge %span.badge
= current_user.snippets.are_public.count = current_user.snippets.are_public.count
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%h3 Sign in %h3 Sign in
.login-body .login-body
- if form_based_providers.any? - if form_based_providers.any?
%ul.nav.nav-tabs %ul.nav-links
- if crowd_enabled? - if crowd_enabled?
%li.active %li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab' = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
......
- header_title group_title(@group, "Settings", edit_group_path(@group)) - header_title group_title(@group, "Settings", edit_group_path(@group))
- @blank_container = true - @blank_container = true
.panel.panel-default .panel.panel-default.prepend-top-default
.panel-heading .panel-heading
Group settings Group settings
.panel-body .panel-body
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- header_title group_title(@group, "Members", group_group_members_path(@group)) - header_title group_title(@group, "Members", group_group_members_path(@group))
- @blank_container = true - @blank_container = true
.group-members-page .group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group) - if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
#{@milestone.open_items_count} open #{@milestone.open_items_count} open
= milestone_progress_bar(@milestone) = milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom %ul.nav-links.no-top.no-bottom
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do = link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues Issues
......
- page_title "Projects" - page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group)) - header_title group_title(@group, "Projects", projects_group_path(@group))
.panel.panel-default .panel.panel-default.prepend-top-default
.panel-heading .panel-heading
%strong= @group.name %strong= @group.name
projects: projects:
......
- @no_container = true
- @blank_container = true
- unless can?(current_user, :read_group, @group) - unless can?(current_user, :read_group, @group)
- @disable_search_panel = true - @disable_search_panel = true
...@@ -25,8 +28,8 @@ ...@@ -25,8 +28,8 @@
.cover-desc.description .cover-desc.description
= markdown(@group.description, pipeline: :description) = markdown(@group.description, pipeline: :description)
- if can?(current_user, :read_group, @group)
%ul.center-top-menu.no-top %ul.nav-links
%li.active %li.active
= link_to "#activity", 'data-toggle' => 'tab' do = link_to "#activity", 'data-toggle' => 'tab' do
Activity Activity
...@@ -35,9 +38,11 @@ ...@@ -35,9 +38,11 @@
= link_to "#projects", 'data-toggle' => 'tab' do = link_to "#projects", 'data-toggle' => 'tab' do
Projects Projects
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.tab-content .tab-content
.tab-pane.active#activity .tab-pane.active#activity
.gray-content-block.activity-filter-block .activity-filter-block
- if current_user - if current_user
= render "events/event_last_push", event: @last_push = render "events/event_last_push", event: @last_push
...@@ -50,5 +55,5 @@ ...@@ -50,5 +55,5 @@
= render "projects", projects: @projects = render "projects", projects: @projects
- else - else
%p.center-top-menu.no-top %p.nav-links.no-top
No projects to show No projects to show
...@@ -139,26 +139,9 @@ ...@@ -139,26 +139,9 @@
%h2#navs Navigation %h2#navs Navigation
%h4 %h4
%code .center-top-menu %code .nav-links
.example .example
%ul.center-top-menu %ul.nav-links
%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
%li.active %li.active
%a Open %a Open
%li %li
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
.content-wrapper .content-wrapper
= render "layouts/flash" = render "layouts/flash"
= yield :flash_message = yield :flash_message
%div{ class: container_class } %div{ class: (container_class unless @no_container) }
.content .content
.clearfix .clearfix
= yield = yield
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.alert.alert-info .alert.alert-info
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.account-page .account-page.prepend-top-default
.panel.panel-default.update-token .panel.panel-default.update-token
.panel-heading .panel-heading
Reset Private token Reset Private token
......
.gray-content-block.activity-filter-block .nav-block.activity-filter-block
- if current_user - 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 = 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 %i.fa.fa-rss
......
#tree-holder.tree-holder.clearfix #tree-holder.tree-holder.clearfix
.gray-content-block.second-block .nav-block
= render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree = render 'projects/tree/tree_content', tree: @tree
......
.md-area .md-area
.md-header.clearfix .md-header.clearfix
%ul.center-top-menu %ul.nav-links
%li.active %li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1") %a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write Write
......
%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
%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')
- 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
.gray-content-block.top-block .nav-block
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path = render 'shared/ref_switcher', destination: 'blob', path: @path
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= render "header_title" = render "header_title"
.file-editor .file-editor
%ul.center-top-menu.no-bottom.js-edit-mode %ul.nav-links.no-bottom.js-edit-mode
%li.active %li.active
= link_to '#editor' do = link_to '#editor' do
= icon('edit') = icon('edit')
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%li(class="js-branch-#{branch.name}") %li(class="js-branch-#{branch.name}")
%div %div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
%strong.str-truncated= branch.name .branch-name.str-truncated= branch.name
&nbsp; &nbsp;
- if branch.name == @repository.root_ref - if branch.name == @repository.root_ref
%span.label.label-primary default %span.label.label-primary default
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @all_builds.running_or_pending.any? - 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 = 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?)} %li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do = link_to project_builds_path(@project) do
All All
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
#up-build-trace #up-build-trace
- if @commit.matrix_for_ref?(@build.ref) - 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| - @commit.latest_builds_for_ref(@build.ref).each do |build|
%li{class: ('active' if build == @build) } %li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do = link_to namespace_project_build_path(@project.namespace, @project, build) do
...@@ -89,9 +89,15 @@ ...@@ -89,9 +89,15 @@
Test coverage Test coverage
%h1 #{@build.coverage}% %h1 #{@build.coverage}%
- if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts?
.build-widget.center
= link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary' .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 .build-widget
%h4.title %h4.title
......
%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 = nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes Changes
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
.commit-info-row.branches .commit-info-row.branches
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
.commit-box.gray-content-block.middle-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line = markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present? - if @commit.description.present?
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
- page_description @commit.description - page_description @commit.description
= render "projects/commits/header_title" = render "projects/commits/header_title"
= render "commit_box"
.prepend-top-default
= render "commit_box"
- if @ci_commit - if @ci_commit
= render "ci_menu" = render "ci_menu"
- else - else
......
...@@ -66,8 +66,8 @@ ...@@ -66,8 +66,8 @@
%td %td
.pull-right .pull-right
- if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts?
= link_to commit_status.download_url, title: 'Download artifacts' do = link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download %i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, commit_status.project) - if current_user && can?(current_user, :manage_builds, commit_status.project)
- if commit_status.active? - if commit_status.active?
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= cache(cache_key) do = cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title .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" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description? - if commit.description?
%a.text-expander.js-toggle-button ... %a.text-expander.js-toggle-button ...
......
%ul.center-top-menu %ul.nav-links
= nav_link(controller: [:commit, :commits]) do = nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits Commits
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= render "head" = render "head"
.gray-content-block .gray-content-block.second-block
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits' = render 'shared/ref_switcher', destination: 'commits'
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- diff_files = safe_diff_files(diffs) - diff_files = safe_diff_files(diffs)
.gray-content-block.middle-block.oneline-block .content-block.oneline-block
.inline-parallel-buttons .inline-parallel-buttons
.btn-group .btn-group
= inline_diff_btn = inline_diff_btn
......
- @blank_container = true - @blank_container = true
.project-edit-container .project-edit-container.prepend-top-default
.project-edit-errors .project-edit-errors
.project-edit-content .project-edit-content
.panel.panel-default .panel.panel-default
......
%ul.center-top-menu %ul.nav-links
= nav_link(action: :show) do = nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path = link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do = nav_link(action: :commits) do
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= f.hidden_field :target_branch = f.hidden_field :target_branch
.mr-compare.merge-request .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 %li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits Commits
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present? - 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 %li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion Discussion
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
%span.pull-right= @milestone.expires_at %span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone) = milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom %ul.nav-links.no-top.no-bottom
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do = link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues Issues
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= render "header_title" = render "header_title"
- @blank_container = true - @blank_container = true
.project-members-page .project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project) - if can?(current_user, :admin_project_member, @project)
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
......
- page_title "Runners" - page_title "Runners"
.light
.light.prepend-top-default
%p %p
A 'runner' is a process which runs a build. A 'runner' is a process which runs a build.
You can setup as many runners as you need. You can setup as many runners as you need.
......
- @no_container = true
- @blank_container = true
= content_for :meta_tags do = content_for :meta_tags do
- if current_user - 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") = 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 @@ ...@@ -8,11 +11,10 @@
= render 'shared/no_password' = render 'shared/no_password'
= render 'projects/last_push' = render 'projects/last_push'
= render "home_panel" = render "home_panel"
.project-stats.gray-content-block.second-block .project-stats.gray-content-block.second-block
%ul.nav.nav-pills %ul.nav
%li %li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit') = pluralize(number_with_delimiter(@project.commit_count), 'commit')
...@@ -57,15 +59,17 @@ ...@@ -57,15 +59,17 @@
= link_to add_contribution_guide_path(@project) do = link_to add_contribution_guide_path(@project) do
Add Contribution guide Add Contribution guide
- if @project.archived? - if @repository.commit
.content-block.second-block.white
%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 .text-warning.center.prepend-top-20
%p %p
= icon("exclamation-triangle fw") = icon("exclamation-triangle fw")
Archived project! Repository is read-only Archived project! Repository is read-only
- if @repository.commit %div{class: "project-show-#{default_project_view}"}
.content-block.second-block.white
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{class: "project-show-#{default_project_view}"}
= render default_project_view = render default_project_view
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%li %li
%div %div
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%strong .tag-name
= icon('tag') = icon('tag')
= tag.name = tag.name
- if tag.message.present? - if tag.message.present?
......
...@@ -17,8 +17,8 @@ ...@@ -17,8 +17,8 @@
.pull-right .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 = 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 %i.fa.fa-trash-o
.title .tag-name.title
%strong= @tag.name = @tag.name
- if @tag.message.present? - if @tag.message.present?
%span.light %span.light
&nbsp; &nbsp;
......
%div.tree-content-holder %div.tree-content-holder
.table-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 %thead
%tr %tr
%th Name %th Name
......
...@@ -5,13 +5,13 @@ ...@@ -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") = 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' = render 'projects/last_push'
.pull-right .tree-controls
= render 'projects/find_file_link' = render 'projects/find_file_link'
- if can? current_user, :download_code, @project - 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 = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix #tree-holder.tree-holder.clearfix
.gray-content-block.top-block .nav-block
= render 'projects/tree/tree_header', tree: @tree = render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree = render 'projects/tree/tree_content', tree: @tree
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= render 'projects/wikis/new' = render 'projects/wikis/new'
%ul.center-top-menu %ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
......
%ul.nav.nav-tabs.search-filter %ul.nav-links.search-filter
- if @project - if @project
%li{class: ("active" if @scope == 'blobs')} %li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do = link_to search_filter_path(scope: 'blobs') do
......
- if @search_results.empty? - if @search_results.empty?
= render partial: "search/results/empty" = render partial: "search/results/empty"
- else - else
%p.light .gray-content-block
Search results for Search results for
%code %code
= @search_term = @search_term
......
- page_title @search_term - page_title @search_term
= render 'search/form'
.prepend-top-default
= render 'search/form'
- if @search_term - if @search_term
= render 'search/category' = render 'search/category'
= render 'search/results' = render 'search/results'
.btn-group.btn-group-next.event-filter %ul.nav-links.event-filter
= event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.comments, 'Comments'
......
.milestones-filters .milestones-filters
%ul.center-top-menu %ul.nav-links
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
= link_to milestones_filter_path(state: 'opened') do = link_to milestones_filter_path(state: 'opened') do
Open Open
......
...@@ -10,8 +10,7 @@ ...@@ -10,8 +10,7 @@
%i.fa.fa-sign-out %i.fa.fa-sign-out
= image_tag group_icon(group), class: "avatar s46 hidden-xs" = image_tag group_icon(group), class: "avatar s46 hidden-xs"
= link_to group, class: 'group-name' do = link_to group.name, group, class: 'group-name'
%strong= group.name
- if group_member - if group_member
as as
......
.issues-filters .issues-filters
.issues-state-filters .issues-state-filters
%ul.center-top-menu %ul.nav-links
- if defined?(type) && type == :merge_requests - if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests' - page_context_word = 'merge requests'
- else - else
......
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query') - page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path - header_title t('sherlock.title'), sherlock_transactions_path
%ul.center-top-menu %ul.nav-links
%li.active %li.active
%a(href="#tab-general" data-toggle="tab") %a(href="#tab-general" data-toggle="tab")
= t('sherlock.general') = t('sherlock.general')
......
- page_title t('sherlock.title'), t('sherlock.transaction') - page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path - header_title t('sherlock.title'), sherlock_transactions_path
%ul.center-top-menu %ul.nav-links
%li.active %li.active
%a(href="#tab-general" data-toggle="tab") %a(href="#tab-general" data-toggle="tab")
= t('sherlock.general') = t('sherlock.general')
......
- page_title @user.name - page_title @user.name
- page_description @user.bio - page_description @user.bio
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- @no_container = true
- @blank_container = true
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
...@@ -8,6 +10,25 @@ ...@@ -8,6 +10,25 @@
= render 'shared/show_aside' = render 'shared/show_aside'
.cover-block .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 .avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do = link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
...@@ -47,34 +68,7 @@ ...@@ -47,34 +68,7 @@
= icon('map-marker') = icon('map-marker')
= @user.location = @user.location
%ul.nav-links.center
.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
%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
%li.active %li.active
= link_to "#activity", 'data-toggle' => 'tab' do = link_to "#activity", 'data-toggle' => 'tab' do
Activity Activity
...@@ -91,8 +85,17 @@ ...@@ -91,8 +85,17 @@
= link_to "#personal", 'data-toggle' => 'tab' do = link_to "#personal", 'data-toggle' => 'tab' do
Personal projects Personal projects
.tab-content %div{ class: container_class }
.tab-content
.tab-pane.active#activity .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
.content_list .content_list
= spinner = spinner
...@@ -107,7 +110,7 @@ ...@@ -107,7 +110,7 @@
.contributed-projects .contributed-projects
= render 'shared/projects/list', = render 'shared/projects/list',
projects: @contributed_projects.sort_by(&:star_count).reverse, projects: @contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: true projects_limit: 10, stars: true, avatar: true
- if @projects.present? - if @projects.present?
.tab-pane#personal .tab-pane#personal
......
...@@ -604,9 +604,14 @@ Rails.application.routes.draw do ...@@ -604,9 +604,14 @@ Rails.application.routes.draw do
member do member do
get :status get :status
post :cancel post :cancel
get :download
post :retry post :retry
end end
resource :artifacts, only: [] do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
end
end end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
......
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
class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_metadata, :text
end
end
...@@ -123,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160113111034) do ...@@ -123,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160113111034) do
t.string "description" t.string "description"
t.text "artifacts_file" t.text "artifacts_file"
t.integer "gl_project_id" t.integer "gl_project_id"
t.text "artifacts_metadata"
end 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 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
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
- [Namespaces](namespaces.md) - [Namespaces](namespaces.md)
- [Settings](settings.md) - [Settings](settings.md)
- [Keys](keys.md) - [Keys](keys.md)
- [Build Variables](build_variables.md)
## Clients ## Clients
......
# 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"
}
```
...@@ -558,7 +558,8 @@ Parameters: ...@@ -558,7 +558,8 @@ Parameters:
- `uid` (required) - id of specified user - `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 ## Unblock user
...@@ -572,4 +573,5 @@ Parameters: ...@@ -572,4 +573,5 @@ Parameters:
- `uid` (required) - id of specified user - `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.
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
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
...@@ -6,7 +6,7 @@ module SharedActiveTab ...@@ -6,7 +6,7 @@ module SharedActiveTab
end end
def ensure_active_sub_tab(content) 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 end
def ensure_active_sub_nav(content) def ensure_active_sub_nav(content)
...@@ -18,7 +18,7 @@ module SharedActiveTab ...@@ -18,7 +18,7 @@ module SharedActiveTab
end end
step 'no other sub tabs should be active' do 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 end
step 'no other sub navs should be active' do step 'no other sub navs should be active' do
......
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
...@@ -54,5 +54,6 @@ module API ...@@ -54,5 +54,6 @@ module API
mount Keys mount Keys
mount Tags mount Tags
mount Triggers mount Triggers
mount Variables
end end
end end
...@@ -366,5 +366,9 @@ module API ...@@ -366,5 +366,9 @@ module API
class TriggerRequest < Grape::Entity class TriggerRequest < Grape::Entity
expose :id, :variables expose :id, :variables
end end
class Variable < Grape::Entity
expose :key, :value
end
end end
end end
...@@ -287,12 +287,14 @@ module API ...@@ -287,12 +287,14 @@ module API
# file helpers # file helpers
def uploaded_file!(field, uploads_path) def uploaded_file(field, uploads_path)
if params[field] if params[field]
bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename) bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
return params[field] return params[field]
end end
return nil unless params["#{field}.path"] && params["#{field}.name"]
# sanitize file paths # sanitize file paths
# this requires all paths to exist # this requires all paths to exist
required_attributes! %W(#{field}.path) required_attributes! %W(#{field}.path)
......
...@@ -284,10 +284,12 @@ module API ...@@ -284,10 +284,12 @@ module API
authenticated_as_admin! authenticated_as_admin!
user = User.find_by(id: params[:id]) user = User.find_by(id: params[:id])
if user if !user
not_found!('User')
elsif !user.ldap_blocked?
user.block user.block
else else
not_found!('User') forbidden!('LDAP blocked users cannot be modified by the API')
end end
end end
...@@ -299,10 +301,12 @@ module API ...@@ -299,10 +301,12 @@ module API
authenticated_as_admin! authenticated_as_admin!
user = User.find_by(id: params[:id]) user = User.find_by(id: params[:id])
if user if !user
user.activate
else
not_found!('User') not_found!('User')
elsif user.ldap_blocked?
forbidden!('LDAP blocked users cannot be unblocked by the API')
else
user.activate
end end
end end
end end
......
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
...@@ -78,11 +78,13 @@ module Ci ...@@ -78,11 +78,13 @@ module Ci
# Parameters: # Parameters:
# id (required) - The ID of a build # id (required) - The ID of a build
# token (required) - The build authorization token # token (required) - The build authorization token
# file (required) - The uploaded file # file (required) - Artifacts file
# Parameters (accelerated by GitLab Workhorse): # Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse) # file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition # file.name - real filename as send in Content-Disposition
# file.type - real content type as send in Content-Type # 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: # Headers:
# BUILD-TOKEN (required) - The build authorization token, the same as token # BUILD-TOKEN (required) - The build authorization token, the same as token
# Body: # Body:
...@@ -96,13 +98,20 @@ module Ci ...@@ -96,13 +98,20 @@ module Ci
build = Ci::Build.find_by_id(params[:id]) build = Ci::Build.find_by_id(params[:id])
not_found! unless build not_found! unless build
authenticate_build_token!(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) artifacts_upload_path = ArtifactUploader.artifacts_upload_path
file_to_large! unless file.size < max_artifacts_size artifacts = uploaded_file(:file, artifacts_upload_path)
metadata = uploaded_file(:metadata, artifacts_upload_path)
if build.update_attributes(artifacts_file: file) bad_request!('Missing artifacts file!') unless artifacts
present build, with: Entities::Build 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 else
render_validation_error!(build) render_validation_error!(build)
end end
...@@ -148,6 +157,7 @@ module Ci ...@@ -148,6 +157,7 @@ module Ci
not_found! unless build not_found! unless build
authenticate_build_token!(build) authenticate_build_token!(build)
build.remove_artifacts_file! build.remove_artifacts_file!
build.remove_artifacts_metadata!
end end
end end
end end
......
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
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
...@@ -37,15 +37,15 @@ module Gitlab ...@@ -37,15 +37,15 @@ module Gitlab
# Block user in GitLab if he/she was blocked in AD # 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) if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
user.block user.ldap_block
false false
else else
user.activate if user.blocked? && !ldap_config.block_auto_created_users user.activate if user.ldap_blocked?
true true
end end
else else
# Block the user if they no longer exist in LDAP/AD # Block the user if they no longer exist in LDAP/AD
user.block user.ldap_block
false false
end end
rescue rescue
......
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
...@@ -34,6 +34,22 @@ describe Admin::UsersController do ...@@ -34,6 +34,22 @@ describe Admin::UsersController do
end end
describe 'PUT unblock/:id' do describe 'PUT unblock/:id' do
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
context 'manually blocked users' do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
...@@ -47,6 +63,7 @@ describe Admin::UsersController do ...@@ -47,6 +63,7 @@ describe Admin::UsersController do
expect(flash[:notice]).to eq 'Successfully unblocked' expect(flash[:notice]).to eq 'Successfully unblocked'
end end
end end
end
describe 'PUT unlock/:id' do describe 'PUT unlock/:id' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
# == 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
...@@ -80,7 +80,11 @@ describe "Builds" do ...@@ -80,7 +80,11 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
end 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
end end
...@@ -111,7 +115,7 @@ describe "Builds" do ...@@ -111,7 +115,7 @@ describe "Builds" do
before do before do
@build.update_attributes(artifacts_file: artifacts_file) @build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build) visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Download artifacts' page.within('.artifacts') { click_link 'Download' }
end end
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
......
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
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
...@@ -17,60 +17,54 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -17,60 +17,54 @@ describe Gitlab::LDAP::Access, lib: true do
it 'should block user in GitLab' do it 'should block user in GitLab' do
access.allowed? access.allowed?
expect(user).to be_blocked expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
context 'when the user is found' do context 'when the user is found' do
before do before do
allow(Gitlab::LDAP::Person). allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
to receive(:find_by_dn).and_return(:ldap_user)
end end
context 'and the user is disabled via active directory' do context 'and the user is disabled via active directory' do
before do before do
allow(Gitlab::LDAP::Person). allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
to receive(:disabled_via_active_directory?).and_return(true)
end end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it "should block user in GitLab" do it 'should block user in GitLab' do
access.allowed? access.allowed?
expect(user).to be_blocked expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
context 'and has no disabled flag in active diretory' do context 'and has no disabled flag in active diretory' do
before 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 end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
context 'when auto-created users are blocked' do context 'when auto-created users are blocked' do
before do before do
allow_any_instance_of(Gitlab::LDAP::Config). user.block
to receive(:block_auto_created_users).and_return(true)
end end
it "does not unblock user in GitLab" do it 'does not unblock user in GitLab' do
access.allowed? access.allowed?
expect(user).to be_blocked 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
end end
context "when auto-created users are not blocked" do context 'when auto-created users are not blocked' do
before do before do
allow_any_instance_of(Gitlab::LDAP::Config). user.ldap_block
to receive(:block_auto_created_users).and_return(false)
end end
it "should unblock user in GitLab" do it 'should unblock user in GitLab' do
access.allowed? access.allowed?
expect(user).not_to be_blocked expect(user).not_to be_blocked
end end
...@@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do
context 'without ActiveDirectory enabled' do context 'without ActiveDirectory enabled' do
before do before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
allow_any_instance_of(Gitlab::LDAP::Config). allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false)
to receive(:active_directory).and_return(false)
end end
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
......
# == 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' require 'spec_helper'
describe Ci::Build, models: true do describe Ci::Build, models: true do
...@@ -368,21 +343,75 @@ describe Ci::Build, models: true do ...@@ -368,21 +343,75 @@ describe Ci::Build, models: true do
end end
end end
describe :download_url do describe :artifacts_download_url do
subject { build.download_url } subject { build.artifacts_download_url }
it "should be nil if artifact doesn't exist" do it "should be nil if artifact doesn't exist" do
build.update_attributes(artifacts_file: nil) build.update_attributes(artifacts_file: nil)
is_expected.to be_nil is_expected.to be_nil
end 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') gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
build.update_attributes(artifacts_file: gif) build.update_attributes(artifacts_file: gif)
is_expected.to_not be_nil is_expected.to_not be_nil
end end
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 describe :repo_url do
let(:build) { FactoryGirl.create :ci_build } let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project } let(:project) { build.project }
......
# == 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
...@@ -569,30 +569,42 @@ describe User, models: true do ...@@ -569,30 +569,42 @@ describe User, models: true do
end end
end end
context 'ldap synchronized user' do
describe :ldap_user? do describe :ldap_user? do
it "is true if provider name starts with ldap" do it 'is true if provider name starts with ldap' do
user = create(:omniauth_user, provider: 'ldapmain') user = create(:omniauth_user, provider: 'ldapmain')
expect( user.ldap_user? ).to be_truthy expect(user.ldap_user?).to be_truthy
end end
it "is false for other providers" do it 'is false for other providers' do
user = create(:omniauth_user, provider: 'other-provider') user = create(:omniauth_user, provider: 'other-provider')
expect( user.ldap_user? ).to be_falsey expect(user.ldap_user?).to be_falsey
end end
it "is false if no extern_uid is provided" do it 'is false if no extern_uid is provided' do
user = create(:omniauth_user, extern_uid: nil) user = create(:omniauth_user, extern_uid: nil)
expect( user.ldap_user? ).to be_falsey expect(user.ldap_user?).to be_falsey
end end
end end
describe :ldap_identity do describe :ldap_identity do
it "returns ldap identity" do it 'returns ldap identity' do
user = create :omniauth_user user = create :omniauth_user
expect(user.ldap_identity.provider).not_to be_empty expect(user.ldap_identity.provider).not_to be_empty
end end
end end
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
describe '#full_website_url' do describe '#full_website_url' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -8,6 +8,8 @@ describe API::API, api: true do ...@@ -8,6 +8,8 @@ describe API::API, api: true do
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) } let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_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 describe "GET /users" do
context "when unauthenticated" do context "when unauthenticated" do
...@@ -783,6 +785,12 @@ describe API::API, api: true do ...@@ -783,6 +785,12 @@ describe API::API, api: true do
expect(user.reload.state).to eq('blocked') expect(user.reload.state).to eq('blocked')
end 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 it 'should not be available for non admin users' do
put api("/users/#{user.id}/block", user) put api("/users/#{user.id}/block", user)
expect(response.status).to eq(403) expect(response.status).to eq(403)
...@@ -797,7 +805,9 @@ describe API::API, api: true do ...@@ -797,7 +805,9 @@ describe API::API, api: true do
end end
describe 'PUT /user/:id/unblock' do describe 'PUT /user/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin } before { admin }
it 'should unblock existing user' do it 'should unblock existing user' do
put api("/users/#{user.id}/unblock", admin) put api("/users/#{user.id}/unblock", admin)
expect(response.status).to eq(200) expect(response.status).to eq(200)
...@@ -805,12 +815,15 @@ describe API::API, api: true do ...@@ -805,12 +815,15 @@ describe API::API, api: true do
end end
it 'should unblock a blocked user' do it 'should unblock a blocked user' do
put api("/users/#{user.id}/block", admin) put api("/users/#{blocked_user.id}/unblock", admin)
expect(response.status).to eq(200)
expect(user.reload.state).to eq('blocked')
put api("/users/#{user.id}/unblock", admin)
expect(response.status).to eq(200) 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 end
it 'should not be available for non admin users' do it 'should not be available for non admin users' do
......
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
...@@ -210,6 +210,52 @@ describe Ci::API::API do ...@@ -210,6 +210,52 @@ describe Ci::API::API do
end end
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 context "should fail to post too large artifact" do
before do before do
build.run! build.run!
......
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment