Commit 768721dc authored by Tomasz Maczukin's avatar Tomasz Maczukin

Merge branch 'master' into ci/api-triggers

* master: (150 commits)
  fixes margin and padding
  Update mailroom/postfix documentation [ci skip]
  Fix css mess around git clone panel. Align it properly
  Fix missing padding for user/group pages
  Fix parse_gollum_tags matcher
  Update documentation on Banzai::Filter::GollumTagsFilter
  Add tests for the wiki pipeline
  Refactoring Banzai::Filter::GollumTagsFilter
  Make sure the .git is at the end on Gitlab::GithubImport::WikiFormatter
  Remove GollumTagsPipeline
  Refactoring Gitlab::GithubImport::Importer
  Remove unnecessary brackets on WIKI_SLUG_ID route constraints
  Move js function to removing accents to vendor/assets/javascripts
  Update CHANGELOG
  Use the WikiPipeline when rendering the wiki markdown content
  Add Banzai::Filter::GollumTagsFilter for parsing Gollum's tags in HTML
  Relax constraints for wiki slug
  Import GitHub wiki into GitLab
  Move Ci::Build#available_statuses to AVAILABLE_STATUSES constant in CommitStatus
  Revert changes to how the notes are paginated in the API
  ...

Conflicts:
	doc/api/README.md
	lib/api/entities.rb
