Commit 51b8b871 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' into issue_14800

# Conflicts:
#	app/assets/stylesheets/framework/variables.scss
parents 824fecb7 67136007
......@@ -132,8 +132,9 @@ linters:
SpaceAroundOperator:
enabled: false
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: false
enabled: true
StringQuotes:
enabled: false
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased)
- Improved Markdown rendering performance !3389 (Yorick Peterse)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
- Expose label description in API (Mariusz Jachimowicz)
- Allow back dating on issues when created through the API
- Fix avatar stretching by providing a cropping feature
- Fix raw/rendered diff producing different results on merge requests !3450
- Add endpoints to archive or unarchive a project !3372
- Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
- Add default scope to projects to exclude projects pending deletion
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Fix creation of merge requests for orphaned branches (Stan Hu)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
v 8.6.4
- Don't attempt to fetch any tags from a forked repo (Stan Hu)
v 8.6.3
- Mentions on confidential issues doesn't create todos for non-members. !3374
- Destroy related todos when an Issue/MR is deleted. !3376
- Fix error 500 when target is nil on todo list. !3376
- Fix copying uploads when moving issue to another project. !3382
- Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432
- Fix raw/rendered diff producing different results on merge requests. !3450
- Fix commit comment alignment (Stan Hu). !3466
- Fix Error 500 when searching for a comment in a project snippet. !3468
- Allow temporary email as notification email. !3477
- Fix issue with dropdowns not selecting values. !3478
- Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280
v 8.6.2
- Fix dropdown alignment. !3298
......@@ -108,6 +128,7 @@ v 8.6.0
- Add main language of a project in the list of projects (Tiago Botelho)
- Add #upcoming filter to Milestone filter (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
- Remove fork link closes all merge requests opened on source project (Florent Baldino)
- Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted
- Continue parameters are checked to ensure redirection goes to the same instance
......
......@@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
......
......@@ -652,7 +652,7 @@ GEM
redis-store (~> 1.1.0)
redis-store (1.1.7)
redis (>= 2.2)
request_store (1.2.1)
request_store (1.3.0)
rerun (0.11.0)
listen (~> 3.0)
responders (2.1.1)
......@@ -1011,7 +1011,7 @@ DEPENDENCIES
redcarpet (~> 3.3.3)
redis-namespace
redis-rails (~> 4.0.0)
request_store (~> 1.2.0)
request_store (~> 1.3.0)
rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 1.10.1)
......
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
......@@ -34,7 +34,7 @@ class @AwardsHandler
$("#emoji_search").focus()
else
$('.js-add-award').addClass "is-loading"
$.get "/emojis", (response) =>
$.get @get_emojis_url, (response) =>
$('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
......
......@@ -344,7 +344,7 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS)
field.remove()
......@@ -376,6 +376,8 @@ class GitLabDropdown
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
else
field.val value
return selectedObject
......
((w) ->
notificationGranted = (message, opts, onclick) ->
notification = new Notification(message, opts)
if onclick
notification.onclick = onclick
notifyPermissions = ->
if 'Notification' of window
Notification.requestPermission()
notifyMe = (message, body, icon, onclick) ->
opts =
body: body
icon: icon
# Let's check if the browser supports notifications
if !('Notification' of window)
# do nothing
else if Notification.permission == 'granted'
# If it's okay let's create a notification
notificationGranted message, opts, onclick
else if Notification.permission != 'denied'
Notification.requestPermission (permission) ->
# If the user accepts, let's create a notification
if permission == 'granted'
notificationGranted message, opts, onclick
w.notify = notifyMe
w.notifyPermissions = notifyPermissions
) window
......@@ -2,13 +2,18 @@ class @MergeRequestWidget
# Initialize MergeRequestWidget behavior
#
# check_enable - Boolean, whether to check automerge status
# url_to_automerge_check - String, URL to use to check automerge status
# current_status - String, current automerge status
# ci_enable - Boolean, whether a CI service is enabled
# url_to_ci_check - String, URL to use to check CI status
# merge_check_url - String, URL to use to check automerge status
# ci_status_url - String, URL to use to check CI status
#
constructor: (@opts) ->
modal = $('#modal_merge_info').modal(show: false)
$('#modal_merge_info').modal(show: false)
@firstCICheck = true
@readyForCICheck = true
clearInterval @fetchBuildStatusInterval
@pollCIStatus()
notifyPermissions()
mergeInProgress: (deleteSourceBranch = false)->
$.ajax
......@@ -27,18 +32,57 @@ class @MergeRequestWidget
dataType: 'json'
getMergeStatus: ->
$.get @opts.url_to_automerge_check, (data) ->
$.get @opts.merge_check_url, (data) ->
$('.mr-state-widget').replaceWith(data)
getCiStatus: ->
if @opts.ci_enable
$.get @opts.url_to_ci_check, (data) =>
this.showCiState data.status
ciLabelForStatus: (status) ->
if status == 'success'
'passed'
else
status
pollCIStatus: ->
@fetchBuildStatusInterval = setInterval ( =>
return if not @readyForCICheck
@getCIStatus(true)
@readyForCICheck = false
), 5000
getCIStatus: (showNotification) ->
_this = @
$('.ci-widget-fetching').show()
$.getJSON @opts.ci_status_url, (data) =>
@readyForCICheck = true
if @firstCICheck
@firstCICheck = false
@opts.ci_status = data.status
if data.status isnt @opts.ci_status
@showCIStatus data.status
if data.coverage
this.showCiCoverage data.coverage
, 'json'
@showCICoverage data.coverage
if showNotification
message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status))
message = message.replace('{{sha}}', data.sha)
message = message.replace('{{title}}', data.title)
notify(
"Build #{@ciLabelForStatus(data.status)}",
message,
@opts.gitlab_icon,
->
@close()
Turbolinks.visit _this.opts.builds_path
)
@opts.ci_status = data.status
showCiState: (state) ->
showCIStatus: (state) ->
$('.ci_widget').hide()
allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"]
if state in allowed_states
......@@ -52,7 +96,7 @@ class @MergeRequestWidget
$('.ci_widget.ci-error').show()
@setMergeButtonClass('btn-danger')
showCiCoverage: (coverage) ->
showCICoverage: (coverage) ->
text = 'Coverage ' + coverage + '%'
$('.ci_widget:visible .ci-coverage').text(text)
......
......@@ -84,12 +84,16 @@ class @MilestoneSelect
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
clicked: (e) ->
clicked: (selected) ->
if $dropdown.hasClass 'js-filter-bulk-update'
return
if $dropdown.hasClass 'js-filter-submit'
$dropdown.parents('form').submit()
if $dropdown.hasClass('js-filter-submit')
if selected.name?
selectedMilestone = selected.name
else
selectedMilestone = ''
Issues.filterResults $dropdown.closest('form')
else
selected = $selectbox
.find('input[type="hidden"]')
......@@ -117,4 +121,4 @@ class @MilestoneSelect
else
$value.html(milestoneLinkNoneTemplate)
$sidebarCollapsedValue.find('span').text('No')
)
\ No newline at end of file
)
......@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( ->
......
......@@ -13,10 +13,10 @@
// Toggle between two states.
.js-toggler-container {
.turn-on { display: block; }
.turn-on { display: block; }
.turn-off { display: none; }
&.on {
.turn-on { display: none; }
.turn-on { display: none; }
.turn-off { display: block; }
}
}
.calender-block {
@media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) {
overflow-x: scroll;
}
}
.user-calendar-activities {
.calendar_onclick_hr {
padding: 0;
......
......@@ -121,7 +121,7 @@ p.time {
text-shadow: none;
}
.thin_area{
.thin_area {
height: 150px;
}
......@@ -148,7 +148,7 @@ li.note {
}
}
.wiki_content code, .readme code{
.wiki_content code, .readme code {
background-color: inherit;
}
......
......@@ -3,7 +3,7 @@
vertical-align: top;
}
@media (min-width: $screen-sm-min) {
@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
.dropdown-menu-toggle {
......
......@@ -91,7 +91,7 @@ label {
}
.form-control::-webkit-input-placeholder {
color: #7f8fa4;
color: $gl-placeholder-color;
}
.input-group {
......
......@@ -33,10 +33,15 @@
background: $color;
}
.complex-sidebar .nav-primary {
border-right: 1px solid lighten($color, 3%);
}
.sidebar-wrapper {
background: $color-darker;
.sidebar-user {
border-top: 1px solid lighten($color, 3%);
background: $color-darker;
color: $color-light;
......@@ -62,7 +67,6 @@
.count {
color: $color-light;
background: $color-dark;
}
}
......
......@@ -36,7 +36,7 @@ header {
padding: 0;
.nav > li > a {
color: #7f8fa4;
color: $gl-icon-color;
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
......@@ -62,7 +62,7 @@ header {
background-color: #eee;
}
&.active {
color: #7f8fa4;
color: $gl-icon-color;
}
}
}
......@@ -81,14 +81,14 @@ header {
font-size: 19px;
line-height: $header-height;
font-weight: normal;
color: #4c4e54;
color: $gl-text-color;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
a {
color: #4c4e54;
color: $gl-text-color;
&:hover {
text-decoration: underline;
}
......@@ -123,11 +123,11 @@ header {
}
@mixin collapsed-header {
margin-left: $sidebar_collapsed_width;
margin-left: 40px;
}
.header-collapsed {
margin-left: $sidebar_collapsed_width;
margin-left: 40px;
@media (min-width: $screen-md-min) {
@include collapsed-header;
......
......@@ -107,7 +107,7 @@
}
.page-title {
.note_created_ago, .new-issue-link {
.note-created-ago, .new-issue-link {
display: none;
}
}
......@@ -116,7 +116,7 @@
display: none;
}
aside:not(.right-sidebar){
aside:not(.right-sidebar) {
display: none;
}
......
......@@ -56,6 +56,17 @@
}
}
.nav-search {
display: inline-block;
width: 50%;
padding: 11px 0;
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
width: 100%;
}
}
.nav-links {
display: inline-block;
width: 50%;
......@@ -100,6 +111,7 @@
> form {
display: inline-block;
margin-top: -1px;
}
.icon-label {
......@@ -110,7 +122,7 @@
height: 34px;
display: inline-block;
position: relative;
top: 1px;
top: 2px;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
......
......@@ -51,7 +51,7 @@
padding: 10px 15px;
}
.select2-drop{
.select2-drop {
color: #7f8fa4;
}
......
......@@ -144,7 +144,7 @@
}
a {
padding: 7px 15px;
padding: 7px 12px;
font-size: $gl-font-size;
line-height: 24px;
color: $gray;
......@@ -169,10 +169,12 @@
}
.count {
float: right;
background: #eee;
padding: 0 8px;
@include border-radius(6px);
&:before {
content: '(';
}
&:after {
content: ')';
}
}
&.back-link i {
......@@ -191,6 +193,27 @@
}
}
.expand-nav a {
color: $gl-icon-color;
width: 60px;
position: fixed;
top: 0;
left: 0;
font-size: 20px;
background: #fff;
height: 59px;
text-align: center;
line-height: 59px;
border-bottom: 1px solid #eee;
transition-duration: .3s;
outline: none;
z-index: 100;
&:hover {
text-decoration: none;
}
}
.collapse-nav a {
width: $sidebar_width;
position: fixed;
......@@ -210,55 +233,12 @@
}
.page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
.header-logo {
width: $sidebar_collapsed_width;
a {
padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
}
}
}
.nav-sidebar {
width: $sidebar_collapsed_width;
li {
width: auto;
a {
span {
display: none;
}
}
}
}
.collapse-nav a {
width: $sidebar_collapsed_width;
}
.sidebar-user {
padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
.username {
display: none;
}
}
display: none;
}
}
.page-sidebar-expanded {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
......@@ -309,3 +289,48 @@
padding-right: $sidebar_collapsed_width;
}
}
.complex-sidebar {
display: inline-block;
.nav-primary {
width: 61px;
float: left;
height: 100vh;
.nav-sidebar {
width: 60px;
li a {
width: 60px;
span {
display: none;
}
}
}
}
.nav-secondary {
$nav-secondary-width: 168px;
float: left;
width: $nav-secondary-width;
.nav-sidebar {
width: $nav-secondary-width;
li {
width: $nav-secondary-width;
a {
width: $nav-secondary-width;
i {
display: none;
}
}
}
}
}
}
......@@ -56,8 +56,8 @@ $component-active-bg: $brand-info;
//##
$input-color: $text-color;
$input-border: #e7e9ed;
$input-border-focus: #7f8fa4;
$input-border: $border-color;
$input-border-focus: $focus-border-color;
$legend-color: $text-color;
......
......@@ -11,6 +11,7 @@ $gutter_inner_width: 258px;
* UI elements
*/
$border-color: #efeff1;
$focus-border-color: #3aabf0;
$table-border-color: #eef0f2;
$background-color: #faf9f9;
......@@ -26,6 +27,7 @@ $gl-text-orange: #d90;
$gl-link-color: #3084bb;
$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
$gl-icon-color: $gl-placeholder-color;
$gl-gray: $gl-text-color;
$gl-header-color: $gl-title-color;
......@@ -178,7 +180,7 @@ $dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
......@@ -207,3 +209,11 @@ $location-badge-bg: $gray-normal;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed;
$location-icon-active-color: #807e7e;
/*
* Notes
*/
$notes-light-color: #8e8e8e;
$notes-action-color: #c3c3c3;
$notes-role-color: #8e8e8e;
$notes-role-border-color: #e4e4e4;
......@@ -37,7 +37,7 @@
height: 300px;
overflow-y: scroll;
input.emoji-search{
input.emoji-search {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
......
......@@ -42,7 +42,7 @@
}
}
.loading{
.loading {
font-size: 20px;
}
......
.commit-title{
.commit-title {
display: block;
}
.commit-author, .commit-committer{
.commit-author, .commit-committer {
display: block;
color: #999;
font-weight: normal;
font-style: italic;
}
.commit-author strong, .commit-committer strong{
.commit-author strong, .commit-committer strong {
font-weight: bold;
font-style: normal;
}
......@@ -74,7 +74,7 @@
color: $gl-text-red;
}
}
.edit-file{
.edit-file {
a {
color: $gl-text-color;
}
......
.commits-compare-switch{
.commits-compare-switch {
@include btn-default;
@include btn-white;
background: image-url("switch_icon.png") no-repeat center center;
......
.file-editor {
#editor{
#editor {
border: none;
@include border-radius(0);
height: 500px;
......
......@@ -43,10 +43,6 @@
.md {
color: #7f8fa4;
font-size: $gl-font-size;
iframe.twitter-share-button {
vertical-align: bottom;
}
}
pre {
......
.ci-body {
.incorrect-syntax{
.incorrect-syntax {
font-size: 19px;
color: red;
}
.correct-syntax{
.correct-syntax {
font-size: 19px;
color: #47a447;
}
......
......@@ -36,7 +36,7 @@
}
}
.login-box{
.login-box {
background: #fafafa;
border-radius: 10px;
box-shadow: 0 0 2px #ccc;
......
......@@ -22,7 +22,7 @@ ul.notes {
margin-left: 55px;
}
.note_created_ago, .note-updated-at {
.note-created-ago, .note-updated-at {
white-space: nowrap;
}
......@@ -39,53 +39,6 @@ ul.notes {
}
}
.discussion-header,
.note-header {
@extend .cgray;
a:hover {
text-decoration: none;
}
.avatar {
float: left;
margin-right: 10px;
}
.discussion-last-update,
.note-last-update {
&:before {
content: "\00b7";
}
a {
color: $gl-gray;
&:hover {
text-decoration: underline;
}
}
}
.author {
color: #4c4e54;
margin-right: 3px;
&:hover {
color: $gl-link-color;
}
}
.author-username {
}
.note-role {
float: right;
margin-top: 1px;
border: 1px solid #bbb;
background-color: transparent;
color: $gl-gray;
}
}
.discussion-body {
padding-top: 15px;
}
......@@ -198,40 +151,88 @@ ul.notes {
border-width: 1px 0;
padding-top: 0;
vertical-align: top;
&.parallel{
&.parallel {
border-width: 1px;
}
}
}
}
.discussion-header,
.note-header {
a {
color: inherit;
&:hover {
color: $gl-link-color;
text-decoration: none;
}
}
.author_link {
font-weight: 600;
}
}
.note-headline-light,
.discussion-headline-light {
color: $notes-light-color;
}
/**
* Actions for Discussions/Notes
*/
.discussion,
.note {
.discussion-actions,
.note-actions {
float: right;
margin-left: 10px;
.discussion-actions,
.note-actions {
float: right;
margin-left: 10px;
color: $notes-action-color;
}
a {
margin-left: 5px;
color: $gl-gray;
.note-action-button,
.discussion-action-button {
display: inline-block;
margin-left: 10px;
line-height: 24px;
i.fa {
font-size: 16px;
line-height: 16px;
}
.fa {
position: relative;
top: 1px;
font-size: 17px;
}
&:hover {
@extend .cgray;
&.danger { @extend .cred; }
}
}
.fa-trash-o {
top: 0;
font-size: 16px;
}
}
.discussion-toggle-button {
line-height: 20px;
font-size: 13px;
.fa {
margin-right: 3px;
font-size: 10px;
line-height: 18px;
vertical-align: top;
}
}
.note-role {
position: relative;
top: -2px;
display: inline-block;
padding-left: 4px;
padding-right: 4px;
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
border: 1px solid $notes-role-border-color;
border-radius: $border-radius-base;
}
.diff-file .note .note-actions {
right: 0;
top: 0;
......
......@@ -6,7 +6,7 @@
.navbar-nav {
li {
.badge.todos-pending-count {
background-color: #7f8fa4;
background-color: $gl-icon-color;
margin-top: -5px;
font-weight: normal;
}
......
......@@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:require_two_factor_authentication,
:two_factor_grace_period,
:gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text,
:help_page_text,
:home_page_url,
......
......@@ -224,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def ci_status
ci_service = @merge_request.source_project.ci_service
status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch)
ci_commit = @merge_request.ci_commit
if ci_commit
status = ci_commit.status
coverage = ci_commit.try(:coverage)
else
ci_service = @merge_request.source_project.ci_service
status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage)
coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
if ci_service.respond_to?(:commit_coverage)
coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
end
end
response = {
title: merge_request.title,
sha: merge_request.last_commit_short_sha,
status: status,
coverage: coverage
}
......
......@@ -71,7 +71,7 @@ class ProjectsController < Projects::ApplicationController
def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project)
if @project.unlink_fork
if ::Projects::UnlinkForkService.new(@project, current_user).execute
flash[:notice] = 'The fork relationship has been removed.'
end
end
......
......@@ -3,10 +3,6 @@ module ApplicationSettingsHelper
current_application_settings.gravatar_enabled?
end
def twitter_sharing_enabled?
current_application_settings.twitter_sharing_enabled?
end
def signup_enabled?
current_application_settings.signup_enabled?
end
......
......@@ -216,7 +216,7 @@ module EventsHelper
end
def event_row_class(event)
if event.body? || event.created_project?
if event.body?
"event-block"
else
"event-inline"
......
......@@ -12,7 +12,6 @@
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
......@@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
......
......@@ -74,14 +74,14 @@ class Commit
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
@reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{7,40})
}x
end
def self.link_reference_pattern
super("commit", /(?<commit>\h{7,40})/)
@link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
end
def to_reference(from_project = nil)
......
......@@ -43,14 +43,14 @@ class CommitRange
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
@reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{STRICT_PATTERN})
}x
end
def self.link_reference_pattern
super("compare", /(?<commit_range>#{PATTERN})/)
@link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/)
end
# Initialize a CommitRange
......
......@@ -19,6 +19,7 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
......@@ -41,7 +42,7 @@ module Issuable
scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) }
delegate :name,
:email,
......
......@@ -31,7 +31,7 @@ class ExternalIssue
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
%r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil)
......
......@@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
}x
end
def self.link_reference_pattern
super("issues", /(?<issue>\d+)/)
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
def to_reference(from_project = nil)
......
......@@ -56,7 +56,7 @@ class Label < ActiveRecord::Base
# This pattern supports cross-project references.
#
def self.reference_pattern
%r{
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
......
......@@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :from_project, ->(project) { where(source_project_id: project.id) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
......@@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
}x
end
def self.link_reference_pattern
super("merge_requests", /(?<merge_request>\d+)/)
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end
# Returns all the merge requests from an ActiveRecord:Relation.
......
......@@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base
end
def self.link_reference_pattern
super("milestones", /(?<milestone>\d+)/)
@link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end
def self.upcoming
......@@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base
def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]")
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
"[#{escaped_title}](#{url})"
......
......@@ -311,7 +311,7 @@ class Note < ActiveRecord::Base
for_merge_request? && for_diff_line?
end
def for_project_snippet?
def for_snippet?
noteable_type == "Snippet"
end
......
......@@ -206,6 +206,8 @@ class Project < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
# Scopes
default_scope { where(pending_delete: false) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
......@@ -469,7 +471,7 @@ class Project < ActiveRecord::Base
end
def web_url
Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self)
Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self)
end
def web_url_without_protocol
......@@ -590,7 +592,7 @@ class Project < ActiveRecord::Base
if avatar.present?
[gitlab_config.url, avatar.url].join
elsif avatar_in_git
Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self)
end
end
......@@ -929,16 +931,6 @@ class Project < ActiveRecord::Base
self.builds_enabled = true
end
def unlink_fork
if forked?
forked_from_project.lfs_objects.find_each do |lfs_object|
lfs_object.projects << self
end
forked_project_link.destroy
end
end
def any_runners?(&block)
if runners.active.any?(&block)
return true
......
......@@ -20,7 +20,7 @@
#
class GitlabIssueTrackerService < IssueTrackerService
include Gitlab::Application.routes.url_helpers
include Gitlab::Routing.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
......
......@@ -21,7 +21,7 @@
class JiraService < IssueTrackerService
include HTTParty
include Gitlab::Application.routes.url_helpers
include Gitlab::Routing.url_helpers
DEFAULT_API_VERSION = 2
......
......@@ -72,7 +72,7 @@ class Repository
return @has_visible_content unless @has_visible_content.nil?
@has_visible_content = cache.fetch(:has_visible_content?) do
raw_repository.branch_count > 0
branch_count > 0
end
end
......@@ -173,7 +173,7 @@ class Repository
end
def branch_names
cache.fetch(:branch_names) { raw_repository.branch_names }
cache.fetch(:branch_names) { branches.map(&:name) }
end
def tag_names
......@@ -191,7 +191,7 @@ class Repository
end
def branch_count
@branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count }
@branch_count ||= cache.fetch(:branch_count) { branches.size }
end
def tag_count
......@@ -239,7 +239,7 @@ class Repository
def expire_branches_cache
cache.expire(:branch_names)
@branches = nil
@local_branches = nil
end
def expire_cache(branch_name = nil, revision = nil)
......@@ -614,10 +614,14 @@ class Repository
refs_contains_sha('tag', sha)
end
def branches
@branches ||= raw_repository.branches
def local_branches
@local_branches ||= rugged.branches.each(:local).map do |branch|
Gitlab::Git::Branch.new(branch.name, branch.target)
end
end
alias_method :branches, :local_branches
def tags
@tags ||= raw_repository.tags
end
......@@ -820,7 +824,7 @@ class Repository
end
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref})
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
......
......@@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base
#
# This pattern supports cross-project references.
def self.reference_pattern
%r{
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<snippet>\d+)
}x
end
def self.link_reference_pattern
super("snippets", /(?<snippet>\d+)/)
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
def to_reference(from_project = nil)
......
......@@ -408,6 +408,8 @@ class User < ActiveRecord::Base
end
def owns_notification_email
return if self.temp_oauth_email?
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end
......
......@@ -43,7 +43,7 @@ module Issues
def create_new_issue
new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
project: @new_project, author: @old_issue.author,
description: unfold_references(@old_issue.description) }
description: rewrite_content(@old_issue.description) }
new_params = @old_issue.serializable_hash.merge(new_params)
CreateService.new(@new_project, @current_user, new_params).execute
......@@ -53,7 +53,7 @@ module Issues
@old_issue.notes.find_each do |note|
new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue,
note: unfold_references(new_note.note),
note: rewrite_content(new_note.note),
created_at: note.created_at,
updated_at: note.updated_at }
......@@ -61,6 +61,18 @@ module Issues
end
end
def rewrite_content(content)
return unless content
rewriters = [Gitlab::Gfm::ReferenceRewriter,
Gitlab::Gfm::UploadsRewriter]
rewriters.inject(content) do |text, klass|
rewriter = klass.new(text, @old_project, @current_user)
rewriter.rewrite(@new_project)
end
end
def close_issue
close_service = CloseService.new(@old_project, @current_user)
close_service.execute(@old_issue, notifications: false, system_note: false)
......@@ -78,20 +90,12 @@ module Issues
direction: :to)
end
def unfold_references(content)
return unless content
rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
@current_user)
rewriter.rewrite(@new_project)
def mark_as_moved
@old_issue.update(moved_to: @new_issue)
end
def notify_participants
notification_service.issue_moved(@old_issue, @new_issue, @current_user)
end
def mark_as_moved
@old_issue.update(moved_to: @new_issue)
end
end
end
module Projects
class UnlinkForkService < BaseService
def execute
return unless @project.forked?
@project.forked_from_project.lfs_objects.find_each do |lfs_object|
lfs_object.projects << @project
end
merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project)
merge_requests.each do |mr|
MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
@project.forked_project_link.destroy
end
end
end
......@@ -95,17 +95,19 @@ class SystemHooksService
end
def project_member_data(model)
project = model.project || Project.unscoped.find(model.source_id)
{
project_name: model.project.name,
project_path: model.project.path,
project_path_with_namespace: model.project.path_with_namespace,
project_id: model.project.id,
user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
user_id: model.user.id,
access_level: model.human_access,
project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
project_name: project.name,
project_path: project.path,
project_path_with_namespace: project.path_with_namespace,
project_id: project.id,
user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
user_id: model.user.id,
access_level: model.human_access,
project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase
}
end
......
......@@ -224,7 +224,7 @@ class SystemNoteService
#
# "Started branch `issue-branch-button-201`"
def self.new_issue_branch(issue, project, author, branch)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})"
......
......@@ -123,7 +123,7 @@ class TodoService
def handle_note(note, author)
# Skip system notes, and notes on project snippet
return if note.system? || note.for_project_snippet?
return if note.system? || note.for_snippet?
project = note.project
target = note.noteable
......@@ -170,14 +170,30 @@ class TodoService
end
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users.select do |user|
user.can?(:read_project, project)
end
mentioned_users = target.mentioned_users
mentioned_users = reject_users_without_access(mentioned_users, project, target)
mentioned_users.delete(author)
mentioned_users.uniq
end
def reject_users_without_access(users, project, target)
if target.is_a?(Note) && target.for_issue?
target = target.noteable
end
if target.is_a?(Issue)
select_users(users, :read_issue, target)
else
select_users(users, :read_project, project)
end
end
def select_users(users, ability, subject)
users.select do |user|
user.can?(ability.to_sym, subject)
end
end
def pending_todos(user, criteria = {})
valid_keys = [:project_id, :target_id, :target_type, :commit_id]
user.todos.pending.where(criteria.slice(*valid_keys))
......
# encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base
include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
storage :file
attr_accessor :project, :secret
def initialize(project, secret = self.class.generate_secret)
def initialize(project, secret = nil)
@project = project
@secret = secret
@secret = secret || self.class.generate_secret
end
def base_dir
......@@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end
def self.generate_secret
SecureRandom.hex
end
def secure_url
File.join("/uploads", @secret, file.filename)
end
def to_markdown
to_h[:markdown]
end
def to_h
filename = image? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]")
......@@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base
markdown: markdown
}
end
def self.generate_secret
SecureRandom.hex
end
end
......@@ -76,13 +76,6 @@
= f.label :gravatar_enabled do
= f.check_box :gravatar_enabled
Gravatar enabled
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :twitter_sharing_enabled do
= f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block'
Twitter enabled
%span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter
.form-group
= f.label :default_projects_limit, class: 'control-label col-sm-2'
.col-sm-10
......
......@@ -15,7 +15,7 @@
%td
- if project
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
= link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
%td
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
......
.admin-dashboard
.admin-dashboard.prepend-top-default
.row
.col-md-4
%h4 Statistics
......
- page_title "Deploy Keys"
.panel.panel-default
.panel.panel-default.prepend-top-default
.panel-heading
Public deploy keys (#{@deploy_keys.count})
.controls
......
- css_class = '' unless local_assigns[:css_class]
- css_class += ' no-description' if group.description.blank?
%li.group-row{ class: css_class }
.controls.hidden-xs
= link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm'
= link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove'
.stats
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
%span
= icon('users')
= number_with_delimiter(group.users.count)
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
= image_tag group_icon(group), class: 'avatar s40 hidden-xs'
.title
= link_to [:admin, group], class: 'group-name' do
= group.name
- if group.description.present?
.description
= markdown(group.description, pipeline: :description)
- page_title "Groups"
%h3.page-title
Groups (#{number_with_delimiter(@groups.total_count)})
= link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
%p.light
Group allows you to keep projects organized.
Use groups for uniting related projects.
%hr
= form_tag admin_groups_path, method: :get, class: 'form-inline' do
= hidden_field_tag :sort, @sort
.form-group
= text_field_tag :name, params[:name], class: "form-control"
= button_tag "Search", class: "btn submit btn-primary"
.top-area
.nav-search
= form_tag admin_groups_path, method: :get, class: 'form-inline' do
= hidden_field_tag :sort, @sort
= text_field_tag :name, params[:name], class: "form-control"
= button_tag "Search", class: "btn submit btn-primary"
.pull-right
.nav-controls
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%span.light
......@@ -33,34 +32,10 @@
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
= link_to 'New Group', new_admin_group_path, class: "btn btn-new"
%hr
%ul.bordered-list
%ul.content-list
- @groups.each do |group|
%li
.clearfix
.pull-right.prepend-top-10
= link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm"
= link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove"
%h4
= link_to [:admin, group] do
%span{ class: visibility_level_color(group.visibility_level) }
= visibility_level_icon(group.visibility_level)
%i.fa.fa-folder
= group.name
&rarr;
%span.monospace
%strong #{group.path}/
.clearfix
%p
= truncate group.description, length: 150
.clearfix
%p.light
#{pluralize(group.members.size, 'member')}, #{pluralize(group.projects.count, 'project')}
= render 'group', group: group
= paginate @groups, theme: "gitlab"
- page_title "Labels"
= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
New label
%h3.page-title
Labels
%div
= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
New label
%h3.page-title
Labels
%hr
.labels
......@@ -13,4 +15,4 @@
- else
.light-well
.nothing-here-block There are no labels yet
%p.lead
%p.lead.prepend-top-default
%span
To register a new runner you should enter the following registration token.
With this token the runner will request a unique runner token and use that for future communication.
......
......@@ -10,7 +10,10 @@
(removed)
%span.todo-label
= todo_action_name(todo)
= todo_target_link(todo)
- if todo.target
= todo_target_link(todo)
- else
(removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
......
......@@ -7,21 +7,3 @@
= link_to_project event.project
- else
= event.project_name
- if !event.project.private? && twitter_sharing_enabled?
.event-body{"data-user-is" => event.author_id}
.event-note
.md
%p
Congratulations! Why not share your accomplishment with the world?
%a.twitter-share-button{ |
href: "https://twitter.com/share", |
"data-url" => event.project.web_url, |
"data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", |
"data-size" => "medium", |
"data-related" => "gitlab", |
"data-hashtags" => "gitlab", |
"data-count" => "none"}
Tweet
%script{src: "//platform.twitter.com/widgets.js"}
- if nav_menu_collapsed?
= link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
- else
= link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
= render "layouts/broadcast"
.expand-nav
= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
%a#logo
......@@ -8,15 +10,19 @@
.gitlab-text-container
%h3 GitLab
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
= render 'layouts/nav/dashboard'
- primary_sidebar = current_user ? 'dashboard' : 'explore'
- if defined?(sidebar) && sidebar && sidebar != primary_sidebar
.complex-sidebar
.nav-primary
= render "layouts/nav/#{primary_sidebar}"
.nav-secondary
= render "layouts/nav/#{sidebar}"
- else
= render 'layouts/nav/explore'
= render "layouts/nav/#{primary_sidebar}"
.collapse-nav
= render partial: 'layouts/collapse_button'
= link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar"
- if current_user
= link_to current_user, class: 'sidebar-user', title: "Profile" do
= image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
......
......@@ -95,7 +95,7 @@
Spam Logs
%span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw')
%span
......
......@@ -15,12 +15,12 @@
= icon('dashboard fw')
%span
Activity
= nav_link(controller: :groups) do
= nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do
= link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
= nav_link(controller: :milestones) do
= nav_link(path: 'dashboard#milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw')
%span
......@@ -48,7 +48,6 @@
%span
Help
%li.separate-item
= nav_link(controller: :profile) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
......
%ul.nav.nav-sidebar
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
= icon('group fw')
......@@ -42,7 +34,7 @@
%span
Members
- if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "separate-item" }) do
= nav_link do
= link_to edit_group_path(@group), title: 'Settings' do
= icon ('cogs fw')
%span
......
%ul.nav.nav-sidebar
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
= icon('user fw')
......
%ul.nav.nav-sidebar
- if @project.group
= nav_link do
= link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to group
- else
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('bookmark fw')
......@@ -113,7 +98,7 @@
Snippets
- if project_nav_tab? :settings
= nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
= nav_link(html_options: {class: "#{project_tab_class}"}) do
= link_to edit_project_path(@project), title: 'Settings' do
= icon('cogs fw')
%span
......
......@@ -7,8 +7,6 @@
%p
Increase your account's security by enabling two-factor authentication (2FA).
.col-lg-9
%p
Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'}
%p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
......
......@@ -3,25 +3,32 @@
%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)
- can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- can_create_snippet = can?(current_user, :create_snippet, @project)
- if can_create_issue
%li
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
%li
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- if can?(current_user, :create_snippet, @project)
- if can_create_snippet
%li
= link_to new_namespace_project_snippet_path(@project.namespace, @project) do
= icon('file-text-o fw')
New snippet
- if can?(current_user, :push_code, @project)
- if can_create_issue || merge_project || can_create_snippet
%li.divider
- if can?(current_user, :push_code, @project)
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
......@@ -35,13 +42,11 @@
= icon('tags fw')
New tag
- elsif current_user && current_user.already_forked?(@project)
%li.divider
%li
= link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
= icon('file fw')
New file
- elsif can?(current_user, :fork_project, @project)
%li.divider
%li
- continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
notice: edit_in_new_fork_notice,
......
......@@ -39,7 +39,7 @@
%td
= build.name
.pull-right
.label-container
- if build.tags.any?
- build.tags.each do |tag|
%span.label.label-primary
......
- diff = diff_file.diff
- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))
- old_commit_id = diff_refs.first.id
- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
// diff_refs will be nil for orphaned commits (e.g. first commit in repo)
- if diff_refs
- old_commit_id = diff_refs.first.id
- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))
- if diff.renamed_file || diff.new_file || diff.deleted_file
.image
%span.wrap
......
- if @ci_commit
.mr-widget-heading
.ci_widget{class: "ci-#{@ci_commit.status}"}
= ci_status_icon(@ci_commit)
%span
Build
= ci_status_label(@ci_commit)
for
= succeed "." do
= link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace"
%span.ci-coverage
= link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
- %w[success skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) }
= ci_icon_for_status(status)
%span
CI build
= ci_label_for_status(status)
for
- commit = @merge_request.last_commit
= succeed "." do
= link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace"
%span.ci-coverage
= link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
......@@ -43,5 +45,5 @@
:javascript
$(function() {
merge_request_widget.getCiStatus();
merge_request_widget.getCIStatus(false);
});
......@@ -9,12 +9,17 @@
:javascript
var merge_request_widget;
merge_request_widget = new MergeRequestWidget({
url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
var opts = {
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "",
ci_message: "Build {{status}} for \"{{title}}\"",
ci_enable: #{@project.ci_service ? "true" : "false"},
current_status: "#{@merge_request.gitlab_merge_status}",
});
builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if(typeof merge_request_widget === 'undefined') {
merge_request_widget = new MergeRequestWidget(opts);
}
......@@ -5,28 +5,21 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
= link_to_member(note.project, note.author, avatar: false)
.inline.note-headline-light
= "#{note.author.to_reference} commented"
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- if note_editable?(note)
.note-actions
= link_to '#', title: 'Edit comment', class: 'js-note-edit' do
- access = note.project.team.human_max_access(note.author.id)
- if access
%span.note-role
= access
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o')
- unless note.system
- access = note.project.team.human_max_access(note.author.id)
- if access
%span.note-role.label
= access
= link_to_member(note.project, note.author, avatar: false)
%span.author-username
= '@' + note.author.username
%span.note-last-update
%a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
......
- note = discussion_notes.first
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
= link_to_member(@project, note.author, avatar: false)
.inline.discussion-headline-light
= "#{note.author.to_reference} started a discussion"
= link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
on the diff
.discussion-actions
= link_to "#", class: "js-toggle-button" do
= link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
%div
= link_to_member(@project, note.author, avatar: false)
started a discussion
= link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
%strong on the diff
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
%span.discussion-last-update
#{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
#{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
......@@ -3,21 +3,20 @@
- commit_description = commit ? 'commit' : 'a deleted commit'
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
= link_to_member(@project, note.author, avatar: false)
.inline.discussion-headline-light
= "#{note.author.to_reference} started a discussion on #{commit_description}"
- if commit
= link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.discussion-actions
= link_to "#", class: "js-toggle-button" do
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
%div
= link_to_member(@project, note.author, avatar: false)
started a discussion on #{commit_description}
- if commit
= link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
%span.discussion-last-update
#{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
#{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content
- if note.for_diff_line?
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
......
- note = discussion_notes.first
.discussion.js-toggle-container{ class: note.discussion_id }
.discussion-header
= link_to_member(@project, note.author, avatar: false)
.inline.discussion-headline-light
= "#{note.author.to_reference} started a discussion"
on the outdated diff
.discussion-actions
= link_to "#", class: "js-toggle-button" do
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-down
Show/hide discussion
%div
= link_to_member(@project, note.author, avatar: false)
started a discussion on the
%strong outdated diff
%div
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
%span.discussion-last-update
#{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
#{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
.discussion-body.js-toggle-content.hide
= render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
- project = note.project
- note_url = Gitlab::UrlBuilder.new(:note).build(note.id)
- noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row
%h5.note-search-caption.str-truncated
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
commented on
= link_to project.name_with_namespace, project
&middot;
- if note.for_commit?
= link_to project do
= project.name_with_namespace
&middot;
= link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do
Commit #{truncate_sha(note.commit_id)}
= link_to "Commit #{truncate_sha(note.commit_id)}", note_url
- else
= link_to project do
= project.name_with_namespace
&middot;
%span #{note.noteable_type.titleize} ##{note.noteable.iid}
%span #{note.noteable_type.titleize} ##{noteable_identifier}
&middot;
= link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do
= note.noteable.title
= link_to note.noteable.title, note_url
.note-search-result
.term
......
......@@ -7,13 +7,13 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- if params[:author_id]
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id]
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
......
- if params[:label_name]
- if params[:label_name].present?
= hidden_field_tag(:label_name, params[:label_name])
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
......
- if params[:milestone_title]
- if params[:milestone_title].present?
= hidden_field_tag(:milestone_title, params[:milestone_title])
= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
......
......@@ -87,7 +87,7 @@
%div{ class: container_class }
.tab-content
#activity.tab-pane
.gray-content-block.white.second-block
.gray-content-block.calender-block.white.second-block.hidden-xs
%div{ class: container_class }
.user-calendar{data: {href: user_calendar_path}}
%h4.center.light
......
......@@ -15,12 +15,14 @@
- if current_user
:javascript
var get_emojis_url = "#{emojis_path}";
var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
var noteable_type = "#{votable.class.name.underscore}";
var noteable_id = "#{votable.id}";
var aliases = #{AwardEmoji.aliases.to_json};
window.awards_handler = new AwardsHandler(
get_emojis_url,
post_emoji_url,
noteable_type,
noteable_id,
......
......@@ -5,7 +5,7 @@ class ProjectDestroyWorker
def perform(project_id, user_id, params)
begin
project = Project.find(project_id)
project = Project.unscoped.find(project_id)
rescue ActiveRecord::RecordNotFound
return
end
......
......@@ -174,7 +174,6 @@ end
Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
......
class RemoveTodosForDeletedIssues < ActiveRecord::Migration
def up
execute <<-SQL
DELETE FROM todos
WHERE todos.target_type = 'Issue'
AND NOT EXISTS (
SELECT *
FROM issues
WHERE issues.id = todos.target_id
AND issues.deleted_at IS NULL
)
SQL
end
def down
end
end
class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration
def change
add_index :projects, :pending_delete
end
end
class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration
def up
execute <<-SQL
DELETE FROM todos
WHERE todos.target_type = 'MergeRequest'
AND NOT EXISTS (
SELECT *
FROM merge_requests
WHERE merge_requests.id = todos.target_id
AND merge_requests.deleted_at IS NULL
)
SQL
end
def down
end
end
class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration
def change
remove_column :application_settings, :twitter_sharing_enabled, :boolean
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160320204112) do
ActiveRecord::Schema.define(version: 20160331133914) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -418,7 +418,7 @@ ActiveRecord::Schema.define(version: 20160320204112) do
t.integer "iid"
t.integer "updated_by_id"
t.integer "moved_to_id"
t.boolean "confidential", default: false
t.boolean "confidential", default: false
t.datetime "deleted_at"
end
......@@ -745,6 +745,7 @@ ActiveRecord::Schema.define(version: 20160320204112) do
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
......
......@@ -491,6 +491,172 @@ Parameters:
- `id` (required) - The ID of the project to be forked
### Archive a project
Archives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus archiving an already archived project will not change the project.
Status code 201 with the project as body is given when successful, in case the user doesn't
have the proper access rights, code 403 is returned. Status 404 is returned if the project
doesn't exist, or is hidden to the user.
```
POST /projects/:id/archive
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project |
```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
```
Example response:
```json
{
"id": 3,
"description": null,
"default_branch": "master",
"public": false,
"visibility_level": 0,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
"tag_list": [
"example",
"disapora project"
],
"owner": {
"id": 3,
"name": "Diaspora",
"created_at": "2013-09-30T13: 46: 02Z"
},
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
"creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
"id": 3,
"name": "Diaspora",
"owner_id": 1,
"path": "diaspora",
"updated_at": "2013-09-30T13: 46: 02Z"
},
"permissions": {
"project_access": {
"access_level": 10,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
}
```
### Unarchive a project
Unarchives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus unarchiving an non-archived project will not change the project.
Status code 201 with the project as body is given when successful, in case the user doesn't
have the proper access rights, code 403 is returned. Status 404 is returned if the project
doesn't exist, or is hidden to the user.
```
POST /projects/:id/archive
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project |
```bash
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
```
Example response:
```json
{
"id": 3,
"description": null,
"default_branch": "master",
"public": false,
"visibility_level": 0,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
"tag_list": [
"example",
"disapora project"
],
"owner": {
"id": 3,
"name": "Diaspora",
"created_at": "2013-09-30T13: 46: 02Z"
},
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
"creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
"id": 3,
"name": "Diaspora",
"owner_id": 1,
"path": "diaspora",
"updated_at": "2013-09-30T13: 46: 02Z"
},
"permissions": {
"project_access": {
"access_level": 10,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
}
```
### Remove project
Removes a project including all associated resources (issues, merge requests etc.)
......
......@@ -26,7 +26,6 @@ Example response:
"default_branch_protection" : 2,
"restricted_visibility_levels" : [],
"signin_enabled" : true,
"twitter_sharing_enabled" : true,
"after_sign_out_path" : null,
"max_attachment_size" : 10,
"user_oauth_applications" : true,
......@@ -57,7 +56,6 @@ PUT /application/settings
| `sign_in_text` | string | no | Text on login page |
| `home_page_url` | string | no | Redirect to this URL when not logged in |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. |
| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter |
| `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. |
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
......@@ -85,7 +83,6 @@ Example response:
"updated_at": "2015-06-30T13:22:42.210Z",
"home_page_url": "",
"default_branch_protection": 2,
"twitter_sharing_enabled": true,
"restricted_visibility_levels": [],
"max_attachment_size": 10,
"session_expire_delay": 10080,
......
......@@ -227,9 +227,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab
**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
......
# From 8.6 to 8.7
Make sure you view this update guide from the tag (version) of GitLab you would
like to install. In most cases this should be the highest numbered production
tag (without rc in it). You can select the tag in the version dropdown at the
top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the
[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
guide links by version.
### 1. Stop server
sudo service gitlab stop
### 2. Backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
### 3. Get latest code
```bash
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
For GitLab Community Edition:
```bash
sudo -u git -H git checkout 8-7-stable
```
OR
For GitLab Enterprise Edition:
```bash
sudo -u git -H git checkout 8-7-stable-ee
```
### 4. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all
sudo -u git -H git checkout v2.7.0
```
### 5. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
sudo -u git -H git checkout v0.7.1
sudo -u git -H make
```
### 6. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 7. Update configuration files
#### Nginx configuration
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
# For HTTPS configurations
git diff origin/8-6-stable:lib/support/nginx/gitlab-ssl origin/8-7-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
git diff origin/8-6-stable:lib/support/nginx/gitlab origin/8-7-stable:lib/support/nginx/gitlab
```
If you are using Apache instead of NGINX please see the updated [Apache templates].
Also note that because Apache does not support upstreams behind Unix sockets you
will need to let gitlab-workhorse listen on a TCP port. You can do this
via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-7-stable/lib/support/init.d/gitlab.default.example#L37
#### Init script
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
### 8. Start application
sudo service gitlab start
sudo service nginx restart
### 9. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
To make sure you didn't miss anything run a more thorough check:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.6)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 8.5 to 8.6](8.5-to-8.6.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
......@@ -6,6 +6,7 @@ Feature: Dashboard
And project "Shop" has push event
And project "Shop" has CI enabled
And project "Shop" has CI build
And project "Shop" has labels: "bug", "feature", "enhancement"
And I visit dashboard page
Scenario: I should see projects list
......@@ -50,6 +51,13 @@ Feature: Dashboard
And I visit dashboard issues page
Then The list should be sorted by "Oldest updated"
@javascript
Scenario: Filtering Issues by label
Given project "Shop" has issue "Bugfix1" with label "feature"
When I visit dashboard issues page
And I filter the list by label "feature"
Then I should see "Bugfix1" in issues list
@javascript
Scenario: Visiting Project's issues after sorting
Given I visit dashboard issues page
......
......@@ -7,10 +7,6 @@ Feature: Groups
When I visit group "NonExistentGroup" page
Then page status code should be 404
Scenario: I should have back to group button
When I visit group "Owned" page
Then I should see back to dashboard button
@javascript
Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page
......
......@@ -18,15 +18,6 @@ Feature: Project
Then I should see the default project avatar
And I should not see the "Remove avatar" button
Scenario: I should have back to group button
And project "Shop" belongs to group
And I visit project "Shop" page
Then I should see back to group button
Scenario: I should have back to group button
And I visit project "Shop" page
Then I should see back to dashboard button
Scenario: I should have readme on page
And I visit project "Shop" page
Then I should see project "Shop" README
......
......@@ -87,4 +87,23 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
step 'I should see 1 project at group list' do
expect(find('span.last_activity/span')).to have_content('1')
end
step 'I filter the list by label "feature"' do
page.within ".labels-filter" do
find('.dropdown').click
click_link "feature"
end
end
step 'I should see "Bugfix1" in issues list' do
page.within "ul.content-list" do
expect(page).to have_content "Bugfix1"
end
end
step 'project "Shop" has issue "Bugfix1" with label "feature"' do
project = Project.find_by(name: "Shop")
issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user)
issue.labels << project.labels.find_by(title: 'feature')
end
end
......@@ -5,7 +5,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include SharedUser
step 'I click on group milestones' do
click_link 'Milestones'
page.within '.nav-secondary' do
click_link("Milestones")
end
end
step 'I should see group milestones index page has no milestones' do
......
......@@ -4,10 +4,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include SharedGroup
include SharedUser
step 'I should see back to dashboard button' do
expect(page).to have_content 'Go to dashboard'
end
step 'I should see group "Owned"' do
expect(page).to have_content '@owned'
end
......
......@@ -82,7 +82,9 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Issues
step 'I click the "Milestones" tab' do
click_link('Milestones')
page.within '.nav-secondary' do
click_link('Milestones')
end
end
step 'I click the "Labels" tab' do
......
......@@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I goto the Merge Requests page' do
page.within '.page-sidebar-expanded' do
page.within '.nav-secondary' do
click_link "Merge Requests"
end
end
......
......@@ -326,7 +326,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on diff' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} started a discussion"
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
......@@ -334,7 +334,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion by user "John Doe" has started on diff' do
page.within(".notes .discussion") do
page.should have_content "#{user_exists("John Doe").name} started a discussion"
page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
......@@ -350,7 +350,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on commit diff' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} started a discussion on commit"
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
end
......@@ -358,7 +358,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a discussion has started on commit' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} started a discussion on commit"
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content "One comment to rule them all"
end
end
......
......@@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should not see "Snippets" button' do
expect(page).not_to have_link 'Snippets'
page.within '.nav-secondary' do
expect(page).not_to have_link 'Snippets'
end
end
step 'project "Shop" belongs to group' do
......@@ -123,14 +125,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
@project.save!
end
step 'I should see back to dashboard button' do
expect(page).to have_content 'Go to dashboard'
end
step 'I should see back to group button' do
expect(page).to have_content 'Go to group'
end
step 'I click notifications drop down button' do
click_link 'notifications-button'
end
......
......@@ -41,7 +41,7 @@ module SharedProjectTab
end
step 'the active main tab should be Settings' do
page.within '.nav-sidebar' do
page.within '.nav-secondary' do
expect(page).to have_content('Go to project')
end
end
......
......@@ -8,7 +8,7 @@ module API
expose :id, :state, :avatar_url
expose :web_url do |user, options|
Gitlab::Application.routes.url_helpers.user_url(user)
Gitlab::Routing.url_helpers.user_url(user)
end
end
......@@ -89,7 +89,7 @@ module API
expose :avatar_url
expose :web_url do |group, options|
Gitlab::Application.routes.url_helpers.group_url(group)
Gitlab::Routing.url_helpers.group_url(group)
end
end
......@@ -334,7 +334,6 @@ module API
expose :updated_at
expose :home_page_url
expose :default_branch_protection
expose :twitter_sharing_enabled
expose :restricted_visibility_levels
expose :max_attachment_size
expose :session_expire_delay
......
......@@ -244,6 +244,34 @@ module API
end
end
# Archive project
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# PUT /projects/:id/archive
post ':id/archive' do
authorize!(:archive_project, user_project)
user_project.archive!
present user_project, with: Entities::Project
end
# Unarchive project
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# PUT /projects/:id/unarchive
post ':id/unarchive' do
authorize!(:archive_project, user_project)
user_project.unarchive!
present user_project, with: Entities::Project
end
# Remove project
#
# Parameters:
......
......@@ -11,15 +11,19 @@ module Banzai
end
def self.object_name
object_class.name.underscore
@object_name ||= object_class.name.underscore
end
def self.object_sym
object_name.to_sym
@object_sym ||= object_name.to_sym
end
def self.data_reference
"data-#{object_name.dasherize}"
@data_reference ||= "data-#{object_name.dasherize}"
end
def self.object_class_title
@object_title ||= object_class.name.titleize
end
# Public: Find references in text (like `!123` for merge requests)
......@@ -53,6 +57,10 @@ module Banzai
self.class.object_sym
end
def object_class_title
self.class.object_class_title
end
def references_in(*args, &block)
self.class.references_in(*args, &block)
end
......@@ -62,36 +70,81 @@ module Banzai
# Example: project.merge_requests.find
end
def find_object_cached(project, id)
if RequestStore.active?
cache = find_objects_cache[object_class][project.id]
get_or_set_cache(cache, id) { find_object(project, id) }
else
find_object(project, id)
end
end
def project_from_ref_cache(ref)
if RequestStore.active?
cache = project_refs_cache
get_or_set_cache(cache, ref) { project_from_ref(ref) }
else
project_from_ref(ref)
end
end
def url_for_object(object, project)
# Implement in child class
# Example: project_merge_request_url
end
def call
if object_class.reference_pattern
# `#123`
replace_text_nodes_matching(object_class.reference_pattern) do |content|
object_link_filter(content, object_class.reference_pattern)
end
def url_for_object_cached(object, project)
if RequestStore.active?
cache = url_for_object_cache[object_class][project.id]
# `[Issue](#123)`, which is turned into
# `<a href="#123">Issue</a>`
replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
object_link_filter(link, object_class.reference_pattern, link_text: text)
end
get_or_set_cache(cache, object) { url_for_object(object, project) }
else
url_for_object(object, project)
end
end
if object_class.link_reference_pattern
# `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
# `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
object_link_filter(text, object_class.link_reference_pattern)
end
def call
return doc if project.nil?
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
# `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
# `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
object_link_filter(link, object_class.link_reference_pattern, link_text: text)
each_node do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
end
elsif element_node?(node)
yield_valid_link(node) do |link, text|
if ref_pattern && link =~ /\A#{ref_pattern}/
replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_text: text)
end
next
end
next unless link_pattern
if link == text && text =~ /\A#{link_pattern}/
replace_link_node_with_text(node, link) do
object_link_filter(text, link_pattern)
end
next
end
if link =~ /\A#{link_pattern}\z/
replace_link_node_with_href(node, link) do
object_link_filter(link, link_pattern, link_text: text)
end
next
end
end
end
end
......@@ -109,9 +162,9 @@ module Banzai
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_text: nil)
references_in(text, pattern) do |match, id, project_ref, matches|
project = project_from_ref(project_ref)
project = project_from_ref_cache(project_ref)
if project && object = find_object(project, id)
if project && object = find_object_cached(project, id)
title = object_link_title(object)
klass = reference_class(object_sym)
......@@ -121,8 +174,11 @@ module Banzai
object_sym => object.id
)
url = matches[:url] if matches.names.include?("url")
url ||= url_for_object(object, project)
if matches.names.include?("url") && matches[:url]
url = matches[:url]
else
url = url_for_object_cached(object, project)
end
text = link_text || object_link_text(object, matches)
......@@ -146,7 +202,7 @@ module Banzai
end
def object_link_title(object)
"#{object_class.name.titleize}: #{object.title}"
"#{object_class_title}: #{object.title}"
end
def object_link_text(object, matches)
......@@ -157,6 +213,32 @@ module Banzai
text
end
private
def project_refs_cache
RequestStore[:banzai_project_refs] ||= {}
end
def find_objects_cache
RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key|
hash[key] = Hash.new { |h, k| h[k] = {} }
end
end
def url_for_object_cache
RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key|
hash[key] = Hash.new { |h, k| h[k] = {} }
end
end
def get_or_set_cache(cache, key)
if cache.key?(key)
cache[key]
else
cache[key] = yield
end
end
end
end
end
......@@ -43,7 +43,7 @@ module Banzai
end
def url_for_object(range, project)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path]))
end
......
......@@ -37,7 +37,7 @@ module Banzai
end
def url_for_object(commit, project)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path])
end
......
......@@ -35,15 +35,29 @@ module Banzai
def call
# Early return if the project isn't using an external tracker
return doc if project.nil? || project.default_issues_tracker?
return doc if project.nil? || default_issues_tracker?
replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content|
issue_link_filter(content)
end
ref_pattern = ExternalIssue.reference_pattern
ref_start_pattern = /\A#{ref_pattern}\z/
each_node do |node|
if text_node?(node)
replace_text_when_pattern_matches(node, ref_pattern) do |content|
issue_link_filter(content)
end
replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text|
issue_link_filter(link, link_text: text)
elsif element_node?(node)
yield_valid_link(node) do |link, text|
if link =~ ref_start_pattern
replace_link_node_with_href(node, link) do
issue_link_filter(link, link_text: text)
end
end
end
end
end
doc
end
# Replace `JIRA-123` issue references in text with links to the referenced
......@@ -76,6 +90,21 @@ module Banzai
def url_for_issue(*args)
IssuesHelper.url_for_issue(*args)
end
def default_issues_tracker?
if RequestStore.active?
default_issues_tracker_cache[project.id] ||=
project.default_issues_tracker?
else
project.default_issues_tracker?
end
end
private
def default_issues_tracker_cache
RequestStore[:banzai_default_issues_tracker_cache] ||= {}
end
end
end
end
......@@ -31,7 +31,7 @@ module Banzai
end
def url_for_object(label, project)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
h.namespace_project_issues_url(project.namespace, project, label_name: label.name,
only_path: context[:only_path])
end
......
......@@ -14,7 +14,7 @@ module Banzai
end
def url_for_object(mr, project)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
h.namespace_project_merge_request_url(project.namespace, project, mr,
only_path: context[:only_path])
end
......
......@@ -11,7 +11,7 @@ module Banzai
end
def url_for_object(issue, project)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
h.namespace_project_milestone_url(project.namespace, project, milestone,
only_path: context[:only_path])
end
......
......@@ -52,18 +52,13 @@ module Banzai
html.html_safe? ? html : ERB::Util.html_escape_once(html)
end
def ignore_parents
@ignore_parents ||= begin
# Don't look for references in text nodes that are children of these
# elements.
def ignore_ancestor_query
@ignore_ancestor_query ||= begin
parents = %w(pre code a style)
parents << 'blockquote' if context[:ignore_blockquotes]
parents.to_set
end
end
def ignored_ancestry?(node)
has_ancestor?(node, ignore_parents)
parents.map { |n| "ancestor::#{n}" }.join(' or ')
end
end
def project
......@@ -74,119 +69,66 @@ module Banzai
"gfm gfm-#{type}"
end
# Iterate through the document's text nodes, yielding the current node's
# content if:
#
# * The `project` context value is present AND
# * The node's content matches `pattern` AND
# * The node is not an ancestor of an ignored node type
#
# pattern - Regex pattern against which to match the node's content
#
# Yields the current node's String contents. The result of the block will
# replace the node's existing content and update the current document.
# Ensure that a :project key exists in context
#
# Returns the updated Nokogiri::HTML::DocumentFragment object.
def replace_text_nodes_matching(pattern)
return doc if project.nil?
search_text_nodes(doc).each do |node|
next if ignored_ancestry?(node)
next unless node.text =~ pattern
content = node.to_html
html = yield content
next if html == content
node.replace(html)
end
doc
# Note that while the key might exist, its value could be nil!
def validate
needs :project
end
# Iterate through the document's link nodes, yielding the current node's
# content if:
#
# * The `project` context value is present AND
# * The node's content matches `pattern`
#
# pattern - Regex pattern against which to match the node's content
#
# Yields the current node's String contents. The result of the block will
# replace the node and update the current document.
# Iterates over all <a> and text() nodes in a document.
#
# Returns the updated Nokogiri::HTML::DocumentFragment object.
def replace_link_nodes_with_text(pattern)
return doc if project.nil?
# Nodes are skipped whenever their ancestor is one of the nodes returned
# by `ignore_ancestor_query`. Link tags are not processed if they have a
# "gfm" class or the "href" attribute is empty.
def each_node
query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
| descendant-or-self::a[
not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
]}
doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
link = node.attr('href')
text = node.text
next unless link && text
link = CGI.unescape(link)
next unless link.force_encoding('UTF-8').valid_encoding?
# Ignore ending punctionation like periods or commas
next unless link == text && text =~ /\A#{pattern}/
html = yield text
doc.xpath(query).each do |node|
yield node
end
end
next if html == text
# Yields the link's URL and text whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = CGI.unescape(node.attr('href').to_s)
text = node.text
node.replace(html)
end
return unless link.force_encoding('UTF-8').valid_encoding?
doc
yield link, text
end
# Iterate through the document's link nodes, yielding the current node's
# content if:
#
# * The `project` context value is present AND
# * The node's HREF matches `pattern`
#
# pattern - Regex pattern against which to match the node's HREF
#
# Yields the current node's String HREF and String content.
# The result of the block will replace the node and update the current document.
#
# Returns the updated Nokogiri::HTML::DocumentFragment object.
def replace_link_nodes_with_href(pattern)
return doc if project.nil?
def replace_text_when_pattern_matches(node, pattern)
return unless node.text =~ pattern
doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
content = node.to_html
html = yield content
link = node.attr('href')
text = node.text
node.replace(html) unless content == html
end
next unless link && text
link = CGI.unescape(link)
next unless link.force_encoding('UTF-8').valid_encoding?
next unless link && link =~ /\A#{pattern}\z/
def replace_link_node_with_text(node, link)
html = yield
html = yield link, text
node.replace(html) unless html == node.text
end
next if html == link
def replace_link_node_with_href(node, link)
html = yield
node.replace(html)
end
node.replace(html) unless html == link
end
doc
def text_node?(node)
node.is_a?(Nokogiri::XML::Text)
end
# Ensure that a :project key exists in context
#
# Note that while the key might exist, its value could be nil!
def validate
needs :project
def element_node?(node)
node.is_a?(Nokogiri::XML::Element)
end
end
end
......
......@@ -14,7 +14,7 @@ module Banzai
end
def url_for_object(snippet, project)
h = Gitlab::Application.routes.url_helpers
h = Gitlab::Routing.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet,
only_path: context[:only_path])
end
......
......@@ -59,13 +59,28 @@ module Banzai
end
def call
replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content)
return doc if project.nil?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
each_node do |node|
if text_node?(node)
replace_text_when_pattern_matches(node, ref_pattern) do |content|
user_link_filter(content)
end
elsif element_node?(node)
yield_valid_link(node) do |link, text|
if link =~ ref_pattern_start
replace_link_node_with_href(node, link) do
user_link_filter(link, link_text: text)
end
end
end
end
end
replace_link_nodes_with_href(User.reference_pattern) do |link, text|
user_link_filter(link, link_text: text)
end
doc
end
# Replace `@user` user references in text with links to the referenced
......@@ -90,7 +105,7 @@ module Banzai
private
def urls
Gitlab::Application.routes.url_helpers
Gitlab::Routing.url_helpers
end
def link_class
......
......@@ -21,7 +21,6 @@ module Gitlab
default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
......
......@@ -5,7 +5,7 @@ module Gitlab
attr_accessor :recipient
attr_reader :author_id, :ref, :action
include Gitlab::Application.routes.url_helpers
include Gitlab::Routing.url_helpers
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author
......
......@@ -26,7 +26,7 @@ module Gitlab
def user_map
users = {}
res = @api.command(:listPeople)
res['people']['person'].each do |user|
[res['people']['person']].flatten.each do |user|
users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] }
end
users
......
......@@ -34,16 +34,21 @@ module Gitlab
@source_project = source_project
@current_user = current_user
@original_html = markdown(text)
@pattern = Gitlab::ReferenceExtractor.references_pattern
end
def rewrite(target_project)
pattern = Gitlab::ReferenceExtractor.references_pattern
return @text unless needs_rewrite?
@text.gsub(pattern) do |reference|
@text.gsub(@pattern) do |reference|
unfold_reference(reference, Regexp.last_match, target_project)
end
end
def needs_rewrite?
@text =~ @pattern
end
private
def unfold_reference(reference, match, target_project)
......
module Gitlab
module Gfm
##
# Class that rewrites markdown links for uploads
#
# Using a pattern defined in `FileUploader` it copies files to a new
# project and rewrites all links to uploads in in a given text.
#
#
class UploadsRewriter
def initialize(text, source_project, _current_user)
@text = text
@source_project = source_project
@pattern = FileUploader::MARKDOWN_PATTERN
end
def rewrite(target_project)
return @text unless needs_rewrite?
@text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
return markdown unless file.try(:exists?)
new_uploader = FileUploader.new(target_project)
new_uploader.store!(file)
new_uploader.to_markdown
end
end
def needs_rewrite?
files.any?
end
def files
referenced_files = @text.scan(@pattern).map do
find_file(@source_project, $~[:secret], $~[:file])
end
referenced_files.compact.select(&:exists?)
end
private
def find_file(project, secret, file)
uploader = FileUploader.new(project, secret)
uploader.retrieve_from_store!(file)
uploader.file
end
end
end
end
......@@ -41,7 +41,7 @@ module Gitlab
data[:issue] = note.noteable.hook_attrs
elsif note.for_merge_request?
data[:merge_request] = note.noteable.hook_attrs
elsif note.for_project_snippet?
elsif note.for_snippet?
data[:snippet] = note.noteable.hook_attrs
end
......
module Gitlab
module Routing
# Returns the URL helpers Module.
#
# This method caches the output as Rails' "url_helpers" method creates an
# anonymous module every time it's called.
#
# Returns a Module.
def self.url_helpers
@url_helpers ||= Gitlab::Application.routes.url_helpers
end
end
end
module Gitlab
class UrlBuilder
include Gitlab::Application.routes.url_helpers
include Gitlab::Routing.url_helpers
include GitlabRoutingHelper
include ActionView::RecordIdentifier
def initialize(type)
@type = type
......@@ -37,19 +38,16 @@ module Gitlab
namespace_project_commit_url(namespace_id: note.project.namespace,
id: note.commit_id,
project_id: note.project,
anchor: "note_#{note.id}")
anchor: dom_id(note))
elsif note.for_issue?
issue = Issue.find(note.noteable_id)
issue_url(issue,
anchor: "note_#{note.id}")
issue_url(issue, anchor: dom_id(note))
elsif note.for_merge_request?
merge_request = MergeRequest.find(note.noteable_id)
merge_request_url(merge_request,
anchor: "note_#{note.id}")
elsif note.for_project_snippet?
merge_request_url(merge_request, anchor: dom_id(note))
elsif note.for_snippet?
snippet = Snippet.find(note.noteable_id)
project_snippet_url(snippet,
anchor: "note_#{note.id}")
project_snippet_url(snippet, anchor: dom_id(note))
end
end
end
......
require 'spec_helper'
describe Admin::UsersController do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
before do
sign_in(admin)
sign_in(create(:admin))
end
describe 'DELETE #user with projects' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :developer]
......@@ -23,8 +22,6 @@ describe Admin::UsersController do
end
describe 'PUT block/:id' do
let(:user) { create(:user) }
it 'blocks user' do
put :block, id: user.username
user.reload
......@@ -50,8 +47,6 @@ describe Admin::UsersController do
end
context 'manually blocked users' do
let(:user) { create(:user) }
before do
user.block
end
......@@ -66,8 +61,6 @@ describe Admin::UsersController do
end
describe 'PUT unlock/:id' do
let(:user) { create(:user) }
before do
request.env["HTTP_REFERER"] = "/"
user.lock_access!
......@@ -95,8 +88,6 @@ describe Admin::UsersController do
end
describe 'PATCH disable_two_factor' do
let(:user) { create(:user) }
it 'disables 2FA for the user' do
expect(user).to receive(:disable_two_factor!)
allow(subject).to receive(:user).and_return(user)
......
FactoryGirl.define do
factory :file_uploader do
project
secret nil
transient do
fixture { 'rails_sample.jpg' }
path { File.join(Rails.root, 'spec/fixtures', fixture) }
file { Rack::Test::UploadedFile.new(path) }
end
after(:build) do |uploader, evaluator|
uploader.store!(evaluator.file)
end
initialize_with do
new(project, secret)
end
end
end
......@@ -13,5 +13,10 @@ FactoryGirl.define do
factory :forked_project_link do
association :forked_to_project, factory: :project
association :forked_from_project, factory: :project
after(:create) do |link|
link.forked_from_project.reload
link.forked_to_project.reload
end
end
end
require 'spec_helper'
FactoryGirl.factories.map(&:name).each do |factory_name|
describe "#{factory_name} factory" do
it 'should be valid' do
expect(build(factory_name)).to be_valid
describe 'factories' do
FactoryGirl.factories.each do |factory|
describe "#{factory.name} factory" do
let(:entity) { build(factory.name) }
it 'does not raise error when created 'do
expect { entity }.to_not raise_error
end
it 'should be valid', if: factory.build_class < ActiveRecord::Base do
expect(entity).to be_valid
end
end
end
end
require 'spec_helper'
feature 'Create New Merge Request', feature: true, js: false do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
before do
project.team << [user, :master]
login_as user
visit namespace_project_merge_requests_path(project.namespace, project)
end
it 'generates a diff for an orphaned branch' do
click_link 'New Merge Request'
select "orphaned-branch", from: "merge_request_source_branch"
select "master", from: "merge_request_target_branch"
click_button "Compare branches"
expect(page).to have_content "README.md"
expect(page).to have_content "wm.png"
fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Submit merge request"
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
end
require 'spec_helper'
describe "Search", feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
before do
login_as :user
@project = create(:project, namespace: @user.namespace)
@project.team << [@user, :reporter]
login_with(user)
project.team << [user, :reporter]
visit search_path
end
page.within '.search-holder' do
fill_in "search", with: @project.name[0..3]
click_button "Search"
describe 'searching for Projects' do
it 'finds a project' do
page.within '.search-holder' do
fill_in "search", with: project.name[0..3]
click_button "Search"
end
expect(page).to have_content project.name
end
end
it "should show project in search results" do
expect(page).to have_content @project.name
context 'search for comments' do
it 'finds a snippet' do
snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
note = create(:note,
noteable: snippet,
author: user,
note: 'Supercalifragilisticexpialidocious',
project: project)
# Must visit project dashboard since global search won't search
# everything (e.g. comments, snippets, etc.)
visit namespace_project_path(project.namespace, project)
page.within '.search' do
fill_in 'search', with: note.note
click_button 'Go'
end
click_link 'Comments'
expect(page).to have_link(snippet.title)
end
end
end
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe ExtractsPath, lib: true do
include ExtractsPath
include RepoHelpers
include Gitlab::Application.routes.url_helpers
include Gitlab::Routing.url_helpers
let(:project) { double('project') }
......
......@@ -236,6 +236,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
def urls
Gitlab::Application.routes.url_helpers
Gitlab::Routing.url_helpers
end
end
require 'spec_helper'
describe Gitlab::FogbugzImport::Client, lib: true do
let(:client) { described_class.new(uri: '', token: '') }
let(:one_user) { { 'people' => { 'person' => { "ixPerson" => "2", "sFullName" => "James" } } } }
let(:two_users) { { 'people' => { 'person' => [one_user, { "ixPerson" => "3" }] } } }
it 'retrieves user_map with one user' do
stub_api(one_user)
expect(client.user_map.count).to eq(1)
end
it 'retrieves user_map with two users' do
stub_api(two_users)
expect(client.user_map.count).to eq(2)
end
def stub_api(users)
allow_any_instance_of(::Fogbugz::Interface).to receive(:command).with(:listPeople).and_return(users)
end
end
require 'spec_helper'
describe Gitlab::Gfm::UploadsRewriter do
let(:user) { create(:user) }
let(:old_project) { create(:project) }
let(:new_project) { create(:project) }
let(:rewriter) { described_class.new(text, old_project, user) }
context 'text contains links to uploads' do
let(:image_uploader) do
build(:file_uploader, project: old_project)
end
let(:zip_uploader) do
build(:file_uploader, project: old_project,
fixture: 'ci_build_artifacts.zip')
end
let(:text) do
"Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}"
end
describe '#rewrite' do
let!(:new_text) { rewriter.rewrite(new_project) }
let(:old_files) { [image_uploader, zip_uploader].map(&:file) }
let(:new_files) do
described_class.new(new_text, new_project, user).files
end
let(:old_paths) { old_files.map(&:path) }
let(:new_paths) { new_files.map(&:path) }
it 'rewrites content' do
expect(new_text).to_not eq text
expect(new_text.length).to eq text.length
end
it 'copies files' do
expect(new_files).to all(exist)
expect(old_paths).to_not match_array new_paths
expect(old_paths).to all(include(old_project.path_with_namespace))
expect(new_paths).to all(include(new_project.path_with_namespace))
end
it 'does not remove old files' do
expect(old_files).to all(exist)
end
it 'generates a new secret for each file' do
expect(new_paths).to_not include image_uploader.secret
expect(new_paths).to_not include zip_uploader.secret
end
end
describe '#needs_rewrite?' do
subject { rewriter.needs_rewrite? }
it { is_expected.to eq true }
end
describe '#files' do
subject { rewriter.files }
it { is_expected.to be_an(Array) }
end
end
end
......@@ -12,7 +12,6 @@
# updated_at :datetime
# home_page_url :string(255)
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null
......
......@@ -9,6 +9,7 @@ describe Issue, "Issuable" do
it { is_expected.to belong_to(:author) }
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
end
describe "Validation" do
......
......@@ -104,6 +104,15 @@ describe Project, models: true do
end
end
describe 'default_scope' do
it 'excludes projects pending deletion from the results' do
project = create(:empty_project)
create(:empty_project, pending_delete: true)
expect(Project.all).to eq [project]
end
end
describe 'project token' do
it 'should set an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
......
......@@ -303,7 +303,7 @@ describe Repository, models: true do
describe 'when there are no branches' do
before do
allow(repository.raw_repository).to receive(:branch_count).and_return(0)
allow(repository).to receive(:branch_count).and_return(0)
end
it { is_expected.to eq(false) }
......@@ -311,13 +311,13 @@ describe Repository, models: true do
describe 'when there are branches' do
it 'returns true' do
expect(repository.raw_repository).to receive(:branch_count).and_return(3)
expect(repository).to receive(:branch_count).and_return(3)
expect(subject).to eq(true)
end
it 'caches the output' do
expect(repository.raw_repository).to receive(:branch_count).
expect(repository).to receive(:branch_count).
once.
and_return(3)
......@@ -436,7 +436,7 @@ describe Repository, models: true do
it 'expires the visible content cache' do
repository.has_visible_content?
expect(repository.raw_repository).to receive(:branch_count).
expect(repository).to receive(:branch_count).
once.
and_return(0)
......@@ -865,4 +865,21 @@ describe Repository, models: true do
repository.build_cache
end
end
describe '#local_branches' do
it 'returns the local branches' do
masterrev = repository.find_branch('master').target
create_remote_branch('joe', 'remote_branch', masterrev)
repository.add_branch(user, 'local_branch', masterrev)
expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
end
end
def create_remote_branch(remote_name, branch_name, target)
rugged = repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
end
end
......@@ -173,6 +173,13 @@ describe User, models: true do
expect(user).to be_invalid
end
end
context 'owns_notification_email' do
it 'accepts temp_oauth_email emails' do
user = build(:user, email: "temp-email-for-oauth@example.com")
expect(user).to be_valid
end
end
end
end
......
......@@ -948,6 +948,78 @@ describe API::API, api: true do
end
end
describe 'POST /projects/:id/archive' do
context 'on an unarchived project' do
it 'archives the project' do
post api("/projects/#{project.id}/archive", user)
expect(response.status).to eq(201)
expect(json_response['archived']).to be_truthy
end
end
context 'on an archived project' do
before do
project.archive!
end
it 'remains archived' do
post api("/projects/#{project.id}/archive", user)
expect(response.status).to eq(201)
expect(json_response['archived']).to be_truthy
end
end
context 'user without archiving rights to the project' do
before do
project.team << [user3, :developer]
end
it 'rejects the action' do
post api("/projects/#{project.id}/archive", user3)
expect(response.status).to eq(403)
end
end
end
describe 'POST /projects/:id/unarchive' do
context 'on an unarchived project' do
it 'remains unarchived' do
post api("/projects/#{project.id}/unarchive", user)
expect(response.status).to eq(201)
expect(json_response['archived']).to be_falsey
end
end
context 'on an archived project' do
before do
project.archive!
end
it 'unarchives the project' do
post api("/projects/#{project.id}/unarchive", user)
expect(response.status).to eq(201)
expect(json_response['archived']).to be_falsey
end
end
context 'user without archiving rights to the project' do
before do
project.team << [user3, :developer]
end
it 'rejects the action' do
post api("/projects/#{project.id}/unarchive", user3)
expect(response.status).to eq(403)
end
end
end
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
it 'should remove project' do
......
......@@ -160,6 +160,20 @@ describe Issues::MoveService, services: true do
.to eq "Note with reference to merge request #{old_project.to_reference}!1"
end
end
context 'issue description with uploads' do
let(:uploader) { build(:file_uploader, project: old_project) }
let(:description) { "Text and #{uploader.to_markdown}" }
include_context 'issue move executed'
it 'rewrites uploads in description' do
expect(new_issue.description).to_not eq description
expect(new_issue.description)
.to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
expect(new_issue.description).to_not include uploader.secret
end
end
end
describe 'rewritting references' do
......
require 'spec_helper'
describe Projects::UnlinkForkService, services: true do
subject { Projects::UnlinkForkService.new(fork_project, user) }
let(:fork_link) { create(:forked_project_link) }
let(:fork_project) { fork_link.forked_to_project }
let(:user) { create(:user) }
context 'with opened merge request on the source project' do
let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) }
let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) }
before do
allow(MergeRequests::CloseService).to receive(:new).
with(fork_project, user).
and_return(mr_close_service)
end
it 'close all pending merge requests' do
expect(mr_close_service).to receive(:execute).with(merge_request)
subject.execute
end
end
it 'remove fork relation' do
expect(fork_project.forked_project_link).to receive(:destroy)
subject.execute
end
end
......@@ -2,22 +2,25 @@ require 'spec_helper'
describe TodoService, services: true do
let(:author) { create(:user) }
let(:john_doe) { create(:user, username: 'john_doe') }
let(:michael) { create(:user, username: 'michael') }
let(:stranger) { create(:user, username: 'stranger') }
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') }
let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
project.team << [author, :developer]
project.team << [member, :developer]
project.team << [john_doe, :developer]
project.team << [michael, :developer]
end
describe 'Issues' do
let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
describe '#new_issue' do
it 'creates a todo if assigned' do
......@@ -37,10 +40,20 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_issue(issue, author)
should_create_todo(user: michael, target: issue, action: Todo::MENTIONED)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
it 'does not create todo for non project members when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
end
......@@ -48,16 +61,26 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.update_issue(issue, author)
should_create_todo(user: michael, target: issue, action: Todo::MENTIONED)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
it 'does not create a todo if user was already mentioned' do
create(:todo, :mentioned, user: michael, project: project, target: issue, author: author)
create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count)
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
it 'does not create todo for non project members when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
end
......@@ -109,8 +132,10 @@ describe TodoService, services: true do
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
......@@ -142,19 +167,29 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_note(note, john_doe)
should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
it 'does not create todo for non project members when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
it 'creates a todo for each valid mentioned user when leaving a note on commit' do
service.new_note(note_on_commit, john_doe)
should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
it 'does not create todo when leaving a note on snippet' do
......@@ -185,10 +220,10 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
end
......@@ -196,16 +231,16 @@ describe TodoService, services: true do
it 'creates a todo for each valid mentioned user' do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
it 'does not create a todo if user was already mentioned' do
create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author)
create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count)
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
end
......
......@@ -78,6 +78,6 @@ module FilterSpecHelper
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
Gitlab::Application.routes.url_helpers
Gitlab::Routing.url_helpers
end
end
......@@ -106,7 +106,7 @@ class MarkdownFeature
end
def urls
Gitlab::Application.routes.url_helpers
Gitlab::Routing.url_helpers
end
def raw_markdown
......
......@@ -15,6 +15,7 @@ module TestEnv
'lfs' => 'be93687',
'master' => '5937ac0',
"'test'" => 'e56497b',
'orphaned-branch' => '45127a9',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
......
......@@ -22,6 +22,8 @@ describe MergeWorker do
merge_request.reload
expect(merge_request).to be_merged
source_project.repository.expire_branches_cache
expect(source_project.repository.branch_names).not_to include('markdown')
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment