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.
v 8.5.0 (unreleased)
- Remove gray background from layout in UI
v 8.4.0 (unreleased)
- Add pagination headers to already paginated API resources
- 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
- 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)
......@@ -47,10 +52,15 @@ v 8.4.0 (unreleased)
- 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 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
- Autosize Markdown textareas
- Import GitHub wiki into GitLab
v 8.3.4
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
- Add build artifacts browser
v 8.3.3
- Preserve CE behavior with JIRA integration by only calling API if URL is set
......@@ -84,6 +94,7 @@ v 8.3.0
- Add open_issues_count to project API (Stan Hu)
- 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 unsubscribe link in the email footer (Zeger-Jan van de Weg)
- 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)
- Remove api credentials from link to build_page
......
class @Activities
constructor: ->
Pager.init 20, true
$(".event-filter .btn").bind "click", (event) =>
$(".event-filter a").bind "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
......@@ -12,7 +12,7 @@ class @Activities
toggleFilter: (sender) ->
sender.toggleClass "active"
sender.closest('li').toggleClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
if event_filters
......
#= require autosize
$ ->
autosize($('.js-autosize'))
......@@ -48,14 +48,15 @@ class @MergeRequest
_this = @
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
$this = $(this)
if $this.data('submitted')
return
e.preventDefault()
e.stopImmediatePropagation()
shouldSubmit = $this.hasClass('btn-comment')
console.log("shouldSubmit")
if shouldSubmit && $this.data('submitted')
return
if shouldSubmit
_this.submitNoteForm($this.closest('form'),$this)
if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
e.preventDefault()
e.stopImmediatePropagation()
_this.submitNoteForm($this.closest('form'),$this)
submitNoteForm: (form, $button) =>
noteText = form.find("textarea.js-note-text").val()
......
......@@ -5,7 +5,7 @@
#
# ### Example Markup
#
# <ul class="nav nav-tabs merge-request-tabs">
# <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
......
#= require autosave
#= require autosize
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
......@@ -246,6 +247,7 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
autosize(textarea)
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
......@@ -521,9 +523,13 @@ class @Notes
if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen')
form.find('.js-note-target-close').text('Comment & close')
form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
form.find('.js-note-target-close').addClass('btn-comment-and-close')
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
form.find('.js-note-target-close').removeClass('btn-comment-and-close')
initTaskList: ->
@enableTaskList()
......
#= require latinise
class @Wikis
constructor: ->
$('.build-new-wiki').bind "click", (e) ->
$('[data-error~=slug]').addClass("hidden")
$('p.hint').show()
$('.build-new-wiki').bind 'click', (e) =>
$('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
valid_slug_pattern = /^[\w\/-]+$/
slug = @slugify(field.val())
slug = field.val()
if slug.match valid_slug_pattern
if (slug.length > 0)
path = field.attr('data-wikis-path')
if(slug.length > 0)
location.href = path + "/" + slug
else
e.preventDefault()
$('p.hint').hide()
$('[data-error~=slug]').removeClass("hidden")
location.href = path + '/' + slug
dasherize: (value) ->
value.replace(/[_\s]+/g, '-')
slugify: (value) =>
@dasherize(value.trim().toLowerCase().latinise())
......@@ -24,6 +24,7 @@
@import "framework/lists.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
......
......@@ -18,9 +18,9 @@
line-height: 36px;
}
.content-block,
.gray-content-block {
margin: -$gl-padding;
margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
margin-bottom: 0px;
......@@ -86,10 +86,7 @@
.cover-block {
text-align: center;
background: $background-color;
margin: -$gl-padding;
margin-bottom: 0;
padding: 44px $gl-padding;
border-bottom: 1px solid $border-color;
padding-top: 44px;
position: relative;
.avatar-holder {
......@@ -136,3 +133,19 @@
.block-connector {
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 {
@include border-radius(3px);
border-width: 1px;
border-style: solid;
font-size: 15px;
font-size: $gl-font-size;
font-weight: 500;
line-height: 18px;
padding: 11px $gl-padding;
letter-spacing: .4px;
padding: $gl-vert-padding $gl-padding;
&:focus,
&:active {
......@@ -17,8 +13,6 @@
@mixin btn-middle {
@include btn-default;
@include border-radius(3px);
padding: 11px 24px;
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
......@@ -74,16 +68,15 @@
@include btn-default;
@include btn-white;
&.btn-small,
&.btn-sm {
padding: 5px 10px;
}
&.btn-nr {
padding: 7px 10px;
padding: 4px 10px;
font-size: 13px;
line-height: 18px;
}
&.btn-xs {
padding: 1px 5px;
padding: 2px 5px;
}
&.btn-success,
......@@ -159,33 +152,42 @@
}
}
.btn-group-next {
.btn-clipboard {
border: none;
padding: 0 5px;
}
.input-group-btn {
.btn {
padding: 9px 0px;
font-size: 15px;
color: #7f8fa4;
border-color: #e7e9ed;
width: 140px;
.badge {
font-weight: normal;
background-color: #eee;
color: #78a;
@include btn-gray;
@include btn-middle;
&:hover {
outline: none;
}
&:focus {
outline: none;
}
&.active {
border-color: $gl-info;
background: $gl-info;
color: #fff;
&:active {
outline: none;
}
.badge {
color: $gl-info;
background-color: white;
}
&.btn-clipboard {
padding-left: 15px;
padding-right: 15px;
}
}
}
.btn-clipboard {
border: none;
.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
}
}
......@@ -374,75 +374,6 @@ table {
}
}
.center-top-menu, .left-top-menu {
@include nav-menu;
text-align: center;
margin-top: 5px;
margin-bottom: $gl-padding;
height: auto;
margin-top: -$gl-padding;
&.no-bottom {
margin-bottom: 0;
}
&.no-top {
margin-top: 0;
}
li a {
display: inline-block;
padding-top: $gl-padding;
padding-bottom: 11px;
margin-bottom: -1px;
}
&.bottom-border {
border-bottom: 1px solid $border-color;
height: 57px;
}
&.wide {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
}
}
.left-top-menu {
text-align: left;
border-bottom: 1px solid #EEE;
}
.center-middle-menu {
@include nav-menu;
padding: 0;
text-align: center;
margin: -$gl-padding;
margin-top: 0;
margin-bottom: 0;
height: 58px;
border-bottom: 1px solid $border-color;
li {
&:after {
content: "|";
color: $border-gray-light;
}
&:last-child {
&:after {
content: none;
}
}
> a {
display: inline-block;
text-transform: uppercase;
font-size: 13px;
}
}
}
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
}
......
......@@ -3,11 +3,8 @@
*
*/
.file-holder {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border: none;
border-top: 1px solid #E7E9EE;
border-bottom: 1px solid #E7E9EE;
border: 1px solid $border-color;
&.readme-holder {
border-bottom: 0;
......
......@@ -8,10 +8,12 @@
.flash-notice {
@extend .alert;
@extend .alert-info;
margin: 0;
}
.flash-alert {
@extend .alert;
@extend .alert-danger;
margin: 0;
}
}
......@@ -74,8 +74,6 @@ label {
.form-control {
@include box-shadow(none);
height: 42px;
padding: 8px $gl-padding;
}
.form-control-inline {
......
......@@ -28,6 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
border-bottom: 1px solid #EEE;
.container-fluid {
width: 100% !important;
......
......@@ -5,8 +5,6 @@ html {
}
body {
background-color: #F3F3F3 !important;
&.navless {
background-color: white !important;
}
......
......@@ -109,10 +109,8 @@ ul.content-list {
padding: 0;
> li {
padding: $gl-padding;
padding: $gl-padding 0;
border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
.avatar {
......@@ -133,6 +131,7 @@ ul.content-list {
.panel > .content-list {
li {
margin: 0;
padding: $gl-padding;
}
}
......@@ -148,7 +147,7 @@ ul.controls {
> li {
float: left;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
......
......@@ -65,13 +65,6 @@
position: relative;
}
.md-header {
ul {
float: left;
margin-bottom: 1px;
}
}
.referenced-users {
color: #4c4e54;
padding-top: 10px;
......@@ -85,28 +78,12 @@
box-shadow: none;
}
.new_note,
.edit_note,
.detail-page-description,
.milestone-description,
.wiki-content,
.merge-request-form {
.nav-tabs {
margin-bottom: 0;
border: none;
li a,
li.active a {
border: 1px solid #DDD;
}
}
}
.markdown-area {
@include border-radius(0);
background: #FFF;
border: 1px solid #ddd;
min-height: 140px;
max-height: 430px;
padding: 5px;
box-shadow: none;
width: 100%;
......
......@@ -118,38 +118,3 @@
font-size: 16px;
line-height: 24px;
}
@mixin nav-menu {
padding: 0;
margin: 0;
list-style: none;
height: 56px;
li {
display: inline-block;
a {
padding: 14px;
font-size: 15px;
line-height: 28px;
color: #959494;
border-bottom: 2px solid transparent;
&:hover, &:active, &:focus {
text-decoration: none;
outline: none;
}
}
&.active a {
color: #616060;
border-bottom: 2px solid #4688f1;
}
.badge {
font-weight: normal;
background-color: #eee;
color: #78a;
}
}
}
......@@ -9,7 +9,7 @@
padding-right: 5px;
}
.nav.nav-tabs > li > a {
.nav-links > li > a {
padding: 10px;
font-size: 12px;
margin-right: 3px;
......@@ -81,7 +81,7 @@
display: none;
}
.center-top-menu, .left-top-menu {
.nav-links, .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
......@@ -100,11 +100,6 @@
}
@media (max-width: $screen-sm-max) {
.page-with-sidebar .content-wrapper {
padding: 0;
padding-top: 1px;
}
.issues-filters {
.milestone-filter, .labels-filter {
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 @@
.select2-choice {
background: #FFF;
border-color: #DDD;
height: 42px;
padding: 8px $gl-padding;
height: 36px;
padding: 6px $gl-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
......
......@@ -21,11 +21,10 @@
.content-wrapper {
width: 100%;
padding: 20px;
.container-fluid {
background: #FFF;
padding: $gl-padding;
padding: 0 $gl-padding;
&.container-blank {
background: none;
......
.table-holder {
margin: -$gl-padding;
margin-top: 0;
margin-bottom: 0;
margin: 0;
}
table {
&.table {
margin-bottom: $gl-padding;
.dropdown-menu a {
text-decoration: none;
}
......@@ -32,6 +30,7 @@ table {
}
th {
background-color: $background-color;
font-weight: normal;
font-size: 15px;
border-bottom: 1px solid $border-color !important;
......
......@@ -5,10 +5,8 @@
padding: 0;
.timeline-entry {
padding: $gl-padding;
padding: $gl-padding 0;
border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
......
......@@ -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
......
......@@ -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).
$padding-base-vertical: 9px;
$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
$component-active-color: #fff;
$component-active-bg: $brand-info;
......
......@@ -177,7 +177,7 @@ body {
}
.page-title {
margin-top: 0px;
margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
......
......@@ -22,6 +22,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
$gl-vert-padding: 6px;
$gl-padding-top:10px;
$gl-avatar-size: 46px;
$secondary-text: #7f8fa4;
......
......@@ -4,7 +4,7 @@
position: absolute;
top: 0px;
right: 4px;
line-height: 40px;
line-height: 56px;
}
a.js-zen-leave {
......
......@@ -39,6 +39,8 @@
}
.commit-box {
border-top: 1px solid $border-color;
.commit-title {
margin: 0;
font-size: 23px;
......
.detail-page-header {
margin: -$gl-padding;
padding: 7px $gl-padding;
margin-bottom: 0px;
padding: 11px 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
......
// Common
.diff-file {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border: none;
border-bottom: 1px solid #E7E9EE;
border: 1px solid $border-color;
border-top: none;
.diff-header {
position: relative;
......@@ -23,14 +21,6 @@
}
}
.diff-controls {
.btn {
padding: 0px 10px;
font-size: 13px;
line-height: 28px;
}
}
.commit-short-id {
font-family: $monospace_font;
font-size: smaller;
......
......@@ -4,9 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
margin-left: -$gl-padding;
margin-right: -$gl-padding;
padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
......
......@@ -27,10 +27,10 @@
.project-issuable-filter {
.controls {
float: right;
margin-top: 7px;
margin-top: 11px;
}
.center-top-menu {
.nav-links {
text-align: left;
}
}
......@@ -95,7 +95,7 @@
.cross-project-reference {
color: $gl-link-color;
span {
white-space: nowrap;
width: 85%;
......@@ -105,8 +105,13 @@
text-overflow: ellipsis;
}
cite {
font-style: normal;
}
button {
float: right;
padding: 3px 5px;
}
}
......
......@@ -3,9 +3,9 @@
*
*/
.mr-state-widget {
background: #F7F8FA;
background: $background-color;
color: $gl-gray;
border: 1px solid #dce0e6;
border: 1px solid $border-color;
@include border-radius(2px);
form {
......
......@@ -159,6 +159,7 @@
.edit_note {
.markdown-area {
min-height: 140px;
max-height: 430px;
}
.note-form-actions {
background: transparent;
......
......@@ -26,6 +26,8 @@
}
.project-home-panel {
padding-bottom: 40px;
border-bottom: 1px solid $border-color;
.cover-controls {
.project-settings-dropdown {
......@@ -51,6 +53,8 @@
}
.notifications-btn {
margin-top: -28px;
.fa-bell {
margin-right: 6px;
}
......@@ -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 {
@extend .btn;
@extend .btn-gray;
......@@ -98,11 +91,6 @@
}
}
.git-clone-holder {
display: inline-table;
position: relative;
}
.project-repo-buttons {
margin-top: 12px;
margin-bottom: 0px;
......@@ -112,10 +100,22 @@
margin-bottom: 12px;
}
.clone-row {
.split-repo-buttons,
.project-clone-holder {
display: inline-block;
}
.split-repo-buttons {
margin: 0 12px;
}
}
.btn {
@include btn-gray;
text-transform: none;
}
.count-with-arrow {
display: inline-block;
position: relative;
......@@ -160,8 +160,8 @@
border-style: solid;
font-size: 13px;
font-weight: 600;
line-height: 20px;
padding: 11px 16px;
line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 10px;
text-align: center;
......@@ -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 {
@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);
......@@ -351,28 +240,6 @@
color: #555;
}
ul.nav.nav-projects-tabs {
@extend .nav-tabs;
padding-left: 8px;
li {
a {
padding: 6px 25px;
margin-top: 2px;
border-color: #DDD;
background-color: #EEE;
text-shadow: 0 1px 1px white;
color: #555;
}
&.active {
a {
font-weight: bold;
}
}
}
}
.project_member_row form {
margin: 0px;
}
......@@ -399,9 +266,9 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb {
padding: 0;
line-height: 42px;
background: transparent;
border: none;
line-height: 42px;
margin: 0;
> li + li:before {
......@@ -416,11 +283,8 @@ ul.nav.nav-projects-tabs {
.top-area {
border-bottom: 1px solid #EEE;
margin: 0 -16px;
padding: 0 $gl-padding;
height: 42px;
ul.left-top-menu {
ul.nav-links {
display: inline-block;
width: 50%;
margin-bottom: 0px;
......@@ -431,12 +295,12 @@ ul.nav.nav-projects-tabs {
width: 50%;
display: inline-block;
float: right;
padding-top: 7px;
padding-top: 11px;
text-align: right;
.btn-green {
margin-top: -2px;
margin-left: 10px;
float: right;
}
}
......@@ -482,11 +346,11 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px;
padding-bottom: 4px;
ul.nav-pills {
ul.nav {
display:inline-block;
}
.nav-pills li {
.nav li {
display:inline;
}
......@@ -523,8 +387,7 @@ pre.light-well {
}
.projects-search-form {
margin: -$gl-padding;
padding: $gl-padding;
padding: $gl-padding 0;
padding-bottom: 0;
margin-bottom: 0px;
......@@ -574,10 +437,8 @@ pre.light-well {
@include basic-list;
.project-row {
padding: $gl-padding;
padding: $gl-padding 0;
border-color: $table-border-color;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
&.no-description {
.project {
......@@ -631,8 +492,6 @@ pre.light-well {
}
.project-last-commit {
margin: 0 7px;
.ci-status {
margin-right: 16px;
}
......@@ -662,9 +521,7 @@ pre.light-well {
}
.project-show-readme .readme-holder {
margin-left: -$gl-padding;
margin-right: -$gl-padding;
padding: ($gl-padding + 7px);
padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
......@@ -672,3 +529,32 @@ pre.light-well {
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 {
> .nav-block {
margin: 11px 0;
}
.file-finder {
width: 50%;
......@@ -13,7 +16,7 @@
tr {
> td, > th {
line-height: 28px;
line-height: 26px;
}
&:hover {
......@@ -86,12 +89,14 @@
.blob-commit-info {
list-style: none;
padding: $gl-padding;
background: $background-color;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
padding: 0;
margin-bottom: 5px;
.commit {
padding: $gl-padding 0;
padding: 0;
.commit-row-title {
.commit-row-message {
......@@ -115,3 +120,8 @@
font-weight: normal;
color: $md-link-color;
}
.tree-controls {
float: right;
margin-top: 11px;
}
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
before_action :build, except: [:index, :cancel_all]
before_action :authorize_manage_builds!, except: [:index, :show, :status]
before_action :authorize_download_build_artifacts!, only: [:download]
layout "project"
......@@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController
redirect_to build_path(build)
end
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
unless artifacts_file.exists?
return not_found!
end
send_file artifacts_file.path, disposition: 'attachment'
end
def status
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
......@@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
def artifacts_file
build.artifacts_file
end
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end
......@@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController
return page_404
end
end
def authorize_download_build_artifacts!
unless can?(current_user, :download_build_artifacts, @project)
if current_user.nil?
return authenticate_user!
else
return render_404
end
end
end
end
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
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
class: 'btn btn-xs btn-clipboard',
class: 'btn btn-clipboard',
data: data,
type: :button
end
......
......@@ -27,13 +27,15 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
class: "event-filter-link btn btn-default #{active}",
class: "event-filter-link",
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
}
link_to request.path, link_opts do
content_tag(:span, ' ' + tooltip)
content_tag :li, class: active do
link_to request.path, link_opts do
content_tag(:span, ' ' + tooltip)
end
end
end
......
......@@ -91,7 +91,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
markdown(wiki_page.content)
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
when :asciidoc
asciidoc(wiki_page.content)
else
......
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
issue_mail_with_notification(issue_id, recipient_id) do
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
setup_issue_mail(issue_id, recipient_id)
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
issue_mail_with_notification(issue_id, recipient_id) do
@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))
end
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))
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
issue_mail_with_notification(issue_id, recipient_id) do
@updated_by = User.find updated_by_user_id
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
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))
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
issue_mail_with_notification(issue_id, recipient_id) do
@issue_status = status
@updated_by = User.find updated_by_user_id
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
setup_issue_mail(issue_id, recipient_id)
@issue_status = status
@updated_by = User.find updated_by_user_id
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
private
......@@ -38,14 +38,12 @@ module Emails
}
end
def issue_mail_with_notification(issue_id, recipient_id)
def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
yield
SentNotification.record(@issue, recipient_id, reply_key)
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
end
end
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
setup_merge_request_mail(merge_request_id, recipient_id)
mail_new_thread(@merge_request,
from: sender(@merge_request.author_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end
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
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end
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
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
@merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace,
@project,
@merge_request)
setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
SentNotification.record(@merge_request, recipient_id, reply_key)
end
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
@project = @merge_request.project
@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,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
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
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
note_mail_with_notification(note_id, recipient_id) do
@commit = @note.noteable
@target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit,
from: sender(@note.author_id),
to: recipient(recipient_id),
subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
setup_note_mail(note_id, recipient_id)
@commit = @note.noteable
@target_url = namespace_project_commit_url(*note_target_url_options)
mail_answer_thread(@commit,
from: sender(@note.author_id),
to: recipient(recipient_id),
subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
def note_issue_email(recipient_id, note_id)
note_mail_with_notification(note_id, recipient_id) do
@issue = @note.noteable
@target_url = namespace_project_issue_url(*note_target_url_options)
mail_answer_thread(@issue, note_thread_options(recipient_id))
end
setup_note_mail(note_id, recipient_id)
@issue = @note.noteable
@target_url = namespace_project_issue_url(*note_target_url_options)
mail_answer_thread(@issue, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
note_mail_with_notification(note_id, recipient_id) do
@merge_request = @note.noteable
@target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
setup_note_mail(note_id, recipient_id)
@merge_request = @note.noteable
@target_url = namespace_project_merge_request_url(*note_target_url_options)
mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
private
......@@ -42,13 +42,11 @@ module Emails
}
end
def note_mail_with_notification(note_id, recipient_id)
def setup_note_mail(note_id, recipient_id)
@note = Note.find(note_id)
@project = @note.project
yield
SentNotification.record_note(@note, recipient_id, reply_key)
@sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
......@@ -107,10 +107,9 @@ class Notify < BaseMailer
end
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
if reply_key
headers['X-GitLab-Reply-Key'] = reply_key
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
......
......@@ -175,7 +175,7 @@ class Ability
:create_merge_request,
:create_wiki,
:manage_builds,
:download_build_artifacts,
:read_build_artifacts,
:push_code
]
end
......
......@@ -30,10 +30,12 @@
# description :string(255)
# artifacts_file :text
# gl_project_id :integer
# artifacts_metadata :text
#
module Ci
class Build < CommitStatus
include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
......@@ -49,6 +51,7 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
......@@ -291,21 +294,18 @@ module Ci
end
def target_url
Gitlab::Application.routes.url_helpers.
namespace_project_build_url(project.namespace, project, self)
namespace_project_build_url(project.namespace, project, self)
end
def cancel_url
if active?
Gitlab::Application.routes.url_helpers.
cancel_namespace_project_build_path(project.namespace, project, self)
cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
if retryable?
Gitlab::Application.routes.url_helpers.
retry_namespace_project_build_path(project.namespace, project, self)
retry_namespace_project_build_path(project.namespace, project, self)
end
end
......@@ -321,20 +321,35 @@ module Ci
pending? && !any_runners_online?
end
def download_url
if artifacts_file.exists?
Gitlab::Application.routes.url_helpers.
download_namespace_project_build_path(project.namespace, project, self)
end
end
def execute_hooks
build_data = Gitlab::BuildDataBuilder.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
end
def artifacts?
artifacts_file.exists?
end
def artifacts_download_url
if artifacts?
download_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_browse_url
if artifacts_browser_supported?
browse_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_browser_supported?
artifacts? && artifacts_metadata.exists?
end
def artifacts_metadata_entry(path)
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry
end
private
......
......@@ -56,6 +56,8 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
state_machine :status, initial: :pending do
event :run do
transition pending: :running
......@@ -131,7 +133,11 @@ class CommitStatus < ActiveRecord::Base
false
end
def download_url
def artifacts_download_url
nil
end
def artifacts_browse_url
nil
end
end
......@@ -119,6 +119,12 @@ module Issuable
update(subscribed: !subscribed?(user))
end
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: false)
end
def to_hook_data(user)
{
object_kind: self.class.name.underscore,
......
......@@ -38,6 +38,10 @@ class ProjectWiki
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
def wiki_base_path
["/", @project.path_with_namespace, "/wikis"].join('')
end
# Returns the Gollum::Wiki object.
def wiki
@wiki ||= begin
......
......@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base
class << self
def reply_key
return nil unless Gitlab::IncomingEmail.enabled?
SecureRandom.hex(16)
end
......@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base
def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code
record(note.noteable, recipient_id, reply_key, params)
end
end
def unsubscribable?
!for_commit?
end
def for_commit?
noteable_type == "Commit"
end
......@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base
super
end
end
def to_param
self.reply_key
end
end
......@@ -169,7 +169,7 @@ class WikiPage
private
def set_attributes
attributes[:slug] = @page.escaped_url_path
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
attributes[:format] = @page.format
end
......
......@@ -44,7 +44,7 @@ module MergeRequests
def after_merge
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).
execute(merge_request.source_branch)
end
......
......@@ -60,8 +60,8 @@
%td
.pull-right
- if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
= link_to build.download_url, title: 'Download artifacts' do
- if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts?
= link_to build.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, build.project)
- if build.active?
......
......@@ -4,7 +4,7 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
%ul.center-top-menu
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
All
......
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
%ul.nav.nav-tabs.log-tabs
%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
%p.light To prevent performance issues admin logs output the last 2000 lines
.gray-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
......
......@@ -12,7 +12,7 @@
%i.fa.fa-pencil-square-o
Edit
%hr
%ul.nav.nav-tabs
%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
......@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
.append-bottom-default
- page_title "Users"
= render 'shared/show_aside'
.row
%aside.col-md-3
.admin-filter
%ul.nav.nav-pills.nav-stacked
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
%small.pull-right= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
%small.pull-right= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.pull-right= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.pull-right= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.pull-right= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
%small.pull-right= number_with_delimiter(User.without_projects.count)
%hr
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
%hr
= link_to 'Reset', admin_users_path, class: "btn btn-cancel"
.admin-filter
%ul.nav-links
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
%small.badge= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
%small.badge= number_with_delimiter(User.admins.count)
%li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count)
%li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
%small.badge= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
%section.col-md-9
.panel.panel-default
.panel-heading
Users (#{number_with_delimiter(@users.total_count)})
.panel-head-actions
.dropdown.inline
%a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_name
%b.caret
%ul.dropdown-menu
%li
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
%ul.well-list
- @users.each do |user|
.gray-content-block.second-block
.pull-right
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_name
%b.caret
%ul.dropdown-menu
%li
.list-item-name
- if user.blocked?
%i.fa.fa-lock.cred
= link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
= sort_title_name
= link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
= sort_title_recently_signin
= link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
= sort_title_oldest_signin
= link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
= sort_title_recently_created
= link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
= sort_title_oldest_created
= link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
= sort_title_recently_updated
= link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
= sort_title_oldest_updated
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
= search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
= hidden_field_tag "filter", params[:filter]
= button_tag class: 'btn btn-primary' do
%i.fa.fa-search
.panel.panel-default
%ul.well-list
- @users.each do |user|
%li
.list-item-name
- if user.blocked?
%i.fa.fa-lock.cred
- else
%i.fa.fa-user.cgreen
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
- if user == current_user
%span.cred It's you!
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
.pull-right
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- unless user == current_user
- if user.ldap_blocked?
= link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
%i.fa.fa-lock
Unblock
- elsif user.blocked?
= link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
%i.fa.fa-user.cgreen
= link_to user.name, [:admin, user]
- if user.admin?
%strong.cred (Admin)
- if user == current_user
%span.cred It's you!
.pull-right
%span.light
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
.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"
= 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
= render "events/event_last_push", event: @last_push
.gray-content-block
.nav-block
- if current_user
.pull-right
.controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
......
%ul.center-top-menu
%ul.nav-links
%li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
......
%ul.center-top-menu
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
Your Groups
......
= content_for :flash_message do
= render 'shared/project_limit'
.top-area
%ul.left-top-menu
%ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
......
%ul.center-top-menu
%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
......
......@@ -48,7 +48,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
......
......@@ -3,32 +3,36 @@
= render 'dashboard/snippets_head'
.gray-content-block
.pull-right
.nav-block
.controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus')
New Snippet
.btn-group.btn-group-next.snippet-scope-menu
= link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
All
%span.badge
= current_user.snippets.count
= link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
Private
%span.badge
= current_user.snippets.are_private.count
= link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
Internal
%span.badge
= current_user.snippets.are_internal.count
= link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
Public
%span.badge
= current_user.snippets.are_public.count
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
= link_to dashboard_snippets_path do
All
%span.badge
= current_user.snippets.count
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to dashboard_snippets_path(scope: 'are_private') do
Private
%span.badge
= current_user.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to dashboard_snippets_path(scope: 'are_internal') do
Internal
%span.badge
= current_user.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public
%span.badge
= current_user.snippets.are_public.count
= render 'snippets/snippets'
......@@ -7,7 +7,7 @@
%h3 Sign in
.login-body
- if form_based_providers.any?
%ul.nav.nav-tabs
%ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
......
- header_title group_title(@group, "Settings", edit_group_path(@group))
- @blank_container = true
.panel.panel-default
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
.panel-body
......
......@@ -2,7 +2,7 @@
- header_title group_title(@group, "Members", group_group_members_path(@group))
- @blank_container = true
.group-members-page
.group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default
.panel-heading
......
......@@ -54,7 +54,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
......
- page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group))
.panel.panel-default
.panel.panel-default.prepend-top-default
.panel-heading
%strong= @group.name
projects:
......
- @no_container = true
- unless can?(current_user, :read_group, @group)
- @disable_search_panel = true
......@@ -25,8 +27,8 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
- if can?(current_user, :read_group, @group)
%ul.center-top-menu.no-top
%ul.nav-links
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
......@@ -35,20 +37,22 @@
= link_to "#projects", 'data-toggle' => 'tab' do
Projects
.tab-content
.tab-pane.active#activity
.gray-content-block.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
- if can?(current_user, :read_group, @group)
%div{ class: container_class }
.tab-content
.tab-pane.active#activity
.activity-filter-block
- if current_user
= render "events/event_last_push", event: @last_push
= render 'shared/event_filter'
= render 'shared/event_filter'
.content_list
= spinner
.content_list
= spinner
.tab-pane#projects
= render "projects", projects: @projects
.tab-pane#projects
= render "projects", projects: @projects
- else
%p.center-top-menu.no-top
%p.nav-links.no-top
No projects to show
......@@ -139,26 +139,9 @@
%h2#navs Navigation
%h4
%code .center-top-menu
%code .nav-links
.example
%ul.center-top-menu
%li.active
%a Open
%li
%a Closed
%h4
%code .btn-group.btn-group-next
.example
%div.btn-group.btn-group-next
%a.btn.active Open
%a.btn Closed
%h4
%code .nav.nav-tabs
.example
%ul.nav.nav-tabs
%ul.nav-links
%li.active
%a Open
%li
......
......@@ -24,7 +24,7 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
%div{ class: container_class }
%div{ class: (container_class unless @no_container) }
.content
.clearfix
= yield
......@@ -44,6 +44,10 @@
%br
-# 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}.
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
\ No newline at end of file
= email_action @target_url
......@@ -6,7 +6,7 @@
.alert.alert-info
Some options are unavailable for LDAP accounts
.account-page
.account-page.prepend-top-default
.panel.panel-default.update-token
.panel-heading
Reset Private token
......
.gray-content-block.activity-filter-block
.nav-block.activity-filter-block
- if current_user
.pull-right
.controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
......
#tree-holder.tree-holder.clearfix
.gray-content-block.second-block
.nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
......
......@@ -44,13 +44,16 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
= render "shared/clone_panel"
.clone-row
.project-clone-holder
= render "shared/clone_panel"
.split-repo-buttons
= render "projects/buttons/download"
= render 'projects/buttons/dropdown'
.split-repo-buttons
.btn-group.pull-left
= render "projects/buttons/download"
= render 'projects/buttons/dropdown'
= render 'projects/buttons/notifications'
= render 'projects/buttons/notifications'
:javascript
new Star();
.md-area
.md-header.clearfix
%ul.center-top-menu
%ul.nav-links
%li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
......
.zennable
.zen-backdrop
- classes << ' js-gfm-input markdown-area'
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
= f.text_area attr, class: classes
- 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
= render 'shared/ref_switcher', destination: 'blob', path: @path
......
......@@ -2,7 +2,7 @@
= render "header_title"
.file-editor
%ul.center-top-menu.no-bottom.js-edit-mode
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
......
......@@ -8,7 +8,7 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
%ul.center-top-menu
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
All
......
......@@ -14,7 +14,7 @@
#up-build-trace
- if @commit.matrix_for_ref?(@build.ref)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
- @commit.latest_builds_for_ref(@build.ref).each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
......@@ -89,9 +89,15 @@
Test coverage
%h1 #{@build.coverage}%
- if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
.build-widget.center
= link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
- if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts?
.build-widget.artifacts
%h4.title Build artifacts
.center
.btn-group{ role: :group }
= link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary'
- if @build.artifacts_browser_supported?
= link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary'
.build-widget
%h4.title
......
- if current_user
%span.dropdown
%a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
.btn-group
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
......
%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
%ul.nav-links.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
......
......@@ -50,7 +50,7 @@
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
.commit-box.gray-content-block.middle-block
.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
......
......@@ -2,7 +2,9 @@
- page_description @commit.description
= render "projects/commits/header_title"
= render "commit_box"
.prepend-top-default
= render "commit_box"
- if @ci_commit
= render "ci_menu"
- else
......
......@@ -66,8 +66,8 @@
%td
.pull-right
- if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
= link_to commit_status.download_url, title: 'Download artifacts' do
- if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts?
= link_to commit_status.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, commit_status.project)
- if commit_status.active?
......
%ul.center-top-menu
%ul.nav-links
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
......
......@@ -6,7 +6,7 @@
= render "head"
.gray-content-block
.gray-content-block.second-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
......
......@@ -3,7 +3,7 @@
- diff_files = safe_diff_files(diffs)
.gray-content-block.middle-block.oneline-block
.content-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
......
- @blank_container = true
.project-edit-container
.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
.panel.panel-default
......
%ul.center-top-menu
%ul.nav-links
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
......
......@@ -18,7 +18,7 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
%ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
......
......@@ -45,7 +45,7 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
%ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
......
......@@ -57,7 +57,7 @@
%span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone)
%ul.center-top-menu.no-top.no-bottom
%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
......
......@@ -2,7 +2,7 @@
= render "header_title"
- @blank_container = true
.project-members-page
.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
......
- page_title "Runners"
.light
.light.prepend-top-default
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
......
- @no_container = true
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
......@@ -8,11 +10,10 @@
= render 'shared/no_password'
= render 'projects/last_push'
= render "home_panel"
.project-stats.gray-content-block.second-block
%ul.nav.nav-pills
%ul.nav
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
......@@ -57,15 +58,17 @@
= link_to add_contribution_guide_path(@project) do
Add Contribution guide
- if @project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
- if @repository.commit
.content-block.second-block.white
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
= render default_project_view
%div{class: "project-show-#{default_project_view}"}
= render default_project_view
%div.tree-content-holder
.table-holder
%table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
%table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
......
......@@ -5,13 +5,13 @@
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
= render 'projects/last_push'
.pull-right
.tree-controls
= render 'projects/find_file_link'
- if can? current_user, :download_code, @project
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix
.gray-content-block.top-block
.nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
......@@ -7,7 +7,7 @@
= render 'projects/wikis/new'
%ul.center-top-menu
%ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
......
......@@ -5,12 +5,9 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
= label_tag :new_wiki_path do
%span Page 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)
%p.hidden.text-danger{data: { error: "slug" }}
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-group
= label_tag :new_wiki_path do
%span Page 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)
.form-actions
= link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
......@@ -3,14 +3,12 @@
= render 'nav'
.gray-content-block
.row
.col-sm-6
%h3.page-title.oneline
Git access for
%strong= @project_wiki.path_with_namespace
%span.oneline
Git access for
%strong= @project_wiki.path_with_namespace
.col-sm-6
= render "shared/clone_panel", project: @project_wiki
.pull-right
= render "shared/clone_panel", project: @project_wiki
.git-empty.prepend-top-default
%fieldset
......
%ul.nav.nav-tabs.search-filter
%ul.nav-links.search-filter
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
......
- if @search_results.empty?
= render partial: "search/results/empty"
- else
%p.light
.gray-content-block
Search results for
%code
= @search_term
......
- page_title @search_term
= render 'search/form'
.prepend-top-default
= render 'search/form'
- if @search_term
= render 'search/category'
= render 'search/results'
- project = project || @project
.git-clone-holder
.btn-group.clone-options
.git-clone-holder.input-group
.input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span
= 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.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
......
.milestones-filters
%ul.center-top-menu
%ul.nav-links
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
= link_to milestones_filter_path(state: 'opened') do
Open
......
.issues-filters
.issues-state-filters
%ul.center-top-menu
%ul.nav-links
- if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests'
- else
......
......@@ -69,15 +69,16 @@
You're not receiving notifications from this thread.
.subscribed{class: ( 'hidden' unless subscribed )}
You're receiving notifications because you're subscribed to this thread.
- project_ref = cross_project_reference(@project, issuable)
.block
.title
.cross-project-reference
%span#cross-project-reference
%span
Reference:
%a{href: '#', title:project_ref}
%cite{title: project_ref}
= project_ref
= clipboard_button(clipboard_target: 'span#cross-project-reference')
= clipboard_button(clipboard_text: project_ref)
:javascript
new Subscription("#{toggle_subscription_path(issuable)}");
......
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
%ul.center-top-menu
%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
......
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
%ul.center-top-menu
%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
......
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......@@ -8,6 +9,25 @@
= render 'shared/show_aside'
.cover-block
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
%span.report-abuse
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
= icon('exclamation-circle')
- if current_user
&nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
......@@ -47,74 +67,56 @@
= icon('map-marker')
= @user.location
%ul.nav-links.center
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups
- if @contributed_projects.present?
%li
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects
- if @projects.present?
%li
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects
.cover-controls
- if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
%span.report-abuse
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
= icon('exclamation-circle')
- else
= link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
= icon('exclamation-circle')
- if current_user
&nbsp;
= link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
= icon('rss')
.gray-content-block.second-block
.user-calendar
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
%div{ class: container_class }
.tab-content
.tab-pane.active#activity
.gray-content-block.white.second-block
%div{ class: container_class }
.user-calendar
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
- if @groups.any?
%li
= link_to "#groups", 'data-toggle' => 'tab' do
Groups
- if @contributed_projects.present?
%li
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects
- if @projects.present?
%li
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects
.tab-content
.tab-pane.active#activity
.content_list
= spinner
.content_list
= spinner
- if @groups.any?
.tab-pane#groups
%ul.content-list
- @groups.each do |group|
= render 'shared/groups/group', group: group
- if @groups.any?
.tab-pane#groups
%ul.content-list
- @groups.each do |group|
= render 'shared/groups/group', group: group
- if @contributed_projects.present?
.tab-pane#contributed
.contributed-projects
= render 'shared/projects/list',
projects: @contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: true
- if @contributed_projects.present?
.tab-pane#contributed
.contributed-projects
= render 'shared/projects/list',
projects: @contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
- if @projects.present?
.tab-pane#personal
.personal-projects
= render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
- if @projects.present?
.tab-pane#personal
.personal-projects
= render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
:javascript
$(".user-calendar").load("#{user_calendar_path}");
......@@ -88,6 +88,12 @@ Rails.application.routes.draw do
end
end
resources :sent_notifications, only: [], constraints: { id: /\h{32}/ } do
member do
get :unsubscribe
end
end
# Spam reports
resources :abuse_reports, only: [:new, :create]
......@@ -513,7 +519,7 @@ Rails.application.routes.draw do
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
# Order matters to give priority to these matches
......@@ -604,9 +610,14 @@ Rails.application.routes.draw do
member do
get :status
post :cancel
get :download
post :retry
end
resource :artifacts, only: [] do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
......
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
t.string "description"
t.text "artifacts_file"
t.integer "gl_project_id"
t.text "artifacts_metadata"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
......
......@@ -23,6 +23,7 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
- [Builds](builds.md)
- [Build triggers](build_triggers.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
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
sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart mailroom
```
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
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:
......@@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
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)_.
2. Check if the `incoming` user received the email:
1. Check if the `incoming` user received the email:
```sh
su - incoming
......@@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
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`:
```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
And I click on the "Pages" button
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
Scenario: Edit Wiki page that has a path
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
expect(current_path).to include 'one/two/three'
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
expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
end
......
......@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
expect(find('div.content ul.center-top-menu li.active')).to have_content(content)
expect(find('div.content ul.nav-links li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
......@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1)
expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
end
step 'no other sub navs should be active' do
......
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
mount Keys
mount Tags
mount Triggers
mount Builds
mount Variables
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
expose :id, :variables
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
expose :token, :created_at, :updated_at, :deleted_at, :last_used
end
......
......@@ -97,11 +97,9 @@ module API
end
def paginate(relation)
per_page = params[:per_page].to_i
paginated = relation.page(params[:page]).per(per_page)
add_pagination_headers(paginated, per_page)
paginated
relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
add_pagination_headers(data)
end
end
def authenticate!
......@@ -289,12 +287,14 @@ module API
# file helpers
def uploaded_file!(field, uploads_path)
def uploaded_file(field, uploads_path)
if params[field]
bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
return params[field]
end
return nil unless params["#{field}.path"] && params["#{field}.name"]
# sanitize file paths
# this requires all paths to exist
required_attributes! %W(#{field}.path)
......@@ -327,16 +327,26 @@ module API
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
links = []
links << %(<#{request_url}?page=#{paginated.current_page - 1}&per_page=#{per_page}>; rel="prev") unless paginated.first_page?
links << %(<#{request_url}?page=#{paginated.current_page + 1}&per_page=#{per_page}>; rel="next") unless paginated.last_page?
links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first")
links << %(<#{request_url}?page=#{paginated.total_pages}&per_page=#{per_page}>; rel="last")
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_data.current_page + 1}&per_page=#{paginated_data.limit_value}>; rel="next") unless paginated_data.last_page?
links << %(<#{request_url}?page=1&per_page=#{paginated_data.limit_value}>; rel="first")
links << %(<#{request_url}?page=#{paginated_data.total_pages}&per_page=#{paginated_data.limit_value}>; rel="last")
header 'Link', links.join(', ')
links.join(', ')
end
def abilities
......
require 'banzai'
module Banzai
# Common methods for ReferenceFilters that support an optional cross-project
# reference.
......
require 'active_support/core_ext/string/output_safety'
require 'banzai'
module Banzai
module Filter
......
require 'banzai'
module Banzai
module Filter
# Issues, Merge Requests, Snippets, Commits and Commit Ranges share
......
require 'banzai'
require 'html/pipeline/filter'
require 'uri'
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces commit range references with links.
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces commit references with links.
......
require 'action_controller'
require 'banzai'
require 'gitlab_emoji'
require 'html/pipeline/filter'
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces external issue tracker references with links.
......
require 'banzai'
require 'html/pipeline/filter'
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 Filter
# HTML filter that replaces issue references with links. References to
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces label references with links.
......
require 'banzai'
require 'html/pipeline/filter'
module Banzai
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces merge request references with links. References
......
require 'banzai'
require 'html/pipeline/filter'
module Banzai
......
require 'active_support/core_ext/string/output_safety'
require 'banzai'
require 'html/pipeline/filter'
module Banzai
......
require 'banzai'
require 'html/pipeline/filter'
module Banzai
......
require 'banzai'
require 'html/pipeline/filter'
require 'uri'
......
require 'banzai'
require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter'
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces snippet references with links. References to
......
require 'banzai'
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
......
require 'banzai'
require 'html/pipeline/filter'
module Banzai
......
require 'banzai'
require 'task_list/filter'
module Banzai
......
require 'banzai'
require 'html/pipeline/filter'
require 'uri'
......
require 'banzai'
module Banzai
module Filter
# HTML filter that replaces user or group references with links.
......
require 'banzai'
module Banzai
class LazyReference
def self.load(refs)
......
require 'banzai'
module Banzai
module Pipeline
def self.[](name)
......
require 'banzai'
module Banzai
module Pipeline
class AsciidocPipeline < BasePipeline
......
require 'banzai'
module Banzai
module Pipeline
class AtomPipeline < FullPipeline
......
require 'banzai'
require 'html/pipeline'
module Banzai
......
require 'banzai'
module Banzai
module Pipeline
module CombinedPipeline
......
require 'banzai'
module Banzai
module Pipeline
class DescriptionPipeline < FullPipeline
......
require 'banzai'
module Banzai
module Pipeline
class EmailPipeline < FullPipeline
......
require 'banzai'
module Banzai
module Pipeline
class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
......
require 'banzai'
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
......
require 'banzai'
module Banzai
module Pipeline
class NotePipeline < FullPipeline
......
require 'banzai'
module Banzai
module Pipeline
class PlainMarkdownPipeline < BasePipeline
......
require 'banzai'
module Banzai
module Pipeline
class PostProcessPipeline < BasePipeline
......
require 'banzai'
module Banzai
module Pipeline
class ReferenceExtractionPipeline < BasePipeline
......
require 'banzai'
module Banzai
module Pipeline
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
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
......
......@@ -78,11 +78,13 @@ module Ci
# Parameters:
# id (required) - The ID of a build
# token (required) - The build authorization token
# file (required) - The uploaded file
# file (required) - Artifacts file
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
# file.type - real content type as send in Content-Type
# metadata.path - path to locally stored body (generated by Workhorse)
# metadata.name - filename (generated by Workhorse)
# Headers:
# BUILD-TOKEN (required) - The build authorization token, the same as token
# Body:
......@@ -96,13 +98,20 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
authenticate_build_token!(build)
forbidden!('build is not running') unless build.running?
forbidden!('Build is not running!') unless build.running?
file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
file_to_large! unless file.size < max_artifacts_size
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
artifacts = uploaded_file(:file, artifacts_upload_path)
metadata = uploaded_file(:metadata, artifacts_upload_path)
if build.update_attributes(artifacts_file: file)
present build, with: Entities::Build
bad_request!('Missing artifacts file!') unless artifacts
file_to_large! unless artifacts.size < max_artifacts_size
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
if build.save
present(build, with: Entities::Build)
else
render_validation_error!(build)
end
......@@ -148,6 +157,7 @@ module Ci
not_found! unless build
authenticate_build_token!(build)
build.remove_artifacts_file!
build.remove_artifacts_metadata!
end
end
end
......
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
private
def filename?(line)
line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
'--- /tmp/diffy', '+++ /tmp/diffy')
line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
'+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit
'--- /tmp/diffy', '+++ /tmp/diffy')
end
def identification_type(line)
......
module Gitlab
module GithubImport
class Importer
include Gitlab::ShellAdapter
attr_reader :project, :client
def initialize(project)
......@@ -12,10 +14,7 @@ module Gitlab
end
def execute
import_issues
import_pull_requests
true
import_issues && import_pull_requests && import_wiki
end
private
......@@ -34,6 +33,10 @@ module Gitlab
end
end
end
true
rescue ActiveRecord::RecordInvalid
false
end
def import_pull_requests
......@@ -48,6 +51,10 @@ module Gitlab
import_comments_on_diff(pull_request.number, merge_request)
end
end
true
rescue ActiveRecord::RecordInvalid
false
end
def import_comments(issue_number, noteable)
......@@ -66,6 +73,18 @@ module Gitlab
noteable.notes.create!(comment.attributes)
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
......@@ -20,7 +20,8 @@ module Gitlab
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "github",
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
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 Markdown
class Pipeline
......
require 'banzai'
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
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
provider 'ldapmain'
extern_uid 'my-ldap-id'
end
factory :sent_notification do
project
recipient factory: :user
noteable factory: :issue
reply_key "0123456789abcdef" * 2
end
end
......@@ -30,6 +30,7 @@ FactoryGirl.define do
name 'test'
ref 'master'
tag false
created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
......@@ -42,6 +43,10 @@ FactoryGirl.define do
commit factory: :ci_commit
trait :canceled do
status 'canceled'
end
after(:build) do |build, evaluator|
build.project = build.commit.project
end
......@@ -54,5 +59,11 @@ FactoryGirl.define do
factory :ci_build_tag do
tag true
end
factory :ci_build_with_trace do
after(:create) do |build, evaluator|
build.trace = 'BUILD TRACE'
end
end
end
end
......@@ -80,7 +80,11 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
it { expect(page).to have_content 'Download artifacts' }
it 'has button to download artifacts' do
page.within('.artifacts') do
expect(page).to have_content 'Download'
end
end
end
end
......@@ -111,7 +115,7 @@ describe "Builds" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Download artifacts'
page.within('.artifacts') { click_link 'Download' }
end
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
......
......@@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do
end
end
context 'default pipeline' do
before(:all) do
@feat = MarkdownFeature.new
before(:all) do
@feat = MarkdownFeature.new
# `markdown` helper expects a `@project` variable
@project = @feat.project
# `markdown` helper expects a `@project` variable
@project = @feat.project
end
context 'default pipeline' do
before(:all) do
@html = markdown(@feat.raw_markdown)
end
......@@ -221,6 +223,57 @@ describe 'GitLab Markdown', feature: true do
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
def current_user
@feat.user
......
......@@ -230,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [ ] Incomplete sub-task 2
- [x] Complete sub-task 1
- [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
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
helper.instance_variable_set(:@project_wiki, @wiki)
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)
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)
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
it { is_expected.to have_body_text /View Commit/ }
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
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
......@@ -115,6 +123,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
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
is_expected.to have_body_text /Click here to set your password/
......@@ -134,7 +143,6 @@ describe Notify do
end
end
describe 'for users that signed up, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
......@@ -144,6 +152,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
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
is_expected.not_to have_body_text /password/
......@@ -157,6 +166,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
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
is_expected.to deliver_to key.user.email
......@@ -181,6 +191,7 @@ describe Notify do
subject { Notify.new_email_email(email.id) }
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
is_expected.to deliver_to email.user.email
......@@ -227,6 +238,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
......@@ -253,6 +265,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
......@@ -283,6 +296,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
......@@ -319,6 +333,7 @@ describe Notify do
it_behaves_like 'an assignee email'
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 "an unsubscribeable thread"
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
......@@ -345,6 +360,7 @@ describe Notify do
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 "an unsubscribeable thread"
it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/
......@@ -357,6 +373,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
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 "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
......@@ -387,6 +404,7 @@ describe Notify do
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 "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
......@@ -417,6 +435,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
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 "an unsubscribeable thread"
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
......@@ -446,6 +465,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
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
is_expected.to have_subject /Project was moved/
......@@ -468,6 +488,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
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
is_expected.to have_subject /Access to project was granted/
......@@ -518,6 +539,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit'
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
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
......@@ -538,6 +560,7 @@ describe Notify do
it_behaves_like 'a note email'
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 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
......@@ -558,6 +581,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
......@@ -579,6 +603,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
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
is_expected.to have_subject /Access to group was granted/
......@@ -607,6 +632,7 @@ describe Notify do
subject { ActionMailer::Base.deliveries.last }
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
is_expected.to deliver_to 'new-email@mail.com'
......@@ -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) }
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
sender = subject.header[:from].addrs[0]
......@@ -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) }
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
sender = subject.header[:from].addrs[0]
......@@ -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) }
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
sender = subject.header[:from].addrs[0]
......@@ -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) }
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
sender = subject.header[:from].addrs[0]
......@@ -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) }
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
sender = subject.header[:from].addrs[0]
......@@ -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) }
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
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'
describe Ci::Build, models: true do
......@@ -368,21 +343,75 @@ describe Ci::Build, models: true do
end
end
describe :download_url do
subject { build.download_url }
describe :artifacts_download_url do
subject { build.artifacts_download_url }
it "should be nil if artifact doesn't exist" do
build.update_attributes(artifacts_file: nil)
is_expected.to be_nil
end
it 'should be nil if artifact exist' do
it 'should not be nil if artifact exist' do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
build.update_attributes(artifacts_file: gif)
is_expected.to_not be_nil
end
end
describe :artifacts_browse_url do
subject { build.artifacts_browse_url }
it "should be nil if artifacts browser is unsupported" do
allow(build).to receive(:artifacts_browser_supported?).and_return(false)
is_expected.to be_nil
end
it 'should not be nil if artifacts browser is supported' do
allow(build).to receive(:artifacts_browser_supported?).and_return(true)
is_expected.to_not be_nil
end
end
describe :artifacts? do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
before { build.update_attributes(artifacts_file: nil) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
before do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
build.update_attributes(artifacts_file: gif)
end
it { is_expected.to be_truthy }
end
end
describe :artifacts_browser_supported? do
subject { build.artifacts_browser_supported? }
context 'artifacts metadata does not exist' do
it { is_expected.to be_falsy }
end
context 'artifacts archive is a zip file and metadata exists' do
before do
fixture_dir = Rails.root + 'spec/fixtures/'
archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip',
'application/zip')
metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz',
'application/x-gzip')
build.update_attributes(artifacts_file: archive)
build.update_attributes(artifacts_metadata: metadata)
end
it { is_expected.to be_truthy }
end
end
describe :repo_url do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
......
......@@ -36,6 +36,13 @@ describe ProjectWiki, models: true do
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
it "contains a Gollum::Wiki instance" do
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'
describe API::API, api: true do
describe API::CommitStatus, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
......@@ -12,6 +12,10 @@ describe API::API, api: true do
let(:commit_status) { create(:commit_status, commit: ci_commit) }
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
let(:statuses_id) { json_response.map { |status| status['id'] } }
......
......@@ -32,6 +32,10 @@ describe API::API, api: true do
before { project.team << [user, :reporter] }
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
it "should return an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
......
......@@ -210,6 +210,52 @@ describe Ci::API::API do
end
end
context 'should post artifacts file and metadata file' do
let!(:artifacts) { file_upload }
let!(:metadata) { file_upload2 }
let(:stored_artifacts_file) { build.reload.artifacts_file.file }
let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
before do
build.run!
post(post_url, post_data, headers_with_token)
end
context 'post data accelerated by workhorse is correct' do
let(:post_data) do
{ 'file.path' => artifacts.path,
'file.name' => artifacts.original_filename,
'metadata.path' => metadata.path,
'metadata.name' => metadata.original_filename }
end
it 'responds with valid status' do
expect(response.status).to eq(201)
end
it 'stores artifacts and artifacts metadata' do
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
end
end
context 'no artifacts file in post data' do
let(:post_data) do
{ 'metadata' => metadata }
end
it 'is expected to respond with bad request' do
expect(response.status).to eq(400)
end
it 'does not store metadata' do
expect(stored_metadata_file).to be_nil
end
end
end
context "should fail to post too large artifact" do
before do
build.run!
......
# 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
end
end
def project_wiki
@project_wiki ||= ProjectWiki.new(project, user)
end
def issue
@issue ||= create(:issue, project: project)
end
......
......@@ -66,6 +66,24 @@ module MarkdownMatchers
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
matcher :reference_users do
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