parents 7ed5bc5c 2f2aada3
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
- Properly generate diff of orphan commits, like the first commit in a repository
- Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages - 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)
...@@ -47,10 +52,15 @@ v 8.4.0 (unreleased) ...@@ -47,10 +52,15 @@ v 8.4.0 (unreleased)
- 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 project's build triggers - Add API support for managing project's build triggers
- Add API support for managing project's build variables - Add API support for managing project's build variables
- Add API support for managing builds of a project
- Add API support for managing build variables of project
- Allow broadcast messages to be edited - Allow broadcast messages to be edited
- Autosize Markdown textareas
- Import GitHub wiki into GitLab
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
...@@ -84,6 +94,7 @@ v 8.3.0 ...@@ -84,6 +94,7 @@ v 8.3.0
- Add open_issues_count to project API (Stan Hu) - Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw) - Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg) - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
- Add unsubscribe link in the email footer (Zeger-Jan van de Weg)
- Provide better diagnostic message upon project creation errors (Stan Hu) - Provide better diagnostic message upon project creation errors (Stan Hu)
- Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu) - Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
- Remove api credentials from link to build_page - Remove api credentials from link to build_page
......
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
......
#= require autosize
$ ->
autosize($('.js-autosize'))
...@@ -48,14 +48,15 @@ class @MergeRequest ...@@ -48,14 +48,15 @@ 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')
return
e.preventDefault()
e.stopImmediatePropagation()
shouldSubmit = $this.hasClass('btn-comment') shouldSubmit = $this.hasClass('btn-comment')
console.log("shouldSubmit") if shouldSubmit && $this.data('submitted')
return
if shouldSubmit if shouldSubmit
_this.submitNoteForm($this.closest('form'),$this) if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
e.preventDefault()
e.stopImmediatePropagation()
_this.submitNoteForm($this.closest('form'),$this)
submitNoteForm: (form, $button) => submitNoteForm: (form, $button) =>
noteText = form.find("textarea.js-note-text").val() noteText = form.find("textarea.js-note-text").val()
......
...@@ -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
......
#= require autosave #= require autosave
#= require autosize
#= require dropzone #= require dropzone
#= require dropzone_input #= require dropzone_input
#= require gfm_auto_complete #= require gfm_auto_complete
...@@ -246,6 +247,7 @@ class @Notes ...@@ -246,6 +247,7 @@ class @Notes
else else
previewButton.removeClass("turn-on").addClass "turn-off" previewButton.removeClass("turn-on").addClass "turn-off"
autosize(textarea)
new Autosave textarea, [ new Autosave textarea, [
"Note" "Note"
form.find("#note_commit_id").val() form.find("#note_commit_id").val()
...@@ -521,9 +523,13 @@ class @Notes ...@@ -521,9 +523,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()
......
#= require latinise
class @Wikis class @Wikis
constructor: -> constructor: ->
$('.build-new-wiki').bind "click", (e) -> $('.build-new-wiki').bind 'click', (e) =>
$('[data-error~=slug]').addClass("hidden") $('[data-error~=slug]').addClass('hidden')
$('p.hint').show()
field = $('#new_wiki_path') field = $('#new_wiki_path')
valid_slug_pattern = /^[\w\/-]+$/ slug = @slugify(field.val())
slug = field.val() if (slug.length > 0)
if slug.match valid_slug_pattern
path = field.attr('data-wikis-path') path = field.attr('data-wikis-path')
if(slug.length > 0) location.href = path + '/' + slug
location.href = path + "/" + slug
else dasherize: (value) ->
e.preventDefault() value.replace(/[_\s]+/g, '-')
$('p.hint').hide()
$('[data-error~=slug]').removeClass("hidden") slugify: (value) =>
@dasherize(value.trim().toLowerCase().latinise())
...@@ -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,19 @@ ...@@ -136,3 +133,19 @@
.block-connector { .block-connector {
margin-top: -1px; margin-top: -1px;
} }
.nav-block {
.controls {
float: right;
margin-top: 11px;
}
}
.content-block {
padding: $gl-padding 0;
border-bottom: 1px solid $border-color;
&.oneline-block {
line-height: 42px;
}
}
@mixin btn-default { @mixin btn-default {
@include border-radius(3px); @include border-radius(3px);
border-width: 1px; font-size: $gl-font-size;
border-style: solid;
font-size: 15px;
font-weight: 500; font-weight: 500;
line-height: 18px; padding: $gl-vert-padding $gl-padding;
padding: 11px $gl-padding;
letter-spacing: .4px;
&:focus, &:focus,
&:active { &:active {
...@@ -17,8 +13,6 @@ ...@@ -17,8 +13,6 @@
@mixin btn-middle { @mixin btn-middle {
@include btn-default; @include btn-default;
@include border-radius(3px);
padding: 11px 24px;
} }
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
...@@ -74,16 +68,15 @@ ...@@ -74,16 +68,15 @@
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
&.btn-small,
&.btn-sm { &.btn-sm {
padding: 5px 10px; padding: 4px 10px;
} font-size: 13px;
line-height: 18px;
&.btn-nr {
padding: 7px 10px;
} }
&.btn-xs { &.btn-xs {
padding: 1px 5px; padding: 2px 5px;
} }
&.btn-success, &.btn-success,
...@@ -159,33 +152,42 @@ ...@@ -159,33 +152,42 @@
} }
} }
.btn-group-next { .btn-clipboard {
border: none;
padding: 0 5px;
}
.input-group-btn {
.btn { .btn {
padding: 9px 0px; @include btn-gray;
font-size: 15px; @include btn-middle;
color: #7f8fa4;
border-color: #e7e9ed; &:hover {
width: 140px; outline: none;
}
.badge {
font-weight: normal; &:focus {
background-color: #eee; outline: none;
color: #78a;
} }
&.active { &:active {
border-color: $gl-info; outline: none;
background: $gl-info; }
color: #fff;
.badge { &.btn-clipboard {
color: $gl-info; padding-left: 15px;
background-color: white; padding-right: 15px;
}
} }
} }
}
.btn-clipboard { .active {
border: none; @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
}
.btn-green {
@include btn-green
}
} }
...@@ -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;
} }
} }
...@@ -74,8 +74,6 @@ label { ...@@ -74,8 +74,6 @@ label {
.form-control { .form-control {
@include box-shadow(none); @include box-shadow(none);
height: 42px;
padding: 8px $gl-padding;
} }
.form-control-inline { .form-control-inline {
......
...@@ -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;
} }
} }
...@@ -148,7 +147,7 @@ ul.controls { ...@@ -148,7 +147,7 @@ ul.controls {
> li { > li {
float: left; float: left;
margin-right: 10px; margin-right: 10px;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
......
...@@ -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,28 +78,12 @@ ...@@ -85,28 +78,12 @@
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;
border: 1px solid #ddd; border: 1px solid #ddd;
min-height: 140px; min-height: 140px;
max-height: 430px;
padding: 5px; padding: 5px;
box-shadow: none; box-shadow: none;
width: 100%; width: 100%;
......
...@@ -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;
...@@ -100,11 +100,6 @@ ...@@ -100,11 +100,6 @@
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
.page-with-sidebar .content-wrapper {
padding: 0;
padding-top: 1px;
}
.issues-filters { .issues-filters {
.milestone-filter, .labels-filter { .milestone-filter, .labels-filter {
display: none; display: none;
......
.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;
}
}
}
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
.select2-choice { .select2-choice {
background: #FFF; background: #FFF;
border-color: #DDD; border-color: #DDD;
height: 42px; height: 36px;
padding: 8px $gl-padding; padding: 6px $gl-padding;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 1.42857143; line-height: 1.42857143;
......
...@@ -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 {
&.table { &.table {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
.dropdown-menu a { .dropdown-menu a {
text-decoration: none; text-decoration: none;
} }
...@@ -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
......
...@@ -46,7 +46,7 @@ $font-size-base: $gl-font-size; ...@@ -46,7 +46,7 @@ $font-size-base: $gl-font-size;
// //
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
$padding-base-vertical: 9px; $padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding; $padding-base-horizontal: $gl-padding;
$component-active-color: #fff; $component-active-color: #fff;
$component-active-bg: $brand-info; $component-active-bg: $brand-info;
......
...@@ -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;
......
...@@ -22,6 +22,7 @@ $header-height: 58px; ...@@ -22,6 +22,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$gl-gray: #5a5a5a; $gl-gray: #5a5a5a;
$gl-padding: 16px; $gl-padding: 16px;
$gl-vert-padding: 6px;
$gl-padding-top:10px; $gl-padding-top:10px;
$gl-avatar-size: 46px; $gl-avatar-size: 46px;
$secondary-text: #7f8fa4; $secondary-text: #7f8fa4;
......
...@@ -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 {
......
...@@ -39,6 +39,8 @@ ...@@ -39,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;
...@@ -23,14 +21,6 @@ ...@@ -23,14 +21,6 @@
} }
} }
.diff-controls {
.btn {
padding: 0px 10px;
font-size: 13px;
line-height: 28px;
}
}
.commit-short-id { .commit-short-id {
font-family: $monospace_font; font-family: $monospace_font;
font-size: smaller; font-size: smaller;
......
...@@ -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;
......
...@@ -27,10 +27,10 @@ ...@@ -27,10 +27,10 @@
.project-issuable-filter { .project-issuable-filter {
.controls { .controls {
float: right; float: right;
margin-top: 7px; margin-top: 11px;
} }
.center-top-menu { .nav-links {
text-align: left; text-align: left;
} }
} }
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
.cross-project-reference { .cross-project-reference {
color: $gl-link-color; color: $gl-link-color;
span { span {
white-space: nowrap; white-space: nowrap;
width: 85%; width: 85%;
...@@ -105,8 +105,13 @@ ...@@ -105,8 +105,13 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
cite {
font-style: normal;
}
button { button {
float: right; float: right;
padding: 3px 5px;
} }
} }
......
...@@ -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 {
......
...@@ -159,6 +159,7 @@ ...@@ -159,6 +159,7 @@
.edit_note { .edit_note {
.markdown-area { .markdown-area {
min-height: 140px; min-height: 140px;
max-height: 430px;
} }
.note-form-actions { .note-form-actions {
background: transparent; background: transparent;
......
...@@ -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 {
...@@ -51,6 +53,8 @@ ...@@ -51,6 +53,8 @@
} }
.notifications-btn { .notifications-btn {
margin-top: -28px;
.fa-bell { .fa-bell {
margin-right: 6px; margin-right: 6px;
} }
...@@ -75,17 +79,6 @@ ...@@ -75,17 +79,6 @@
} }
} }
.git-clone-holder {
max-width: 498px;
.form-control {
background: #FFF;
font-size: 14px;
height: 42px;
margin-left: -1px;
}
}
.visibility-level-label { .visibility-level-label {
@extend .btn; @extend .btn;
@extend .btn-gray; @extend .btn-gray;
...@@ -98,11 +91,6 @@ ...@@ -98,11 +91,6 @@
} }
} }
.git-clone-holder {
display: inline-table;
position: relative;
}
.project-repo-buttons { .project-repo-buttons {
margin-top: 12px; margin-top: 12px;
margin-bottom: 0px; margin-bottom: 0px;
...@@ -112,10 +100,22 @@ ...@@ -112,10 +100,22 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
.clone-row {
.split-repo-buttons,
.project-clone-holder {
display: inline-block;
}
.split-repo-buttons {
margin: 0 12px;
}
}
.btn { .btn {
@include btn-gray; @include btn-gray;
text-transform: none; text-transform: none;
} }
.count-with-arrow { .count-with-arrow {
display: inline-block; display: inline-block;
position: relative; position: relative;
...@@ -160,8 +160,8 @@ ...@@ -160,8 +160,8 @@
border-style: solid; border-style: solid;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
line-height: 20px; line-height: 13px;
padding: 11px 16px; padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px; letter-spacing: .4px;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
...@@ -189,117 +189,6 @@ ...@@ -189,117 +189,6 @@
} }
} }
.git-clone-holder {
.project-home-dropdown + & {
margin-right: 45px;
}
.clone-options {
display: table-cell;
a.btn {
width: 100%;
}
}
.form-control {
cursor: auto;
@extend .monospace;
background: #FAFAFA;
width: 101%;
}
.input-group-addon {
background: #f7f8fa;
&.git-protocols {
padding: 0;
border: none;
.input-group-btn:last-child > .btn {
@include border-radius-right(0);
border-left: 1px solid #c6cacf;
margin-left: -2px !important;
}
}
}
}
.projects-search-form {
.input-group .form-control {
height: 42px;
}
}
.input-group-btn {
.btn {
@include btn-gray;
@include btn-middle;
&:hover {
outline: none;
}
&:focus {
outline: none;
}
&:active {
outline: none;
}
&.btn-clipboard {
padding-left: 15px;
padding-right: 15px;
}
}
.active {
@include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
}
.btn-green {
@include btn-green
}
}
.split-repo-buttons {
display: inline-table;
margin: 0 12px 0 12px;
.btn{
@include btn-gray;
@include btn-default;
}
.dropdown-toggle {
margin: -5px;
}
}
#notification-form {
margin-left: 5px;
}
.dropdown-new {
margin-left: -5px;
}
.open > .dropdown-new.btn {
@include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
text-transform: none;
color: #313236 !important;
font-size: 15px;
}
.dropdown-menu { .dropdown-menu {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px); @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px); @include border-radius (0px);
...@@ -351,28 +240,6 @@ ...@@ -351,28 +240,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;
} }
...@@ -399,9 +266,9 @@ ul.nav.nav-projects-tabs { ...@@ -399,9 +266,9 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb { .breadcrumb.repo-breadcrumb {
padding: 0; padding: 0;
line-height: 42px;
background: transparent; background: transparent;
border: none; border: none;
line-height: 42px;
margin: 0; margin: 0;
> li + li:before { > li + li:before {
...@@ -416,11 +283,8 @@ ul.nav.nav-projects-tabs { ...@@ -416,11 +283,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;
...@@ -431,12 +295,12 @@ ul.nav.nav-projects-tabs { ...@@ -431,12 +295,12 @@ ul.nav.nav-projects-tabs {
width: 50%; width: 50%;
display: inline-block; display: inline-block;
float: right; float: right;
padding-top: 7px; padding-top: 11px;
text-align: right; text-align: right;
.btn-green { .btn-green {
margin-top: -2px;
margin-left: 10px; margin-left: 10px;
float: right;
} }
} }
...@@ -482,11 +346,11 @@ table.table.protected-branches-list tr.no-border { ...@@ -482,11 +346,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 +387,7 @@ pre.light-well { ...@@ -523,8 +387,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 +437,8 @@ pre.light-well { ...@@ -574,10 +437,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 +492,6 @@ pre.light-well { ...@@ -631,8 +492,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 +521,7 @@ pre.light-well { ...@@ -662,9 +521,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 {
...@@ -672,3 +529,32 @@ pre.light-well { ...@@ -672,3 +529,32 @@ pre.light-well {
position: relative; position: relative;
} }
} }
.git-clone-holder {
width: 498px;
.btn-clipboard {
border: 1px solid $border-color;
padding: 6px $gl-padding;
}
.project-home-dropdown + & {
margin-right: 45px;
}
.clone-options {
display: table-cell;
a.btn {
width: 100%;
}
}
.form-control {
@extend .monospace;
background: #FFF;
font-size: 14px;
margin-left: -1px;
cursor: auto;
width: 101%;
}
}
.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;
}
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
class SentNotificationsController < ApplicationController
skip_before_action :authenticate_user!
def unsubscribe
@sent_notification = SentNotification.for(params[:id])
return render_404 unless @sent_notification && @sent_notification.unsubscribable?
noteable = @sent_notification.noteable
noteable.unsubscribe(@sent_notification.recipient)
flash[:notice] = "You have been unsubscribed from this thread."
if current_user
case noteable
when Issue
redirect_to issue_path(noteable)
when MergeRequest
redirect_to merge_request_path(noteable)
else
redirect_to root_path
end
else
redirect_to new_user_session_path
end
end
end
...@@ -17,7 +17,7 @@ module ButtonHelper ...@@ -17,7 +17,7 @@ module ButtonHelper
def clipboard_button(data = {}) def clipboard_button(data = {})
content_tag :button, content_tag :button,
icon('clipboard'), icon('clipboard'),
class: 'btn btn-xs btn-clipboard', class: 'btn btn-clipboard',
data: data, data: data,
type: :button type: :button
end end
......
...@@ -27,13 +27,15 @@ module EventsHelper ...@@ -27,13 +27,15 @@ 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}",
} }
link_to request.path, link_opts do content_tag :li, class: active do
content_tag(:span, ' ' + tooltip) link_to request.path, link_opts do
content_tag(:span, ' ' + tooltip)
end
end end
end end
......
...@@ -91,7 +91,7 @@ module GitlabMarkdownHelper ...@@ -91,7 +91,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page) def render_wiki_content(wiki_page)
case wiki_page.format case wiki_page.format
when :markdown when :markdown
markdown(wiki_page.content) markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
when :asciidoc when :asciidoc
asciidoc(wiki_page.content) asciidoc(wiki_page.content)
else else
......
module Emails module Emails
module Issues module Issues
def new_issue_email(recipient_id, issue_id) def new_issue_email(recipient_id, issue_id)
issue_mail_with_notification(issue_id, recipient_id) do setup_issue_mail(issue_id, recipient_id)
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id) def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
issue_mail_with_notification(issue_id, recipient_id) do setup_issue_mail(issue_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
end mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id) def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
issue_mail_with_notification(issue_id, recipient_id) do setup_issue_mail(issue_id, recipient_id)
@updated_by = User.find updated_by_user_id
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) @updated_by = User.find updated_by_user_id
end mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
issue_mail_with_notification(issue_id, recipient_id) do setup_issue_mail(issue_id, recipient_id)
@issue_status = status
@updated_by = User.find updated_by_user_id @issue_status = status
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) @updated_by = User.find updated_by_user_id
end mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
private private
...@@ -38,14 +38,12 @@ module Emails ...@@ -38,14 +38,12 @@ module Emails
} }
end end
def issue_mail_with_notification(issue_id, recipient_id) def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id) @issue = Issue.find(issue_id)
@project = @issue.project @project = @issue.project
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue) @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
yield @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
SentNotification.record(@issue, recipient_id, reply_key)
end end
end end
end end
module Emails module Emails
module MergeRequests module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id) def new_merge_request_email(recipient_id, merge_request_id)
@merge_request = MergeRequest.find(merge_request_id) setup_merge_request_mail(merge_request_id, recipient_id)
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_new_thread(@merge_request, mail_new_thread(@merge_request,
from: sender(@merge_request.author_id), from: sender(@merge_request.author_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
@merge_request = MergeRequest.find(merge_request_id) setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request,
from: sender(updated_by_user_id), from: sender(updated_by_user_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
@merge_request = MergeRequest.find(merge_request_id) setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find updated_by_user_id @updated_by = User.find updated_by_user_id
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request,
from: sender(updated_by_user_id), from: sender(updated_by_user_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
@merge_request = MergeRequest.find(merge_request_id) setup_merge_request_mail(merge_request_id, recipient_id)
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request,
from: sender(updated_by_user_id), from: sender(updated_by_user_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
@merge_request = MergeRequest.find(merge_request_id) setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status @mr_status = status
@project = @merge_request.project
@updated_by = User.find updated_by_user_id @updated_by = User.find updated_by_user_id
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request,
from: sender(updated_by_user_id), from: sender(updated_by_user_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end
private
def setup_merge_request_mail(merge_request_id, recipient_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
SentNotification.record(@merge_request, recipient_id, reply_key) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end end
end end
end end
module Emails module Emails
module Notes module Notes
def note_commit_email(recipient_id, note_id) def note_commit_email(recipient_id, note_id)
note_mail_with_notification(note_id, recipient_id) do setup_note_mail(note_id, recipient_id)
@commit = @note.noteable
@target_url = namespace_project_commit_url(*note_target_url_options) @commit = @note.noteable
@target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit,
from: sender(@note.author_id), mail_answer_thread(@commit,
to: recipient(recipient_id), from: sender(@note.author_id),
subject: subject("#{@commit.title} (#{@commit.short_id})")) to: recipient(recipient_id),
end subject: subject("#{@commit.title} (#{@commit.short_id})"))
end end
def note_issue_email(recipient_id, note_id) def note_issue_email(recipient_id, note_id)
note_mail_with_notification(note_id, recipient_id) do setup_note_mail(note_id, recipient_id)
@issue = @note.noteable
@target_url = namespace_project_issue_url(*note_target_url_options) @issue = @note.noteable
mail_answer_thread(@issue, note_thread_options(recipient_id)) @target_url = namespace_project_issue_url(*note_target_url_options)
end mail_answer_thread(@issue, note_thread_options(recipient_id))
end end
def note_merge_request_email(recipient_id, note_id) def note_merge_request_email(recipient_id, note_id)
note_mail_with_notification(note_id, recipient_id) do setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable
@target_url = namespace_project_merge_request_url(*note_target_url_options) @merge_request = @note.noteable
mail_answer_thread(@merge_request, note_thread_options(recipient_id)) @target_url = namespace_project_merge_request_url(*note_target_url_options)
end mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end end
private private
...@@ -42,13 +42,11 @@ module Emails ...@@ -42,13 +42,11 @@ module Emails
} }
end end
def note_mail_with_notification(note_id, recipient_id) def setup_note_mail(note_id, recipient_id)
@note = Note.find(note_id) @note = Note.find(note_id)
@project = @note.project @project = @note.project
yield @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
SentNotification.record_note(@note, recipient_id, reply_key)
end end
end end
end end
...@@ -107,10 +107,9 @@ class Notify < BaseMailer ...@@ -107,10 +107,9 @@ class Notify < BaseMailer
end end
headers["X-GitLab-#{model.class.name}-ID"] = model.id headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
if reply_key if Gitlab::IncomingEmail.enabled?
headers['X-GitLab-Reply-Key'] = reply_key
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.name_with_namespace
......
...@@ -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,21 +294,18 @@ module Ci ...@@ -291,21 +294,18 @@ 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
......
...@@ -56,6 +56,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -56,6 +56,8 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) } scope :for_ref, ->(ref) { where(ref: ref) }
AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
state_machine :status, initial: :pending do state_machine :status, initial: :pending do
event :run do event :run do
transition pending: :running transition pending: :running
...@@ -131,7 +133,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -131,7 +133,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
...@@ -119,6 +119,12 @@ module Issuable ...@@ -119,6 +119,12 @@ module Issuable
update(subscribed: !subscribed?(user)) update(subscribed: !subscribed?(user))
end end
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: false)
end
def to_hook_data(user) def to_hook_data(user)
{ {
object_kind: self.class.name.underscore, object_kind: self.class.name.underscore,
......
...@@ -38,6 +38,10 @@ class ProjectWiki ...@@ -38,6 +38,10 @@ class ProjectWiki
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end end
def wiki_base_path
["/", @project.path_with_namespace, "/wikis"].join('')
end
# Returns the Gollum::Wiki object. # Returns the Gollum::Wiki object.
def wiki def wiki
@wiki ||= begin @wiki ||= begin
......
...@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base ...@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base
class << self class << self
def reply_key def reply_key
return nil unless Gitlab::IncomingEmail.enabled?
SecureRandom.hex(16) SecureRandom.hex(16)
end end
...@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base ...@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base
def record_note(note, recipient_id, reply_key, params = {}) def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code params[:line_code] = note.line_code
record(note.noteable, recipient_id, reply_key, params) record(note.noteable, recipient_id, reply_key, params)
end end
end end
def unsubscribable?
!for_commit?
end
def for_commit? def for_commit?
noteable_type == "Commit" noteable_type == "Commit"
end end
...@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base ...@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base
super super
end end
end end
def to_param
self.reply_key
end
end end
...@@ -169,7 +169,7 @@ class WikiPage ...@@ -169,7 +169,7 @@ class WikiPage
private private
def set_attributes def set_attributes
attributes[:slug] = @page.escaped_url_path attributes[:slug] = @page.url_path
attributes[:title] = @page.title attributes[:title] = @page.title
attributes[:format] = @page.format attributes[:format] = @page.format
end end
......
...@@ -44,7 +44,7 @@ module MergeRequests ...@@ -44,7 +44,7 @@ module MergeRequests
def after_merge def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch] if params[:should_remove_source_branch].present?
DeleteBranchService.new(@merge_request.source_project, current_user). DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch) execute(merge_request.source_branch)
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 %li{class: "#{'active' unless params[:filter]}"}
%ul.nav.nav-pills.nav-stacked = link_to admin_users_path do
%li{class: "#{'active' unless params[:filter]}"} Active
= link_to admin_users_path do %small.badge= number_with_delimiter(User.active.count)
Active %li{class: "#{'active' if params[:filter] == "admins"}"}
%small.pull-right= number_with_delimiter(User.active.count) = link_to admin_users_path(filter: "admins") do
%li{class: "#{'active' if params[:filter] == "admins"}"} Admins
= link_to admin_users_path(filter: "admins") do %small.badge= number_with_delimiter(User.admins.count)
Admins %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
%small.pull-right= number_with_delimiter(User.admins.count) = link_to admin_users_path(filter: 'two_factor_enabled') do
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} 2FA Enabled
= link_to admin_users_path(filter: 'two_factor_enabled') do %small.badge= number_with_delimiter(User.with_two_factor.count)
2FA Enabled %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
%small.pull-right= number_with_delimiter(User.with_two_factor.count) = link_to admin_users_path(filter: 'two_factor_disabled') do
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} 2FA Disabled
= link_to admin_users_path(filter: 'two_factor_disabled') do %small.badge= number_with_delimiter(User.without_two_factor.count)
2FA Disabled %li{class: "#{'active' if params[:filter] == "blocked"}"}
%small.pull-right= number_with_delimiter(User.without_two_factor.count) = link_to admin_users_path(filter: "blocked") do
%li{class: "#{'active' if params[:filter] == "blocked"}"} Blocked
= link_to admin_users_path(filter: "blocked") do %small.badge= number_with_delimiter(User.blocked.count)
Blocked %li{class: "#{'active' if params[:filter] == "wop"}"}
%small.pull-right= number_with_delimiter(User.blocked.count) = link_to admin_users_path(filter: "wop") do
%li{class: "#{'active' if params[:filter] == "wop"}"} Without projects
= link_to admin_users_path(filter: "wop") do %small.badge= number_with_delimiter(User.without_projects.count)
Without projects
%small.pull-right= number_with_delimiter(User.without_projects.count)
%hr
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
%hr
= link_to 'Reset', admin_users_path, class: "btn btn-cancel"
%section.col-md-9 .gray-content-block.second-block
.panel.panel-default .pull-right
.panel-heading .dropdown.inline
Users (#{number_with_delimiter(@users.total_count)}) %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
.panel-head-actions %span.light sort:
.dropdown.inline - if @sort.present?
%a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} = sort_options_hash[@sort]
%span.light sort: - else
- if @sort.present? = sort_title_name
= sort_options_hash[@sort] %b.caret
- else %ul.dropdown-menu
= sort_title_name
%b.caret
%ul.dropdown-menu
%li
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
%ul.well-list
- @users.each do |user|
%li %li
.list-item-name = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
- if user.blocked? = sort_title_name
%i.fa.fa-lock.cred = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
.panel.panel-default
%ul.well-list
- @users.each do |user|
%li
.list-item-name
- if user.blocked?
%i.fa.fa-lock.cred
- else
%i.fa.fa-user.cgreen
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
- if user == current_user
%span.cred It's you!
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
.pull-right
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- unless user == current_user
- if user.ldap_blocked?
= link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
%i.fa.fa-lock
Unblock
- elsif user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else - else
%i.fa.fa-user.cgreen = 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'
= link_to user.name, [:admin, user] - if user.access_locked?
- if user.admin? = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
%strong.cred (Admin) - if user.can_be_removed?
- if user == current_user = 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'
%span.cred It's you! = paginate @users, theme: "gitlab"
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
.pull-right
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- unless user == current_user
- if user.ldap_blocked?
= link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
%i.fa.fa-lock
Unblock
- elsif user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
= link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
- if user.access_locked?
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- if user.can_be_removed?
= link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
= paginate @users, theme: "gitlab"
.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,32 +3,36 @@ ...@@ -3,32 +3,36 @@
= 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]) }
All = link_to dashboard_snippets_path do
%span.badge All
= current_user.snippets.count %span.badge
= current_user.snippets.count
= link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
Private %li{ class: ("active" if params[:scope] == "are_private") }
%span.badge = link_to dashboard_snippets_path(scope: 'are_private') do
= current_user.snippets.are_private.count Private
%span.badge
= link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do = current_user.snippets.are_private.count
Internal
%span.badge %li{ class: ("active" if params[:scope] == "are_internal") }
= current_user.snippets.are_internal.count = link_to dashboard_snippets_path(scope: 'are_internal') do
Internal
= link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do %span.badge
Public = current_user.snippets.are_internal.count
%span.badge
= current_user.snippets.are_public.count %li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public
%span.badge
= current_user.snippets.are_public.count
= render 'snippets/snippets' = render 'snippets/snippets'
...@@ -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
- unless can?(current_user, :read_group, @group) - unless can?(current_user, :read_group, @group)
- @disable_search_panel = true - @disable_search_panel = true
...@@ -25,8 +27,8 @@ ...@@ -25,8 +27,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,20 +37,22 @@ ...@@ -35,20 +37,22 @@
= link_to "#projects", 'data-toggle' => 'tab' do = link_to "#projects", 'data-toggle' => 'tab' do
Projects Projects
.tab-content - if can?(current_user, :read_group, @group)
.tab-pane.active#activity %div{ class: container_class }
.gray-content-block.activity-filter-block .tab-content
- if current_user .tab-pane.active#activity
= render "events/event_last_push", event: @last_push .activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
= render 'shared/event_filter' = render 'shared/event_filter'
.content_list .content_list
= spinner = spinner
.tab-pane#projects .tab-pane#projects
= 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
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
%br %br
-# Don't link the host is the line below, one link in the email is easier to quickly click than two. -# Don't link the host is the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
If you'd like to receive fewer emails, you can adjust your notification settings. If you'd like to receive fewer emails, you can
- if @sent_notification && @sent_notification.unsubscribable?
= link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
from this thread or
adjust your notification settings.
= email_action @target_url = email_action @target_url
\ No newline at end of file
...@@ -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
......
...@@ -44,13 +44,16 @@ ...@@ -44,13 +44,16 @@
= render 'projects/buttons/star' = render 'projects/buttons/star'
= render 'projects/buttons/fork' = render 'projects/buttons/fork'
= render "shared/clone_panel" .clone-row
.project-clone-holder
= render "shared/clone_panel"
.split-repo-buttons .split-repo-buttons
= render "projects/buttons/download" .btn-group.pull-left
= render 'projects/buttons/dropdown' = render "projects/buttons/download"
= render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications' = render 'projects/buttons/notifications'
:javascript :javascript
new Star(); new Star();
.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
......
.zennable .zennable
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
= f.text_area attr, class: classes = f.text_area attr, class: classes
- else - else
......
%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')
......
...@@ -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
......
- if current_user - if current_user
%span.dropdown .btn-group
%a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"} %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus') = icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project) - if can?(current_user, :create_issue, @project)
......
%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?
......
%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
= 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 +10,10 @@ ...@@ -8,11 +10,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 +58,17 @@ ...@@ -57,15 +58,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?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
- if @repository.commit - if @repository.commit
.content-block.second-block.white .content-block.second-block.white
= render 'projects/last_commit', commit: @repository.commit, project: @project %div{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"} %div{class: "project-show-#{default_project_view}"}
= render default_project_view = render default_project_view
%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)
......
...@@ -5,12 +5,9 @@ ...@@ -5,12 +5,9 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page %h3.page-title New Wiki Page
.modal-body .modal-body
= label_tag :new_wiki_path do .form-group
%span Page slug = label_tag :new_wiki_path do
= text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) %span Page slug
%p.hidden.text-danger{data: { error: "slug" }} = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
%p.hint
Please don't use spaces.
.form-actions .form-actions
= link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create' = link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
...@@ -3,14 +3,12 @@ ...@@ -3,14 +3,12 @@
= render 'nav' = render 'nav'
.gray-content-block .gray-content-block
.row %span.oneline
.col-sm-6 Git access for
%h3.page-title.oneline %strong= @project_wiki.path_with_namespace
Git access for
%strong= @project_wiki.path_with_namespace
.col-sm-6 .pull-right
= render "shared/clone_panel", project: @project_wiki = render "shared/clone_panel", project: @project_wiki
.git-empty.prepend-top-default .git-empty.prepend-top-default
%fieldset %fieldset
......
%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'
- project = project || @project - project = project || @project
.git-clone-holder .git-clone-holder.input-group
.btn-group.clone-options .input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'} %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span %span
= default_clone_protocol.upcase = default_clone_protocol.upcase
......
.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
......
.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
......
...@@ -69,15 +69,16 @@ ...@@ -69,15 +69,16 @@
You're not receiving notifications from this thread. You're not receiving notifications from this thread.
.subscribed{class: ( 'hidden' unless subscribed )} .subscribed{class: ( 'hidden' unless subscribed )}
You're receiving notifications because you're subscribed to this thread. You're receiving notifications because you're subscribed to this thread.
- project_ref = cross_project_reference(@project, issuable) - project_ref = cross_project_reference(@project, issuable)
.block .block
.title .title
.cross-project-reference .cross-project-reference
%span#cross-project-reference %span
Reference: Reference:
%a{href: '#', title:project_ref} %cite{title: project_ref}
= project_ref = project_ref
= clipboard_button(clipboard_target: 'span#cross-project-reference') = clipboard_button(clipboard_text: project_ref)
:javascript :javascript
new Subscription("#{toggle_subscription_path(issuable)}"); new Subscription("#{toggle_subscription_path(issuable)}");
......
- 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
= 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 +9,25 @@ ...@@ -8,6 +9,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,74 +67,56 @@ ...@@ -47,74 +67,56 @@
= icon('map-marker') = icon('map-marker')
= @user.location = @user.location
%ul.nav-links.center
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups
- if @contributed_projects.present?
%li
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects
- if @projects.present?
%li
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects
.cover-controls %div{ class: container_class }
- if @user == current_user .tab-content
= link_to profile_path, class: 'btn btn-gray' do .tab-pane.active#activity
= icon('pencil') .gray-content-block.white.second-block
- elsif current_user %div{ class: container_class }
%span.report-abuse .user-calendar
- if @user.abuse_report %h4.center.light
%button.btn.btn-danger{ title: 'Already reported for abuse', %i.fa.fa-spinner.fa-spin
data: { toggle: 'tooltip', placement: 'left', container: 'body' }} .user-calendar-activities
= 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
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups
- if @contributed_projects.present?
%li
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects
- if @projects.present?
%li
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects
.tab-content .content_list
.tab-pane.active#activity = spinner
.content_list
= spinner
- if @groups.any? - if @groups.any?
.tab-pane#groups .tab-pane#groups
%ul.content-list %ul.content-list
- @groups.each do |group| - @groups.each do |group|
= render 'shared/groups/group', group: group = render 'shared/groups/group', group: group
- if @contributed_projects.present? - if @contributed_projects.present?
.tab-pane#contributed .tab-pane#contributed
.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
.personal-projects .personal-projects
= render 'shared/projects/list', = render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse, projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true projects_limit: 10, stars: true, avatar: true
:javascript :javascript
$(".user-calendar").load("#{user_calendar_path}"); $(".user-calendar").load("#{user_calendar_path}");
...@@ -88,6 +88,12 @@ Rails.application.routes.draw do ...@@ -88,6 +88,12 @@ Rails.application.routes.draw do
end end
end end
resources :sent_notifications, only: [], constraints: { id: /\h{32}/ } do
member do
get :unsubscribe
end
end
# Spam reports # Spam reports
resources :abuse_reports, only: [:new, :create] resources :abuse_reports, only: [:new, :create]
...@@ -513,7 +519,7 @@ Rails.application.routes.draw do ...@@ -513,7 +519,7 @@ Rails.application.routes.draw do
end end
end end
WIKI_SLUG_ID = { id: /[a-zA-Z.0-9_\-\/]+/ } unless defined? WIKI_SLUG_ID WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do scope do
# Order matters to give priority to these matches # Order matters to give priority to these matches
...@@ -604,9 +610,14 @@ Rails.application.routes.draw do ...@@ -604,9 +610,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)
- [Builds](builds.md)
- [Build triggers](build_triggers.md) - [Build triggers](build_triggers.md)
- [Build Variables](build_variables.md) - [Build Variables](build_variables.md)
......
# Builds API
## List project builds
Get a list of builds in a project.
```
GET /projects/:id/builds
```
### Parameters
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| id | integer | yes | The ID of a project |
| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
### Example of request
```
curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
```
### Example of response
```json
[
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.802Z",
"download_url": null,
"finished_at": "2015-12-24T17:54:27.895Z",
"id": 7,
"name": "teaspoon",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:27.722Z",
"status": "failed",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"website_url": ""
}
},
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.727Z",
"download_url": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
"name": "spinach:other",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:24.729Z",
"status": "failed",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"website_url": ""
}
}
]
```
## List commit builds
Get a list of builds for specific commit in a project.
```
GET /projects/:id/repository/commits/:sha/builds
```
### Parameters
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| id | integer | yes | The ID of a project |
| sha | string | yes | The SHA id of a commit |
| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
### Example of request
```
curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
```
### Example of response
```json
[
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
"download_url": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
"name": "rubocop",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": null,
"status": "canceled",
"tag": false,
"user": null
},
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.957Z",
"download_url": null,
"finished_at": "2015-12-24T17:54:33.913Z",
"id": 9,
"name": "brakeman",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:33.727Z",
"status": "failed",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"website_url": ""
}
}
]
```
## Get a single build
Get a single build of a project
```
GET /projects/:id/builds/:build_id
```
### Parameters
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| id | integer | yes | The ID of a project |
| build\_id | integer | yes | The ID of a build |
### Example of request
```
curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
```
### Example of response
```json
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.880Z",
"download_url": null,
"finished_at": "2015-12-24T17:54:31.198Z",
"id": 8,
"name": "rubocop",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:30.733Z",
"status": "failed",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2015-12-21T13:14:24.077Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://gitlab.dev/u/root",
"website_url": ""
}
}
```
## Cancel a build
Cancel a single build of a project
```
POST /projects/:id/builds/:build_id/cancel
```
### Parameters
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| id | integer | yes | The ID of a project |
| build\_id | integer | yes | The ID of a build |
### Example of request
```
curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
```
### Example of response
```json
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
"download_url": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
"name": "rubocop",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": null,
"status": "canceled",
"tag": false,
"user": null
}
```
## Retry a build
Retry a single build of a project
```
POST /projects/:id/builds/:build_id/retry
```
### Parameters
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| id | integer | yes | The ID of a project |
| build\_id | integer | yes | The ID of a build |
### Example of request
```
curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
```
### Example of response
```json
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
"download_url": null,
"finished_at": null,
"id": 69,
"name": "rubocop",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": null,
"status": "pending",
"tag": false,
"user": null
}
```
...@@ -74,10 +74,11 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -74,10 +74,11 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`. As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Reconfigure GitLab for the changes to take effect: 1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh ```sh
sudo gitlab-ctl reconfigure sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart mailroom
``` ```
1. Verify that everything is configured correctly: 1. Verify that everything is configured correctly:
......
...@@ -84,7 +84,12 @@ The instructions make the assumption that you will be using the email address `i ...@@ -84,7 +84,12 @@ The instructions make the assumption that you will be using the email address `i
quit quit
``` ```
(Note: The `.` is a literal period on its own line) _**Note:** The `.` is a literal period on its own line._
_**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
then your Postfix `my_network` configuration is not correct. The error will
say 'Temporary lookup failure'. See
[Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
1. Check if the `incoming` user received the email: 1. Check if the `incoming` user received the email:
...@@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo ...@@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
1. Test the new setup: 1. Test the new setup:
1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_. 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
2. Check if the `incoming` user received the email: 1. Check if the `incoming` user received the email:
```sh ```sh
su - incoming su - incoming
...@@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo ...@@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
q q
``` ```
_**Note:** If `mail` returns an error `Maildir: Is a directory` then your
version of `mail` doesn't support Maildir style mailboxes. Install
`heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
try the above steps again, substituting `heirloom-mailx` for the `mail`
command._
1. Log out of the `incoming` account and go back to being `root`: 1. Log out of the `incoming` account and go back to being `root`:
```sh ```sh
......
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
...@@ -69,11 +69,6 @@ Feature: Project Wiki ...@@ -69,11 +69,6 @@ Feature: Project Wiki
And I click on the "Pages" button And I click on the "Pages" button
Then I should see non-escaped link in the pages list Then I should see non-escaped link in the pages list
@javascript
Scenario: Creating an invalid new page
Given I create a New page with an invalid name
Then I should see an error message
@javascript @javascript
Scenario: Edit Wiki page that has a path Scenario: Edit Wiki page that has a path
Given I create a New page with paths Given I create a New page with paths
......
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
...@@ -132,16 +132,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps ...@@ -132,16 +132,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(current_path).to include 'one/two/three' expect(current_path).to include 'one/two/three'
end end
step 'I create a New page with an invalid name' do
click_on 'New Page'
fill_in 'Page slug', with: 'invalid name'
click_on 'Create Page'
end
step 'I should see an error message' do
expect(page).to have_content "The page slug is invalid"
end
step 'I should see non-escaped link in the pages list' do step 'I should see non-escaped link in the pages list' do
expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']") expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
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,6 +54,7 @@ module API ...@@ -54,6 +54,7 @@ module API
mount Keys mount Keys
mount Tags mount Tags
mount Triggers mount Triggers
mount Builds
mount Variables mount Variables
end end
end end
module API
# Projects builds API
class Builds < Grape::API
before { authenticate! }
resource :projects do
# Get a project builds
#
# Parameters:
# id (required) - The ID of a project
# scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
# if none provided showing all builds)
# Example Request:
# GET /projects/:id/builds
get ':id/builds' do
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
present paginate(builds), with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
# Get builds for a specific commit of a project
#
# Parameters:
# id (required) - The ID of a project
# sha (required) - The SHA id of a commit
# scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
# if none provided showing all builds)
# Example Request:
# GET /projects/:id/repository/commits/:sha/builds
get ':id/repository/commits/:sha/builds' do
commit = user_project.ci_commits.find_by_sha(params[:sha])
return not_found! unless commit
builds = commit.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
present paginate(builds), with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
# Get a specific build of a project
#
# Parameters:
# id (required) - The ID of a project
# build_id (required) - The ID of a build
# Example Request:
# GET /projects/:id/builds/:build_id
get ':id/builds/:build_id' do
build = get_build(params[:build_id])
return not_found!(build) unless build
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
# Get a trace of a specific build of a project
#
# Parameters:
# id (required) - The ID of a project
# build_id (required) - The ID of a build
# Example Request:
# GET /projects/:id/build/:build_id/trace
#
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
get ':id/builds/:build_id/trace' do
build = get_build(params[:build_id])
return not_found!(build) unless build
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
env['api.format'] = :binary
trace = build.trace
body trace
end
# Cancel a specific build of a project
#
# parameters:
# id (required) - the id of a project
# build_id (required) - the id of a build
# example request:
# post /projects/:id/build/:build_id/cancel
post ':id/builds/:build_id/cancel' do
authorize_manage_builds!
build = get_build(params[:build_id])
return not_found!(build) unless build
build.cancel
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
# Retry a specific build of a project
#
# parameters:
# id (required) - the id of a project
# build_id (required) - the id of a build
# example request:
# post /projects/:id/build/:build_id/retry
post ':id/builds/:build_id/retry' do
authorize_manage_builds!
build = get_build(params[:build_id])
return forbidden!('Build is not retryable') unless build && build.retryable?
build = Ci::Build.retry(build)
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
end
helpers do
def get_build(id)
user_project.builds.find_by(id: id.to_i)
end
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
available_statuses = ::CommitStatus::AVAILABLE_STATUSES
scope =
if scope.is_a?(String)
[scope]
elsif scope.is_a?(Hashie::Mash)
scope.values
else
['unknown']
end
unknown = scope - available_statuses
render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
builds.where(status: available_statuses && scope)
end
def authorize_manage_builds!
authorize! :manage_builds, user_project
end
end
end
end
...@@ -367,6 +367,33 @@ module API ...@@ -367,6 +367,33 @@ module API
expose :id, :variables expose :id, :variables
end end
class Runner < Grape::Entity
expose :id
expose :description
expose :active
expose :is_shared
expose :name
end
class Build < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
# TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API
# for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255)
expose :download_url do |repo_obj, options|
if options[:user_can_download_artifacts]
repo_obj.download_url
end
end
expose :commit, with: RepoCommit do |repo_obj, _options|
if repo_obj.respond_to?(:commit)
repo_obj.commit.commit_data
end
end
expose :runner, with: Runner
end
class Trigger < Grape::Entity class Trigger < Grape::Entity
expose :token, :created_at, :updated_at, :deleted_at, :last_used expose :token, :created_at, :updated_at, :deleted_at, :last_used
end end
......
...@@ -97,11 +97,9 @@ module API ...@@ -97,11 +97,9 @@ module API
end end
def paginate(relation) def paginate(relation)
per_page = params[:per_page].to_i relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
paginated = relation.page(params[:page]).per(per_page) add_pagination_headers(data)
add_pagination_headers(paginated, per_page) end
paginated
end end
def authenticate! def authenticate!
...@@ -289,12 +287,14 @@ module API ...@@ -289,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)
...@@ -327,16 +327,26 @@ module API ...@@ -327,16 +327,26 @@ module API
private private
def add_pagination_headers(paginated, per_page) def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s
header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s
header 'X-Prev-Page', paginated_data.prev_page.to_s
header 'Link', pagination_links(paginated_data)
end
def pagination_links(paginated_data)
request_url = request.url.split('?').first request_url = request.url.split('?').first
links = [] links = []
links << %(<#{request_url}?page=#{paginated.current_page - 1}&per_page=#{per_page}>; rel="prev") unless paginated.first_page? links << %(<#{request_url}?page=#{paginated_data.current_page - 1}&per_page=#{paginated_data.limit_value}>; rel="prev") unless paginated_data.first_page?
links << %(<#{request_url}?page=#{paginated.current_page + 1}&per_page=#{per_page}>; rel="next") unless paginated.last_page? links << %(<#{request_url}?page=#{paginated_data.current_page + 1}&per_page=#{paginated_data.limit_value}>; rel="next") unless paginated_data.last_page?
links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first") links << %(<#{request_url}?page=1&per_page=#{paginated_data.limit_value}>; rel="first")
links << %(<#{request_url}?page=#{paginated.total_pages}&per_page=#{per_page}>; rel="last") links << %(<#{request_url}?page=#{paginated_data.total_pages}&per_page=#{paginated_data.limit_value}>; rel="last")
header 'Link', links.join(', ') links.join(', ')
end end
def abilities def abilities
......
require 'banzai'
module Banzai module Banzai
# Common methods for ReferenceFilters that support an optional cross-project # Common methods for ReferenceFilters that support an optional cross-project
# reference. # reference.
......
require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/output_safety'
require 'banzai'
module Banzai module Banzai
module Filter module Filter
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# Issues, Merge Requests, Snippets, Commits and Commit Ranges share # Issues, Merge Requests, Snippets, Commits and Commit Ranges share
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'uri' require 'uri'
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces commit range references with links. # HTML filter that replaces commit range references with links.
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces commit references with links. # HTML filter that replaces commit references with links.
......
require 'action_controller' require 'action_controller'
require 'banzai'
require 'gitlab_emoji' require 'gitlab_emoji'
require 'html/pipeline/filter' require 'html/pipeline/filter'
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces external issue tracker references with links. # HTML filter that replaces external issue tracker references with links.
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Banzai module Banzai
......
require 'banzai'
require 'html/pipeline/filter'
module Banzai
module Filter
# HTML Filter for parsing Gollum's tags in HTML. It's only parses the
# following tags:
#
# - Link to internal pages:
#
# * [[Bug Reports]]
# * [[How to Contribute|Contributing]]
#
# - Link to external resources:
#
# * [[http://en.wikipedia.org/wiki/Git_(software)]]
# * [[Git|http://en.wikipedia.org/wiki/Git_(software)]]
#
# - Link internal images, the special attributes will be ignored:
#
# * [[images/logo.png]]
# * [[images/logo.png|alt=Logo]]
#
# - Link external images, the special attributes will be ignored:
#
# * [[http://example.com/images/logo.png]]
# * [[http://example.com/images/logo.png|alt=Logo]]
#
# Based on Gollum::Filter::Tags
#
# Context options:
# :project_wiki (required) - Current project wiki.
#
class GollumTagsFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper
# Pattern to match tags content that should be parsed in HTML.
#
# Gollum's tags have been made to resemble the tags of other markups,
# especially MediaWiki. The basic syntax is:
#
# [[tag]]
#
# Some tags will accept attributes which are separated by pipe
# symbols.Some attributes must precede the tag and some must follow it:
#
# [[prefix-attribute|tag]]
# [[tag|suffix-attribute]]
#
# See https://github.com/gollum/gollum/wiki
#
# Rubular: http://rubular.com/r/7dQnE5CUCH
TAGS_PATTERN = %r{\[\[(.+?)\]\]}
# Pattern to match allowed image extensions
ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i
def call
search_text_nodes(doc).each do |node|
content = node.content
next unless content.match(TAGS_PATTERN)
html = process_tag($1)
if html && html != node.content
node.replace(html)
end
end
doc
end
private
# Process a single tag into its final HTML form.
#
# tag - The String tag contents (the stuff inside the double brackets).
#
# Returns the String HTML version of the tag.
def process_tag(tag)
parts = tag.split('|')
return if parts.size.zero?
process_image_tag(parts) || process_page_link_tag(parts)
end
# Attempt to process the tag as an image tag.
#
# tag - The String tag contents (the stuff inside the double brackets).
#
# Returns the String HTML if the tag is a valid image tag or nil
# if it is not.
def process_image_tag(parts)
content = parts[0].strip
return unless image?(content)
if url?(content)
path = content
elsif file = project_wiki.find_file(content)
path = ::File.join project_wiki_base_path, file.path
end
if path
content_tag(:img, nil, src: path)
end
end
def image?(path)
path =~ ALLOWED_IMAGE_EXTENSIONS
end
def url?(path)
path.start_with?(*%w(http https))
end
# Attempt to process the tag as a page link tag.
#
# tag - The String tag contents (the stuff inside the double brackets).
#
# Returns the String HTML if the tag is a valid page link tag or nil
# if it is not.
def process_page_link_tag(parts)
if parts.size == 1
url = parts[0].strip
else
name, url = *parts.compact.map(&:strip)
end
content_tag(:a, name || url, href: url)
end
def project_wiki
context[:project_wiki]
end
def project_wiki_base_path
project_wiki && project_wiki.wiki_base_path
end
# Ensure that a :project_wiki key exists in context
#
# Note that while the key might exist, its value could be nil!
def validate
needs :project_wiki
end
end
end
end
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces issue references with links. References to # HTML filter that replaces issue references with links. References to
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces label references with links. # HTML filter that replaces label references with links.
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Banzai module Banzai
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces merge request references with links. References # HTML filter that replaces merge request references with links. References
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Banzai module Banzai
......
require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/output_safety'
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Banzai module Banzai
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Banzai module Banzai
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'uri' require 'uri'
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter' require 'html/pipeline/sanitization_filter'
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces snippet references with links. References to # HTML filter that replaces snippet references with links. References to
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet' require 'rouge/plugins/redcarpet'
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
module Banzai module Banzai
......
require 'banzai'
require 'task_list/filter' require 'task_list/filter'
module Banzai module Banzai
......
require 'banzai'
require 'html/pipeline/filter' require 'html/pipeline/filter'
require 'uri' require 'uri'
......
require 'banzai'
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces user or group references with links. # HTML filter that replaces user or group references with links.
......
require 'banzai'
module Banzai module Banzai
class LazyReference class LazyReference
def self.load(refs) def self.load(refs)
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
def self.[](name) def self.[](name)
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class AsciidocPipeline < BasePipeline class AsciidocPipeline < BasePipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class AtomPipeline < FullPipeline class AtomPipeline < FullPipeline
......
require 'banzai'
require 'html/pipeline' require 'html/pipeline'
module Banzai module Banzai
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
module CombinedPipeline module CombinedPipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class DescriptionPipeline < FullPipeline class DescriptionPipeline < FullPipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class EmailPipeline < FullPipeline class EmailPipeline < FullPipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline) class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class GfmPipeline < BasePipeline class GfmPipeline < BasePipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class NotePipeline < FullPipeline class NotePipeline < FullPipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class PlainMarkdownPipeline < BasePipeline class PlainMarkdownPipeline < BasePipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class PostProcessPipeline < BasePipeline class PostProcessPipeline < BasePipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class ReferenceExtractionPipeline < BasePipeline class ReferenceExtractionPipeline < BasePipeline
......
require 'banzai'
module Banzai module Banzai
module Pipeline module Pipeline
class SingleLinePipeline < GfmPipeline class SingleLinePipeline < GfmPipeline
......
require 'banzai'
module Banzai
module Pipeline
class WikiPipeline < FullPipeline
def self.filters
super.insert(1, Filter::GollumTagsFilter)
end
end
end
end
require 'banzai'
module Banzai module Banzai
# Extract possible GFM references from an arbitrary String for further processing. # Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor class ReferenceExtractor
......
...@@ -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
...@@ -56,8 +56,9 @@ module Gitlab ...@@ -56,8 +56,9 @@ module Gitlab
private private
def filename?(line) def filename?(line)
line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b', line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
'--- /tmp/diffy', '+++ /tmp/diffy') '+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit
'--- /tmp/diffy', '+++ /tmp/diffy')
end end
def identification_type(line) def identification_type(line)
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class Importer class Importer
include Gitlab::ShellAdapter
attr_reader :project, :client attr_reader :project, :client
def initialize(project) def initialize(project)
...@@ -12,10 +14,7 @@ module Gitlab ...@@ -12,10 +14,7 @@ module Gitlab
end end
def execute def execute
import_issues import_issues && import_pull_requests && import_wiki
import_pull_requests
true
end end
private private
...@@ -34,6 +33,10 @@ module Gitlab ...@@ -34,6 +33,10 @@ module Gitlab
end end
end end
end end
true
rescue ActiveRecord::RecordInvalid
false
end end
def import_pull_requests def import_pull_requests
...@@ -48,6 +51,10 @@ module Gitlab ...@@ -48,6 +51,10 @@ module Gitlab
import_comments_on_diff(pull_request.number, merge_request) import_comments_on_diff(pull_request.number, merge_request)
end end
end end
true
rescue ActiveRecord::RecordInvalid
false
end end
def import_comments(issue_number, noteable) def import_comments(issue_number, noteable)
...@@ -66,6 +73,18 @@ module Gitlab ...@@ -66,6 +73,18 @@ module Gitlab
noteable.notes.create!(comment.attributes) noteable.notes.create!(comment.attributes)
end end
end end
def import_wiki
unless project.wiki_enabled?
wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(wiki.path_with_namespace, wiki.import_url)
project.update_attribute(:wiki_enabled, true)
end
true
rescue Gitlab::Shell::Error
false
end
end end
end end
end end
...@@ -20,7 +20,8 @@ module Gitlab ...@@ -20,7 +20,8 @@ module Gitlab
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "github", import_type: "github",
import_source: repo.full_name, import_source: repo.full_name,
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
).execute ).execute
project.create_import_data(data: { "github_session" => session_data } ) project.create_import_data(data: { "github_session" => session_data } )
......
module Gitlab
module GithubImport
class WikiFormatter
attr_reader :project
def initialize(project)
@project = project
end
def path_with_namespace
"#{project.path_with_namespace}.wiki"
end
def import_url
project.import_url.sub(/\.git\z/, ".wiki.git")
end
end
end
end
require 'banzai'
module Gitlab module Gitlab
module Markdown module Markdown
class Pipeline class Pipeline
......
require 'banzai'
module Gitlab module Gitlab
# Extract possible GFM references from an arbitrary String for further processing. # Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor class ReferenceExtractor < Banzai::ReferenceExtractor
......
require 'rails_helper'
describe SentNotificationsController, type: :controller do
let(:user) { create(:user) }
let(:issue) { create(:issue, author: user) }
let(:sent_notification) { create(:sent_notification, noteable: issue) }
describe 'GET #unsubscribe' do
it 'returns a 404 when calling without existing id' do
get(:unsubscribe, id: '0' * 32)
expect(response.status).to be 404
end
context 'calling with id' do
it 'shows a flash message to the user' do
get(:unsubscribe, id: sent_notification.reply_key)
expect(response.status).to be 302
expect(response).to redirect_to new_user_session_path
expect(controller).to set_flash[:notice].to(/unsubscribed/).now
end
end
end
end
...@@ -212,4 +212,11 @@ FactoryGirl.define do ...@@ -212,4 +212,11 @@ FactoryGirl.define do
provider 'ldapmain' provider 'ldapmain'
extern_uid 'my-ldap-id' extern_uid 'my-ldap-id'
end end
factory :sent_notification do
project
recipient factory: :user
noteable factory: :issue
reply_key "0123456789abcdef" * 2
end
end end
...@@ -30,6 +30,7 @@ FactoryGirl.define do ...@@ -30,6 +30,7 @@ FactoryGirl.define do
name 'test' name 'test'
ref 'master' ref 'master'
tag false tag false
created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a' commands 'ls -a'
...@@ -42,6 +43,10 @@ FactoryGirl.define do ...@@ -42,6 +43,10 @@ FactoryGirl.define do
commit factory: :ci_commit commit factory: :ci_commit
trait :canceled do
status 'canceled'
end
after(:build) do |build, evaluator| after(:build) do |build, evaluator|
build.project = build.commit.project build.project = build.commit.project
end end
...@@ -54,5 +59,11 @@ FactoryGirl.define do ...@@ -54,5 +59,11 @@ FactoryGirl.define do
factory :ci_build_tag do factory :ci_build_tag do
tag true tag true
end end
factory :ci_build_with_trace do
after(:create) do |build, evaluator|
build.trace = 'BUILD TRACE'
end
end
end end
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) }
......
...@@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do ...@@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do
end end
end end
context 'default pipeline' do before(:all) do
before(:all) do @feat = MarkdownFeature.new
@feat = MarkdownFeature.new
# `markdown` helper expects a `@project` variable # `markdown` helper expects a `@project` variable
@project = @feat.project @project = @feat.project
end
context 'default pipeline' do
before(:all) do
@html = markdown(@feat.raw_markdown) @html = markdown(@feat.raw_markdown)
end end
...@@ -221,6 +223,57 @@ describe 'GitLab Markdown', feature: true do ...@@ -221,6 +223,57 @@ describe 'GitLab Markdown', feature: true do
end end
end end
context 'wiki pipeline' do
before do
@project_wiki = @feat.project_wiki
file = Gollum::File.new(@project_wiki.wiki)
expect(file).to receive(:path).and_return('images/example.jpg')
expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
@html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
end
it_behaves_like 'all pipelines'
it 'includes RelativeLinkFilter' do
expect(doc).not_to parse_relative_links
end
it 'includes EmojiFilter' do
expect(doc).to parse_emoji
end
it 'includes TableOfContentsFilter' do
expect(doc).to create_header_links
end
it 'includes AutolinkFilter' do
expect(doc).to create_autolinks
end
it 'includes all reference filters' do
aggregate_failures do
expect(doc).to reference_users
expect(doc).to reference_issues
expect(doc).to reference_merge_requests
expect(doc).to reference_snippets
expect(doc).to reference_commit_ranges
expect(doc).to reference_commits
expect(doc).to reference_labels
expect(doc).to reference_milestones
end
end
it 'includes TaskListFilter' do
expect(doc).to parse_task_lists
end
it 'includes GollumTagsFilter' do
expect(doc).to parse_gollum_tags
end
end
# Fake a `current_user` helper # Fake a `current_user` helper
def current_user def current_user
@feat.user @feat.user
......
...@@ -230,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e ...@@ -230,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [ ] Incomplete sub-task 2 - [ ] Incomplete sub-task 2
- [x] Complete sub-task 1 - [x] Complete sub-task 1
- [X] Complete task 2 - [X] Complete task 2
#### Gollum Tags
- [[linked-resource]]
- [[link-text|linked-resource]]
- [[http://example.com]]
- [[link-text|http://example.com/pdfs/gollum.pdf]]
- [[images/example.jpg]]
- [[http://example.com/images/example.jpg]]
...@@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do ...@@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do
before do before do
@wiki = double('WikiPage') @wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content') allow(@wiki).to receive(:content).and_return('wiki content')
helper.instance_variable_set(:@project_wiki, @wiki)
end end
it "should use GitLab Flavored Markdown for markdown files" do it "should use Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown) allow(@wiki).to receive(:format).and_return(:markdown)
expect(helper).to receive(:markdown).with('wiki content') expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki)
helper.render_wiki_content(@wiki) helper.render_wiki_content(@wiki)
end end
......
require 'spec_helper'
describe Banzai::Filter::GollumTagsFilter, lib: true do
include FilterSpecHelper
let(:project) { create(:project) }
let(:user) { double }
let(:project_wiki) { ProjectWiki.new(project, user) }
describe 'validation' do
it 'ensure that a :project_wiki key exists in context' do
expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :project_wiki"
end
end
context 'linking internal images' do
it 'creates img tag if image exists' do
file = Gollum::File.new(project_wiki.wiki)
expect(file).to receive(:path).and_return('images/image.jpg')
expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file)
tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
end
it 'does not creates img tag if image does not exist' do
expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(nil)
tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.css('img').size).to eq 0
end
end
context 'linking external images' do
it 'creates img tag for valid URL' do
tag = '[[http://example.com/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg"
end
it 'does not creates img tag for invalid URL' do
tag = '[[http://example.com/image.pdf]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.css('img').size).to eq 0
end
end
context 'linking external resources' do
it "the created link's text will be equal to the resource's text" do
tag = '[[http://example.com]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('a').text).to eq 'http://example.com'
expect(doc.at_css('a')['href']).to eq 'http://example.com'
end
it "the created link's text will be link-text" do
tag = '[[link-text|http://example.com/pdfs/gollum.pdf]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('a').text).to eq 'link-text'
expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf'
end
end
context 'linking internal resources' do
it "the created link's text will be equal to the resource's text" do
tag = '[[wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('a').text).to eq 'wiki-slug'
expect(doc.at_css('a')['href']).to eq 'wiki-slug'
end
it "the created link's text will be link-text" do
tag = '[[link-text|wiki-slug]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
expect(doc.at_css('a').text).to eq 'link-text'
expect(doc.at_css('a')['href']).to eq 'wiki-slug'
end
end
end
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
require 'spec_helper'
describe Gitlab::GithubImport::WikiFormatter, lib: true do
let(:project) do
create(:project, namespace: create(:namespace, path: 'gitlabhq'),
import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
end
subject(:wiki) { described_class.new(project)}
describe '#path_with_namespace' do
it 'appends .wiki to project path' do
expect(wiki.path_with_namespace).to eq 'gitlabhq/gitlabhq.wiki'
end
end
describe '#import_url' do
it 'returns URL of the wiki repository' do
expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
end
end
end
...@@ -104,6 +104,14 @@ describe Notify do ...@@ -104,6 +104,14 @@ describe Notify do
it { is_expected.to have_body_text /View Commit/ } it { is_expected.to have_body_text /View Commit/ }
end end
shared_examples 'an unsubscribeable thread' do
it { is_expected.to have_body_text /unsubscribe/ }
end
shared_examples "a user cannot unsubscribe through footer link" do
it { is_expected.not_to have_body_text /unsubscribe/ }
end
describe 'for new users, the email' do describe 'for new users, the email' do
let(:example_site_path) { root_path } let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
...@@ -115,6 +123,7 @@ describe Notify do ...@@ -115,6 +123,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address it_behaves_like 'a new user email', new_user_address
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'contains the password text' do it 'contains the password text' do
is_expected.to have_body_text /Click here to set your password/ is_expected.to have_body_text /Click here to set your password/
...@@ -134,7 +143,6 @@ describe Notify do ...@@ -134,7 +143,6 @@ describe Notify do
end end
end end
describe 'for users that signed up, the email' do describe 'for users that signed up, the email' do
let(:example_site_path) { root_path } let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
...@@ -144,6 +152,7 @@ describe Notify do ...@@ -144,6 +152,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address it_behaves_like 'a new user email', new_user_address
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'should not contain the new user\'s password' do it 'should not contain the new user\'s password' do
is_expected.not_to have_body_text /password/ is_expected.not_to have_body_text /password/
...@@ -157,6 +166,7 @@ describe Notify do ...@@ -157,6 +166,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do it 'is sent to the new user' do
is_expected.to deliver_to key.user.email is_expected.to deliver_to key.user.email
...@@ -181,6 +191,7 @@ describe Notify do ...@@ -181,6 +191,7 @@ describe Notify do
subject { Notify.new_email_email(email.id) } subject { Notify.new_email_email(email.id) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do it 'is sent to the new user' do
is_expected.to deliver_to email.user.email is_expected.to deliver_to email.user.email
...@@ -227,6 +238,7 @@ describe Notify do ...@@ -227,6 +238,7 @@ describe Notify do
it_behaves_like 'an assignee email' it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue' it_behaves_like 'an email starting a new thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
...@@ -253,6 +265,7 @@ describe Notify do ...@@ -253,6 +265,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -283,6 +296,7 @@ describe Notify do ...@@ -283,6 +296,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -319,6 +333,7 @@ describe Notify do ...@@ -319,6 +333,7 @@ describe Notify do
it_behaves_like 'an assignee email' it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request' it_behaves_like 'an email starting a new thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
...@@ -345,6 +360,7 @@ describe Notify do ...@@ -345,6 +360,7 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
it 'contains the description' do it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/ is_expected.to have_body_text /#{merge_request_with_description.description}/
...@@ -357,6 +373,7 @@ describe Notify do ...@@ -357,6 +373,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -387,6 +404,7 @@ describe Notify do ...@@ -387,6 +404,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -417,6 +435,7 @@ describe Notify do ...@@ -417,6 +435,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
it 'is sent as the merge author' do it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -446,6 +465,7 @@ describe Notify do ...@@ -446,6 +465,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /Project was moved/ is_expected.to have_subject /Project was moved/
...@@ -468,6 +488,7 @@ describe Notify do ...@@ -468,6 +488,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/ is_expected.to have_subject /Access to project was granted/
...@@ -518,6 +539,7 @@ describe Notify do ...@@ -518,6 +539,7 @@ describe Notify do
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit' it_behaves_like 'an answer to an existing thread', 'commit'
it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
...@@ -538,6 +560,7 @@ describe Notify do ...@@ -538,6 +560,7 @@ describe Notify do
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
...@@ -558,6 +581,7 @@ describe Notify do ...@@ -558,6 +581,7 @@ describe Notify do
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
...@@ -579,6 +603,7 @@ describe Notify do ...@@ -579,6 +603,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/ is_expected.to have_subject /Access to group was granted/
...@@ -607,6 +632,7 @@ describe Notify do ...@@ -607,6 +632,7 @@ describe Notify do
subject { ActionMailer::Base.deliveries.last } subject { ActionMailer::Base.deliveries.last }
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent to the new user' do it 'is sent to the new user' do
is_expected.to deliver_to 'new-email@mail.com' is_expected.to deliver_to 'new-email@mail.com'
...@@ -629,6 +655,7 @@ describe Notify do ...@@ -629,6 +655,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -657,6 +684,7 @@ describe Notify do ...@@ -657,6 +684,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -684,6 +712,7 @@ describe Notify do ...@@ -684,6 +712,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -707,6 +736,7 @@ describe Notify do ...@@ -707,6 +736,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -734,6 +764,7 @@ describe Notify do ...@@ -734,6 +764,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -839,6 +870,7 @@ describe Notify do ...@@ -839,6 +870,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
......
# == 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 }
......
...@@ -36,6 +36,13 @@ describe ProjectWiki, models: true do ...@@ -36,6 +36,13 @@ describe ProjectWiki, models: true do
end end
end end
describe "#wiki_base_path" do
it "returns the wiki base path" do
wiki_base_path = "/#{project.path_with_namespace}/wikis"
expect(subject.wiki_base_path).to eq(wiki_base_path)
end
end
describe "#wiki" do describe "#wiki" do
it "contains a Gollum::Wiki instance" do it "contains a Gollum::Wiki instance" do
expect(subject.wiki).to be_a Gollum::Wiki expect(subject.wiki).to be_a Gollum::Wiki
......
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!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) }
let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) }
let(:commit) { create(:ci_commit, project: project)}
let(:build) { create(:ci_build, commit: commit) }
let(:build_with_trace) { create(:ci_build_with_trace, commit: commit) }
let(:build_canceled) { create(:ci_build, :canceled, commit: commit) }
describe 'GET /projects/:id/builds ' do
context 'authorized user' do
it 'should return project builds' do
get api("/projects/#{project.id}/builds", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
end
it 'should filter project with one scope element' do
get api("/projects/#{project.id}/builds?scope=pending", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
end
it 'should filter project with array of scope elements' do
get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=running", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
end
it 'should respond 400 when scope contains invalid state' do
get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=unknown_status", user)
expect(response.status).to eq(400)
end
end
context 'unauthorized user' do
it 'should not return project builds' do
get api("/projects/#{project.id}/builds")
expect(response.status).to eq(401)
end
end
end
describe 'GET /projects/:id/repository/commits/:sha/builds' do
context 'authorized user' do
it 'should return project builds for specific commit' do
project.ensure_ci_commit(commit.sha)
get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
end
end
context 'unauthorized user' do
it 'should not return project builds' do
project.ensure_ci_commit(commit.sha)
get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds")
expect(response.status).to eq(401)
end
end
end
describe 'GET /projects/:id/builds/:build_id' do
context 'authorized user' do
it 'should return specific build data' do
get api("/projects/#{project.id}/builds/#{build.id}", user)
expect(response.status).to eq(200)
expect(json_response['name']).to eq('test')
end
end
context 'unauthorized user' do
it 'should not return specific build data' do
get api("/projects/#{project.id}/builds/#{build.id}")
expect(response.status).to eq(401)
end
end
end
describe 'GET /projects/:id/builds/:build_id/trace' do
context 'authorized user' do
it 'should return specific build trace' do
get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace", user)
expect(response.status).to eq(200)
expect(response.body).to eq(build_with_trace.trace)
end
end
context 'unauthorized user' do
it 'should not return specific build trace' do
get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace")
expect(response.status).to eq(401)
end
end
end
describe 'POST /projects/:id/builds/:build_id/cancel' do
context 'authorized user' do
context 'user with :manage_builds persmission' do
it 'should cancel running or pending build' do
post api("/projects/#{project.id}/builds/#{build.id}/cancel", user)
expect(response.status).to eq(201)
expect(project.builds.first.status).to eq('canceled')
end
end
context 'user without :manage_builds permission' do
it 'should not cancel build' do
post api("/projects/#{project.id}/builds/#{build.id}/cancel", user2)
expect(response.status).to eq(403)
end
end
end
context 'unauthorized user' do
it 'should not cancel build' do
post api("/projects/#{project.id}/builds/#{build.id}/cancel")
expect(response.status).to eq(401)
end
end
end
describe 'POST /projects/:id/builds/:build_id/retry' do
context 'authorized user' do
context 'user with :manage_builds persmission' do
it 'should retry non-running build' do
post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user)
expect(response.status).to eq(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
end
end
context 'user without :manage_builds permission' do
it 'should not retry build' do
post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user2)
expect(response.status).to eq(403)
end
end
end
context 'unauthorized user' do
it 'should not retry build' do
post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry")
expect(response.status).to eq(401)
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe API::API, api: true do describe API::CommitStatus, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
...@@ -12,6 +12,10 @@ describe API::API, api: true do ...@@ -12,6 +12,10 @@ describe API::API, api: true do
let(:commit_status) { create(:commit_status, commit: ci_commit) } let(:commit_status) { create(:commit_status, commit: ci_commit) }
describe "GET /projects/:id/repository/commits/:sha/statuses" do describe "GET /projects/:id/repository/commits/:sha/statuses" do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) }
end
context "reporter user" do context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } } let(:statuses_id) { json_response.map { |status| status['id'] } }
......
...@@ -32,6 +32,10 @@ describe API::API, api: true do ...@@ -32,6 +32,10 @@ describe API::API, api: true do
before { project.team << [user, :reporter] } before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do describe "GET /projects/:id/noteable/:noteable_id/notes" do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
end
context "when noteable is an Issue" do context "when noteable is an Issue" do
it "should return an array of issue notes" do it "should return an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
......
...@@ -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!
......
# Specs for paginated resources.
#
# Requires an API request:
# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
shared_examples 'a paginated resources' do
before do
# Fires the request
request
end
it 'has pagination headers' do
expect(response.headers).to include('X-Total')
expect(response.headers).to include('X-Total-Pages')
expect(response.headers).to include('X-Per-Page')
expect(response.headers).to include('X-Page')
expect(response.headers).to include('X-Next-Page')
expect(response.headers).to include('X-Prev-Page')
expect(response.headers).to include('Link')
end
end
...@@ -28,6 +28,10 @@ class MarkdownFeature ...@@ -28,6 +28,10 @@ class MarkdownFeature
end end
end end
def project_wiki
@project_wiki ||= ProjectWiki.new(project, user)
end
def issue def issue
@issue ||= create(:issue, project: project) @issue ||= create(:issue, project: project)
end end
......
...@@ -66,6 +66,24 @@ module MarkdownMatchers ...@@ -66,6 +66,24 @@ module MarkdownMatchers
end end
end end
# GollumTagsFilter
matcher :parse_gollum_tags do
def have_image(src)
have_css("img[src$='#{src}']")
end
set_default_markdown_messages
match do |actual|
expect(actual).to have_link('linked-resource', href: 'linked-resource')
expect(actual).to have_link('link-text', href: 'linked-resource')
expect(actual).to have_link('http://example.com', href: 'http://example.com')
expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf')
expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg')
expect(actual).to have_image('http://example.com/images/example.jpg')
end
end
# UserReferenceFilter # UserReferenceFilter
matcher :reference_users do matcher :reference_users do
set_default_markdown_messages set_default_markdown_messages
......
/*!
Autosize 3.0.14
license: MIT
http://www.jacklmoore.com/autosize
*/
(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports', 'module'], factory);
} else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
factory(exports, module);
} else {
var mod = {
exports: {}
};
factory(mod.exports, mod);
global.autosize = mod.exports;
}
})(this, function (exports, module) {
'use strict';
var set = typeof Set === 'function' ? new Set() : (function () {
var list = [];
return {
has: function has(key) {
return Boolean(list.indexOf(key) > -1);
},
add: function add(key) {
list.push(key);
},
'delete': function _delete(key) {
list.splice(list.indexOf(key), 1);
} };
})();
function assign(ta) {
var _ref = arguments[1] === undefined ? {} : arguments[1];
var _ref$setOverflowX = _ref.setOverflowX;
var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
var _ref$setOverflowY = _ref.setOverflowY;
var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
var heightOffset = null;
var overflowY = null;
var clientWidth = ta.clientWidth;
function init() {
var style = window.getComputedStyle(ta, null);
overflowY = style.overflowY;
if (style.resize === 'vertical') {
ta.style.resize = 'none';
} else if (style.resize === 'both') {
ta.style.resize = 'horizontal';
}
if (style.boxSizing === 'content-box') {
heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
} else {
heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
}
// Fix when a textarea is not on document body and heightOffset is Not a Number
if (isNaN(heightOffset)) {
heightOffset = 0;
}
update();
}
function changeOverflow(value) {
{
// Chrome/Safari-specific fix:
// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
// made available by removing the scrollbar. The following forces the necessary text reflow.
var width = ta.style.width;
ta.style.width = '0px';
// Force reflow:
/* jshint ignore:start */
ta.offsetWidth;
/* jshint ignore:end */
ta.style.width = width;
}
overflowY = value;
if (setOverflowY) {
ta.style.overflowY = value;
}
resize();
}
function resize() {
var htmlTop = window.pageYOffset;
var bodyTop = document.body.scrollTop;
var originalHeight = ta.style.height;
ta.style.height = 'auto';
var endHeight = ta.scrollHeight + heightOffset;
if (ta.scrollHeight === 0) {
// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
ta.style.height = originalHeight;
return;
}
ta.style.height = endHeight + 'px';
// used to check if an update is actually necessary on window.resize
clientWidth = ta.clientWidth;
// prevents scroll-position jumping
document.documentElement.scrollTop = htmlTop;
document.body.scrollTop = bodyTop;
}
function update() {
var startHeight = ta.style.height;
resize();
var style = window.getComputedStyle(ta, null);
if (style.height !== ta.style.height) {
if (overflowY !== 'visible') {
changeOverflow('visible');
}
} else {
if (overflowY !== 'hidden') {
changeOverflow('hidden');
}
}
if (startHeight !== ta.style.height) {
var evt = document.createEvent('Event');
evt.initEvent('autosize:resized', true, false);
ta.dispatchEvent(evt);
}
}
var pageResize = function pageResize() {
if (ta.clientWidth !== clientWidth) {
update();
}
};
var destroy = (function (style) {
window.removeEventListener('resize', pageResize, false);
ta.removeEventListener('input', update, false);
ta.removeEventListener('keyup', update, false);
ta.removeEventListener('autosize:destroy', destroy, false);
ta.removeEventListener('autosize:update', update, false);
set['delete'](ta);
Object.keys(style).forEach(function (key) {
ta.style[key] = style[key];
});
}).bind(ta, {
height: ta.style.height,
resize: ta.style.resize,
overflowY: ta.style.overflowY,
overflowX: ta.style.overflowX,
wordWrap: ta.style.wordWrap });
ta.addEventListener('autosize:destroy', destroy, false);
// IE9 does not fire onpropertychange or oninput for deletions,
// so binding to onkeyup to catch most of those events.
// There is no way that I know of to detect something like 'cut' in IE9.
if ('onpropertychange' in ta && 'oninput' in ta) {
ta.addEventListener('keyup', update, false);
}
window.addEventListener('resize', pageResize, false);
ta.addEventListener('input', update, false);
ta.addEventListener('autosize:update', update, false);
set.add(ta);
if (setOverflowX) {
ta.style.overflowX = 'hidden';
ta.style.wordWrap = 'break-word';
}
init();
}
function destroy(ta) {
if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
var evt = document.createEvent('Event');
evt.initEvent('autosize:destroy', true, false);
ta.dispatchEvent(evt);
}
function update(ta) {
if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
var evt = document.createEvent('Event');
evt.initEvent('autosize:update', true, false);
ta.dispatchEvent(evt);
}
var autosize = null;
// Do nothing in Node.js environment and IE8 (or lower)
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
autosize = function (el) {
return el;
};
autosize.destroy = function (el) {
return el;
};
autosize.update = function (el) {
return el;
};
} else {
autosize = function (el, options) {
if (el) {
Array.prototype.forEach.call(el.length ? el : [el], function (x) {
return assign(x, options);
});
}
return el;
};
autosize.destroy = function (el) {
if (el) {
Array.prototype.forEach.call(el.length ? el : [el], destroy);
}
return el;
};
autosize.update = function (el) {
if (el) {
Array.prototype.forEach.call(el.length ? el : [el], update);
}
return el;
};
}
module.exports = autosize;
});
\ No newline at end of file
// Converting text to basic latin (aka removing accents)
//
// Based on: http://semplicewebsites.com/removing-accents-javascript
//
var Latinise = {
map: {"Á":"A","Ă":"A","":"A","":"A","":"A","":"A","":"A","Ǎ":"A","Â":"A","":"A","":"A","":"A","":"A","":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","":"A","Ȁ":"A","À":"A","":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","":"A","Ⱥ":"A","Ã":"A","":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","":"AO","":"AU","":"AV","":"AV","":"AY","":"B","":"B","Ɓ":"B","":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","":"D","":"D","":"D","":"D","Ɗ":"D","":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","":"E","Ê":"E","":"E","":"E","":"E","":"E","":"E","":"E","Ë":"E","Ė":"E","":"E","Ȅ":"E","È":"E","":"E","Ȇ":"E","Ē":"E","":"E","":"E","Ę":"E","Ɇ":"E","":"E","":"E","":"ET","":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","":"G","Ǥ":"G","":"H","Ȟ":"H","":"H","Ĥ":"H","":"H","":"H","":"H","":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","":"I","İ":"I","":"I","Ȉ":"I","Ì":"I","":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","":"I","":"D","":"F","":"G","":"R","":"S","":"T","":"IS","Ĵ":"J","Ɉ":"J","":"K","Ǩ":"K","Ķ":"K","":"K","":"K","":"K","Ƙ":"K","":"K","":"K","":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","":"L","":"L","":"L","":"L","":"L","":"L","Ŀ":"L","":"L","Lj":"L","Ł":"L","LJ":"LJ","":"M","":"M","":"M","":"M","Ń":"N","Ň":"N","Ņ":"N","":"N","":"N","":"N","Ǹ":"N","Ɲ":"N","":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","":"O","":"O","":"O","":"O","":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","":"O","Ő":"O","Ȍ":"O","Ò":"O","":"O","Ơ":"O","":"O","":"O","":"O","":"O","":"O","Ȏ":"O","":"O","":"O","Ō":"O","":"O","":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","":"O","":"O","Ȭ":"O","Ƣ":"OI","":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","":"P","":"P","":"P","Ƥ":"P","":"P","":"P","":"P","":"Q","":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","":"R","":"R","":"R","Ȑ":"R","Ȓ":"R","":"R","Ɍ":"R","":"R","":"C","Ǝ":"E","Ś":"S","":"S","Š":"S","":"S","Ş":"S","Ŝ":"S","Ș":"S","":"S","":"S","":"S","":"SS","Ť":"T","Ţ":"T","":"T","Ț":"T","Ⱦ":"T","":"T","":"T","Ƭ":"T","":"T","Ʈ":"T","Ŧ":"T","":"A","":"L","Ɯ":"M","Ʌ":"V","":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","":"U","":"U","Ű":"U","Ȕ":"U","Ù":"U","":"U","Ư":"U","":"U","":"U","":"U","":"U","":"U","Ȗ":"U","Ū":"U","":"U","Ų":"U","Ů":"U","Ũ":"U","":"U","":"U","":"V","":"V","Ʋ":"V","":"V","":"VY","":"W","Ŵ":"W","":"W","":"W","":"W","":"W","":"W","":"X","":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","":"Y","":"Y","":"Y","Ƴ":"Y","":"Y","":"Y","Ȳ":"Y","Ɏ":"Y","":"Y","Ź":"Z","Ž":"Z","":"Z","":"Z","Ż":"Z","":"Z","Ȥ":"Z","":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","":"A","":"AE","ʙ":"B","":"B","":"C","":"D","":"E","":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","":"J","":"K","ʟ":"L","":"L","":"M","ɴ":"N","":"O","ɶ":"OE","":"O","":"OU","":"P","ʀ":"R","":"N","":"R","":"S","":"T","":"E","":"R","":"U","":"V","":"W","ʏ":"Y","":"Z","á":"a","ă":"a","":"a","":"a","":"a","":"a","":"a","ǎ":"a","â":"a","":"a","":"a","":"a","":"a","":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","":"a","ȁ":"a","à":"a","":"a","ȃ":"a","ā":"a","ą":"a","":"a","":"a","å":"a","ǻ":"a","":"a","":"a","ã":"a","":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","":"ao","":"au","":"av","":"av","":"ay","":"b","":"b","ɓ":"b","":"b","":"b","":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","":"d","":"d","ȡ":"d","":"d","":"d","ɗ":"d","":"d","":"d","":"d","":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","":"e","ê":"e","ế":"e","":"e","":"e","":"e","":"e","":"e","ë":"e","ė":"e","":"e","ȅ":"e","è":"e","":"e","ȇ":"e","ē":"e","":"e","":"e","":"e","ę":"e","":"e","ɇ":"e","":"e","":"e","":"et","":"f","ƒ":"f","":"f","":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","":"g","":"g","ǥ":"g","":"h","ȟ":"h","":"h","ĥ":"h","":"h","":"h","":"h","":"h","ɦ":"h","":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","":"i","":"i","ȉ":"i","ì":"i","":"i","ȋ":"i","ī":"i","į":"i","":"i","ɨ":"i","ĩ":"i","":"i","":"d","":"f","":"g","":"r","":"s","":"t","":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","":"k","ǩ":"k","ķ":"k","":"k","":"k","":"k","ƙ":"k","":"k","":"k","":"k","":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","":"l","ȴ":"l","":"l","":"l","":"l","":"l","":"l","ŀ":"l","ɫ":"l","":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","":"s","":"s","":"s","ḿ":"m","":"m","":"m","ɱ":"m","":"m","":"m","ń":"n","ň":"n","ņ":"n","":"n","ȵ":"n","":"n","":"n","ǹ":"n","ɲ":"n","":"n","ƞ":"n","":"n","":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","":"o","":"o","":"o","":"o","":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","":"o","ő":"o","ȍ":"o","ò":"o","":"o","ơ":"o","":"o","":"o","":"o","":"o","":"o","ȏ":"o","":"o","":"o","":"o","ō":"o","":"o","":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","":"o","":"o","ȭ":"o","ƣ":"oi","":"oo","ɛ":"e","":"e","ɔ":"o","":"o","ȣ":"ou","":"p","":"p","":"p","ƥ":"p","":"p","":"p","":"p","":"p","":"p","":"q","ʠ":"q","ɋ":"q","":"q","ŕ":"r","ř":"r","ŗ":"r","":"r","":"r","":"r","ȑ":"r","ɾ":"r","":"r","ȓ":"r","":"r","ɼ":"r","":"r","":"r","ɍ":"r","ɽ":"r","":"c","":"c","ɘ":"e","ɿ":"r","ś":"s","":"s","š":"s","":"s","ş":"s","ŝ":"s","ș":"s","":"s","":"s","":"s","ʂ":"s","":"s","":"s","ȿ":"s","ɡ":"g","ß":"ss","":"o","":"o","":"u","ť":"t","ţ":"t","":"t","ț":"t","ȶ":"t","":"t","":"t","":"t","":"t","ƭ":"t","":"t","":"t","ƫ":"t","ʈ":"t","ŧ":"t","":"th","ɐ":"a","":"ae","ǝ":"e","":"g","ɥ":"h","ʮ":"h","ʯ":"h","":"i","ʞ":"k","":"l","ɯ":"m","ɰ":"m","":"oe","ɹ":"r","ɻ":"r","ɺ":"r","":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","":"u","":"u","ű":"u","ȕ":"u","ù":"u","":"u","ư":"u","":"u","":"u","":"u","":"u","":"u","ȗ":"u","ū":"u","":"u","ų":"u","":"u","ů":"u","ũ":"u","":"u","":"u","":"ue","":"um","":"v","":"v","ṿ":"v","ʋ":"v","":"v","":"v","":"v","":"vy","":"w","ŵ":"w","":"w","":"w","":"w","":"w","":"w","":"w","":"x","":"x","":"x","ý":"y","ŷ":"y","ÿ":"y","":"y","":"y","":"y","ƴ":"y","":"y","ỿ":"y","ȳ":"y","":"y","ɏ":"y","":"y","ź":"z","ž":"z","":"z","ʑ":"z","":"z","ż":"z","":"z","ȥ":"z","":"z","":"z","":"z","ʐ":"z","ƶ":"z","ɀ":"z","":"ff","":"ffi","":"ffl","":"fi","":"fl","ij":"ij","œ":"oe","":"st","":"a","":"e","":"i","":"j","":"o","":"r","":"u","":"v","":"x"}
};
String.prototype.latinise = function() {
return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
};
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